summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/editor/editor.go20
-rw-r--r--internal/editor/editor_test.go42
-rw-r--r--internal/hexaicli/runner.go25
-rw-r--r--internal/hexaicli/runner_test.go55
-rw-r--r--internal/version.go2
5 files changed, 143 insertions, 1 deletions
diff --git a/internal/editor/editor.go b/internal/editor/editor.go
index a1af1be..722e336 100644
--- a/internal/editor/editor.go
+++ b/internal/editor/editor.go
@@ -68,3 +68,23 @@ func OpenTempAndEdit(initial []byte) (string, error) {
}
return strings.TrimSpace(string(b)), nil
}
+
+// OpenFile ensures the parent directory exists, then opens path in the editor
+// from Resolve() (HEXAI_EDITOR or EDITOR).
+func OpenFile(path string) error {
+ ed, err := Resolve()
+ if err != nil {
+ return err
+ }
+ path = filepath.Clean(strings.TrimSpace(path))
+ if path == "" || path == "." {
+ return errors.New("config path is empty")
+ }
+ dir := filepath.Dir(path)
+ if dir != "" && dir != "." {
+ if mkErr := os.MkdirAll(dir, 0o700); mkErr != nil {
+ return mkErr
+ }
+ }
+ return RunEditor(ed, path)
+}
diff --git a/internal/editor/editor_test.go b/internal/editor/editor_test.go
index 260fb85..403d165 100644
--- a/internal/editor/editor_test.go
+++ b/internal/editor/editor_test.go
@@ -182,3 +182,45 @@ func TestOpenTempAndEdit_TempFileCleanup(t *testing.T) {
t.Fatalf("temp file was not cleaned up: %s", capturedPath)
}
}
+
+func TestOpenFile_CreatesParentAndInvokesEditor(t *testing.T) {
+ old := RunEditor
+ t.Cleanup(func() { RunEditor = old })
+ t.Setenv("HEXAI_EDITOR", "dummy")
+ target := filepath.Join(t.TempDir(), "nested", "config.toml")
+ var gotEditor, gotPath string
+ RunEditor = func(editorCmd, path string) error {
+ gotEditor = editorCmd
+ gotPath = path
+ return nil
+ }
+ if err := OpenFile(target); err != nil {
+ t.Fatalf("OpenFile: %v", err)
+ }
+ if gotEditor != "dummy" {
+ t.Fatalf("editor = %q", gotEditor)
+ }
+ if gotPath != target {
+ t.Fatalf("path = %q, want %q", gotPath, target)
+ }
+ if _, err := os.Stat(filepath.Dir(target)); err != nil {
+ t.Fatalf("parent dir: %v", err)
+ }
+}
+
+func TestOpenFile_NoEditor(t *testing.T) {
+ t.Setenv("HEXAI_EDITOR", "")
+ t.Setenv("EDITOR", "")
+ err := OpenFile(filepath.Join(t.TempDir(), "x.toml"))
+ if err == nil {
+ t.Fatal("expected error when no editor is set")
+ }
+}
+
+func TestOpenFile_EmptyPath(t *testing.T) {
+ t.Setenv("HEXAI_EDITOR", "true")
+ err := OpenFile(" ")
+ if err == nil {
+ t.Fatal("expected error for empty path")
+ }
+}
diff --git a/internal/hexaicli/runner.go b/internal/hexaicli/runner.go
index f372021..eaae1cd 100644
--- a/internal/hexaicli/runner.go
+++ b/internal/hexaicli/runner.go
@@ -67,6 +67,31 @@ func NewRunner() *Runner {
func (r *Runner) Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
runner := normalizeRunner(r)
+ if len(args) > 0 {
+ sub := args[0]
+ if sub == "config" {
+ if len(args) > 1 {
+ err := fmt.Errorf(`hexai %s: unexpected arguments (use only %q)`, sub, sub)
+ _, _ = fmt.Fprintf(stderr, logging.AnsiBase+"%v"+logging.AnsiReset+"\n", err)
+ return err
+ }
+ cfgPath := strings.TrimSpace(configPathFromContext(ctx))
+ if cfgPath == "" {
+ p, pathErr := appconfig.ConfigPath()
+ if pathErr != nil {
+ err := fmt.Errorf("hexai %s: %w", sub, pathErr)
+ _, _ = fmt.Fprintf(stderr, logging.AnsiBase+"%v"+logging.AnsiReset+"\n", err)
+ return err
+ }
+ cfgPath = p
+ }
+ if err := editor.OpenFile(cfgPath); err != nil {
+ _, _ = fmt.Fprintf(stderr, logging.AnsiBase+"hexai %s: %v"+logging.AnsiReset+"\n", sub, err)
+ return err
+ }
+ return nil
+ }
+ }
if spec, ok, err := tpsSimulationFromContext(ctx); err != nil {
_, _ = fmt.Fprintln(stderr, logging.AnsiBase+err.Error()+logging.AnsiReset)
return err
diff --git a/internal/hexaicli/runner_test.go b/internal/hexaicli/runner_test.go
index 1d438b0..009af54 100644
--- a/internal/hexaicli/runner_test.go
+++ b/internal/hexaicli/runner_test.go
@@ -4,10 +4,12 @@ import (
"bytes"
"context"
"log"
+ "path/filepath"
"strings"
"testing"
"codeberg.org/snonux/hexai/internal/appconfig"
+ "codeberg.org/snonux/hexai/internal/editor"
"codeberg.org/snonux/hexai/internal/llm"
"codeberg.org/snonux/hexai/internal/stats"
)
@@ -59,3 +61,56 @@ func TestRunner_UsesInjectedDependencies(t *testing.T) {
t.Fatalf("expected one global status update, got %d", sink.globalCalls)
}
}
+
+func TestRunner_ConfigSubcommand_OpensConfigFromContext(t *testing.T) {
+ old := editor.RunEditor
+ t.Cleanup(func() { editor.RunEditor = old })
+ t.Setenv("EDITOR", "true")
+ var gotPath string
+ editor.RunEditor = func(_, path string) error {
+ gotPath = path
+ return nil
+ }
+ cfgFile := filepath.Join(t.TempDir(), "hexai", "config.toml")
+ ctx := WithCLIConfigPath(context.Background(), cfgFile)
+ runner := NewRunner()
+ if err := runner.Run(ctx, []string{"config"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
+ t.Fatalf("Run: %v", err)
+ }
+ if gotPath != cfgFile {
+ t.Fatalf("opened %q, want %q", gotPath, cfgFile)
+ }
+}
+
+func TestRunner_ConfigSubcommand_UsesXDGWhenNoOverride(t *testing.T) {
+ old := editor.RunEditor
+ t.Cleanup(func() { editor.RunEditor = old })
+ t.Setenv("HEXAI_EDITOR", "true")
+ xdg := t.TempDir()
+ t.Setenv("XDG_CONFIG_HOME", xdg)
+ var gotPath string
+ editor.RunEditor = func(_, path string) error {
+ gotPath = path
+ return nil
+ }
+ runner := NewRunner()
+ want := filepath.Join(xdg, "hexai", "config.toml")
+ if err := runner.Run(context.Background(), []string{"config"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
+ t.Fatalf("Run: %v", err)
+ }
+ if gotPath != want {
+ t.Fatalf("opened %q, want %q", gotPath, want)
+ }
+}
+
+func TestRunner_ConfigSubcommand_RejectsExtraArgs(t *testing.T) {
+ runner := NewRunner()
+ var stderr bytes.Buffer
+ err := runner.Run(context.Background(), []string{"config", "nope"}, strings.NewReader(""), &bytes.Buffer{}, &stderr)
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ if !strings.Contains(err.Error(), "unexpected arguments") {
+ t.Fatalf("err = %v", err)
+ }
+}
diff --git a/internal/version.go b/internal/version.go
index 4fa574f..4305500 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -1,4 +1,4 @@
// Package internal provides the Hexai semantic version identifier used by CLI and LSP binaries.
package internal
-const Version = "0.38.3"
+const Version = "0.38.4"