diff --git a/cmd/harness.go b/cmd/harness.go index fe46b65..ec6f803 100644 --- a/cmd/harness.go +++ b/cmd/harness.go @@ -31,9 +31,6 @@ func runHarness(cmd *cobra.Command, args []string) error { return fmt.Errorf("coding harness is not installed and update checks are disabled — " + "unset KIMCHI_NO_UPDATE_CHECK or install the harness manually") } - if err := guardAgainstSelfExec(harnessPath); err != nil { - return err - } return launchHarness(cmd, harnessPath, args) } @@ -49,10 +46,6 @@ func runHarness(cmd *cobra.Command, args []string) error { return nil } - if err := guardAgainstSelfExec(harnessPath); err != nil { - return err - } - var props map[string]any if v, err := update.HarnessCurrentVersion(ctx); err == nil && v != nil { props = map[string]any{"version": v.String()} @@ -62,33 +55,6 @@ func runHarness(cmd *cobra.Command, args []string) error { return launchHarness(cmd, harnessPath, args) } -// guardAgainstSelfExec refuses to launch the harness if its resolved path -// points at the running CLI binary. Without this check a misconfigured path -// would cause syscall.Exec to replace the CLI with itself, producing an -// infinite launch loop instead of a clear failure. -func guardAgainstSelfExec(harnessPath string) error { - selfPath, err := update.ResolveExecutablePath() - if err != nil { - klog.V(1).ErrorS(err, "self-exec guard: could not resolve current executable; skipping check") - return nil - } - selfInfo, err := os.Stat(selfPath) - if err != nil { - klog.V(1).ErrorS(err, "self-exec guard: stat self failed; skipping check", "path", selfPath) - return nil - } - harnessInfo, err := os.Stat(harnessPath) - if err != nil { - klog.V(1).ErrorS(err, "self-exec guard: stat harness failed; skipping check", "path", harnessPath) - return nil - } - if os.SameFile(selfInfo, harnessInfo) { - return fmt.Errorf("refusing to launch harness: resolved harness path %q is the kimchi CLI itself "+ - "(this would loop forever). Remove the stray file and reinstall: rm %q && kimchi update", harnessPath, harnessPath) - } - return nil -} - // launchHarness resolves the API key (prompting if missing), ensures it is // saved in the config file, and execs into the harness binary. func launchHarness(cmd *cobra.Command, harnessPath string, args []string) error { diff --git a/internal/update/extract_test.go b/internal/update/extract_test.go index adb0086..64f14ba 100644 --- a/internal/update/extract_test.go +++ b/internal/update/extract_test.go @@ -17,18 +17,18 @@ func TestExtractStructuredArchive_SplitsBinAndData(t *testing.T) { archive := createArchive(t, []archiveFile{ {Name: "bin", IsDir: true}, - {Name: "bin/kimchi", Content: binaryContent, Mode: 0755}, + {Name: "bin/kimchi-code", Content: binaryContent, Mode: 0755}, {Name: "share/kimchi", IsDir: true}, {Name: "share/kimchi/package.json", Content: packageJSON}, {Name: "share/kimchi/theme", IsDir: true}, {Name: "share/kimchi/theme/dark.json", Content: themeContent}, }) - root, err := extractStructuredArchive(bytes.NewReader(archive), "kimchi") + root, err := extractStructuredArchive(bytes.NewReader(archive), "kimchi-code") require.NoError(t, err) defer func() { _ = os.RemoveAll(root) }() - gotBinary, err := os.ReadFile(filepath.Join(root, "bin", "kimchi")) + gotBinary, err := os.ReadFile(filepath.Join(root, "bin", "kimchi-code")) require.NoError(t, err) assert.Equal(t, binaryContent, gotBinary) @@ -46,39 +46,39 @@ func TestExtractStructuredArchive_MissingBinary(t *testing.T) { {Name: "share/kimchi/package.json", Content: []byte(`{}`)}, }) - _, err := extractStructuredArchive(bytes.NewReader(archive), "kimchi") + _, err := extractStructuredArchive(bytes.NewReader(archive), "kimchi-code") require.Error(t, err) - assert.ErrorContains(t, err, "kimchi") + assert.ErrorContains(t, err, "kimchi-code") assert.ErrorContains(t, err, "not found") } func TestExtractStructuredArchive_PreservesFilePermissions(t *testing.T) { archive := createArchive(t, []archiveFile{ - {Name: "bin/kimchi", Content: []byte("binary"), Mode: 0755}, + {Name: "bin/kimchi-code", Content: []byte("binary"), Mode: 0755}, {Name: "share/kimchi/package.json", Content: []byte(`{}`)}, }) - root, err := extractStructuredArchive(bytes.NewReader(archive), "kimchi") + root, err := extractStructuredArchive(bytes.NewReader(archive), "kimchi-code") require.NoError(t, err) defer func() { _ = os.RemoveAll(root) }() - info, err := os.Stat(filepath.Join(root, "bin", "kimchi")) + info, err := os.Stat(filepath.Join(root, "bin", "kimchi-code")) require.NoError(t, err) assert.Equal(t, os.FileMode(0755), info.Mode().Perm()) } func TestExtractStructuredArchive_SkipsDirectoryTraversal(t *testing.T) { archive := createArchive(t, []archiveFile{ - {Name: "bin/kimchi", Content: []byte("binary"), Mode: 0755}, + {Name: "bin/kimchi-code", Content: []byte("binary"), Mode: 0755}, {Name: "share/kimchi/package.json", Content: []byte(`{}`)}, {Name: "../etc/passwd", Content: []byte("malicious")}, }) - root, err := extractStructuredArchive(bytes.NewReader(archive), "kimchi") + root, err := extractStructuredArchive(bytes.NewReader(archive), "kimchi-code") require.NoError(t, err) defer func() { _ = os.RemoveAll(root) }() - assert.FileExists(t, filepath.Join(root, "bin", "kimchi")) + assert.FileExists(t, filepath.Join(root, "bin", "kimchi-code")) assert.FileExists(t, filepath.Join(root, "share", "kimchi", "package.json")) assert.NoFileExists(t, filepath.Join(root, "..", "etc", "passwd")) } diff --git a/internal/update/github_client.go b/internal/update/github_client.go index 0143886..ab4b9fb 100644 --- a/internal/update/github_client.go +++ b/internal/update/github_client.go @@ -27,7 +27,7 @@ type Repo struct { var ( kimchiRepo = Repo{Owner: "castai", Name: "kimchi", Binary: "kimchi"} - kimchiDevRepo = Repo{Owner: "castai", Name: "kimchi-dev", Binary: "kimchi"} + kimchiDevRepo = Repo{Owner: "castai", Name: "kimchi-dev", Binary: "kimchi-code"} ) type ReleaseInfo struct { diff --git a/internal/update/harness.go b/internal/update/harness.go index 93e7b11..628360b 100644 --- a/internal/update/harness.go +++ b/internal/update/harness.go @@ -83,11 +83,15 @@ func HarnessPathInDir(dir string) string { return filepath.Join(dir, kimchiDevRepo.Binary) } -// ResolveHarnessPath returns the path where the harness binary lives. -// It is placed inside the harness data directory so it does not collide -// with the Go CLI binary (both are now named "kimchi" after the merge). +// ResolveHarnessPath derives the harness binary path from the kimchi executable's +// resolved directory. For example, if kimchi is at /usr/local/bin/kimchi, this +// returns /usr/local/bin/kimchi-code. func ResolveHarnessPath() (string, error) { - return HarnessPathInDir(filepath.Join(harnessDataDir(), "bin")), nil + execPath, err := ResolveExecutablePath() + if err != nil { + return "", fmt.Errorf("resolve harness path: %w", err) + } + return HarnessPathInDir(filepath.Dir(execPath)), nil } // HarnessInstalled reports whether the harness binary exists at the given path. diff --git a/internal/update/harness_test.go b/internal/update/harness_test.go index 5bea21c..f5fa28a 100644 --- a/internal/update/harness_test.go +++ b/internal/update/harness_test.go @@ -11,19 +11,19 @@ import ( func TestHarnessPathInDir(t *testing.T) { got := HarnessPathInDir("/usr/local/bin") - assert.Equal(t, "/usr/local/bin/kimchi", got) + assert.Equal(t, "/usr/local/bin/kimchi-code", got) } func Test_HarnessInstalled(t *testing.T) { t.Run("exists", func(t *testing.T) { dir := t.TempDir() - path := filepath.Join(dir, "kimchi") + path := filepath.Join(dir, "kimchi-code") require.NoError(t, os.WriteFile(path, []byte("binary"), 0755)) assert.True(t, HarnessInstalled(path)) }) t.Run("missing", func(t *testing.T) { - assert.False(t, HarnessInstalled(filepath.Join(t.TempDir(), "kimchi"))) + assert.False(t, HarnessInstalled(filepath.Join(t.TempDir(), "kimchi-code"))) }) } @@ -37,7 +37,7 @@ func TestResolveHarnessPackageJSON(t *testing.T) { require.NoError(t, os.WriteFile(xdgPkg, []byte(`{"version":"2.0.0"}`), 0644)) binDir := t.TempDir() - binaryPath := filepath.Join(binDir, "kimchi") + binaryPath := filepath.Join(binDir, "kimchi-code") legacyPkg := filepath.Join(binDir, "package.json") require.NoError(t, os.WriteFile(legacyPkg, []byte(`{"version":"1.0.0"}`), 0644)) @@ -50,7 +50,7 @@ func TestResolveHarnessPackageJSON(t *testing.T) { t.Setenv("XDG_DATA_HOME", t.TempDir()) binDir := t.TempDir() - binaryPath := filepath.Join(binDir, "kimchi") + binaryPath := filepath.Join(binDir, "kimchi-code") got, err := resolveHarnessPackageJSON(binaryPath) require.NoError(t, err) diff --git a/internal/update/workflow_test.go b/internal/update/workflow_test.go index c8b9217..d63e131 100644 --- a/internal/update/workflow_test.go +++ b/internal/update/workflow_test.go @@ -420,7 +420,7 @@ func TestWorkflowRun_HarnessUpdate_PlacesSupportingFilesInDataDir(t *testing.T) t.Setenv("XDG_CACHE_HOME", t.TempDir()) newBinary := []byte("#!/bin/sh\necho v2.0.0") - packageJSON := []byte(`{"name":"kimchi","version":"2.0.0"}`) + packageJSON := []byte(`{"name":"kimchi-code","version":"2.0.0"}`) themeContent := []byte(`{"background":"#000"}`) archive := createArchive(t, []archiveFile{ @@ -485,7 +485,7 @@ func TestWorkflowRun_HarnessFreshInstall_PlacesSupportingFilesInDataDir(t *testi t.Setenv("XDG_CACHE_HOME", t.TempDir()) newBinary := []byte("#!/bin/sh\necho v1.0.0") - packageJSON := []byte(`{"name":"kimchi","version":"1.0.0"}`) + packageJSON := []byte(`{"name":"kimchi-code","version":"1.0.0"}`) themeContent := []byte(`{"background":"#000"}`) archive := createArchive(t, []archiveFile{