diff --git a/cmd/harness.go b/cmd/harness.go index ec6f803..e255202 100644 --- a/cmd/harness.go +++ b/cmd/harness.go @@ -34,12 +34,18 @@ func runHarness(cmd *cobra.Command, args []string) error { return launchHarness(cmd, harnessPath, args) } - // Reuse the update command logic for CLI + harness updates. // Uses cache (skipCache=false) since harness launches happen frequently. - if _, err := runUpdate(cmd, false, false, false); err != nil { + result, err := runUpdate(cmd, false, false, false) + if err != nil { return err } + // If the harness was updated, exit and ask the user to re-run kimchi. + if result != nil && result.Updated { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n✓ Harness was updated. Please re-run the 'kimchi' command to use the new version.") + return nil + } + // If the harness is not installed (e.g. user declined, or fetch failed), exit // gracefully instead of treating it as a fatal error. if !update.HarnessInstalled(harnessPath) { diff --git a/cmd/update.go b/cmd/update.go index 2cc6abe..8ca4f34 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -14,7 +14,6 @@ import ( "k8s.io/klog/v2" "github.com/castai/kimchi/internal/update" - "github.com/castai/kimchi/internal/version" ) type updateDoneMsg struct{ err error } @@ -52,10 +51,6 @@ func (m updateModel) View() string { return m.spinner.View() + " Updating to " + m.version + "...\n" } -type updateResult struct { - cliUpdated bool -} - func NewUpdateCommand() *cobra.Command { var ( force bool @@ -78,58 +73,17 @@ func NewUpdateCommand() *cobra.Command { return cmd } -func runUpdate(cmd *cobra.Command, force, dryRun, skipCache bool) (*updateResult, error) { +func runUpdate(cmd *cobra.Command, force, dryRun, skipCache bool) (*update.WorkflowResult, error) { ctx := cmd.Context() - res := &updateResult{} - - cliResult, err := runCLIUpdate(cmd, ctx, force, dryRun, skipCache) - if err != nil { - return nil, err - } - res.cliUpdated = cliResult.Updated + // CLI update is deprecated - only run harness update klog.V(1).Info("checking for coding harness updates") - runHarnessUpdate(cmd, ctx, force, dryRun, skipCache) - return res, nil -} - -func runCLIUpdate(cmd *cobra.Command, ctx context.Context, force, dryRun, skipCache bool) (*update.WorkflowResult, error) { - var opts []update.WorkflowOpt - if dryRun { - opts = append(opts, update.WithDryRun()) - } - if skipCache { - opts = append(opts, update.WithSkipUpdateCache()) - } - - opts = append(opts, - update.WithCurrentVersionFn(func(ctx context.Context) (*semver.Version, error) { - if version.Version == "dev" { - return semver.MustParse("0.0.0-dev"), nil - } - return semver.NewVersion(version.Version) - }), - update.WithConfirmFn(func(current, latest *semver.Version) (bool, error) { - prompt := fmt.Sprintf("Kimchi update available: %s → %s\nUpdate? [Y/n]: ", current, latest) - return confirmAction(cmd, prompt, "Update skipped.", force), nil - }), - update.WithProgressFn(runUpdateWithSpinner), - ) - wf := update.NewCLIWorkflow(opts...) - - result, err := wf.Run(ctx) - if err != nil { - return nil, fmt.Errorf("check for kimchi updates: %w", err) - } - - printUpdateResult(cmd, "CLI", result, dryRun) - - return result, nil + return runHarnessUpdate(cmd, ctx, force, dryRun, skipCache) } // runHarnessUpdate checks for the coding harness and installs or updates it. // Failures are reported as warnings and never block the CLI update. -func runHarnessUpdate(cmd *cobra.Command, ctx context.Context, force, dryRun, skipCache bool) { +func runHarnessUpdate(cmd *cobra.Command, ctx context.Context, force, dryRun, skipCache bool) (*update.WorkflowResult, error) { var opts []update.WorkflowOpt if dryRun { opts = append(opts, update.WithDryRun()) @@ -153,10 +107,11 @@ func runHarnessUpdate(cmd *cobra.Command, ctx context.Context, force, dryRun, sk result, err := wf.Run(ctx) if err != nil { _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %v\n", err) - return + return nil, err } printUpdateResult(cmd, "Coding harness", result, dryRun) + return result, nil } func printUpdateResult(cmd *cobra.Command, label string, result *update.WorkflowResult, dryRun bool) { diff --git a/internal/tui/steps/update.go b/internal/tui/steps/update.go index b568cb5..6c3c009 100644 --- a/internal/tui/steps/update.go +++ b/internal/tui/steps/update.go @@ -10,18 +10,14 @@ import ( "github.com/castai/kimchi/internal/update" ) -type cliApplyMsg struct{ err error } type harnessApplyMsg struct{ err error } type updateState int const ( - updateStateChoice updateState = iota // CLI update choice - updateStateApplying // CLI update in progress - updateStateCLIDone // CLI update finished, transition to harness - updateStateHarnessPrompt // prompt to install harness - updateStateHarnessApplying // harness install/update in progress - updateStateDone // all done + updateStateHarnessPrompt updateState = iota // prompt to install/update harness + updateStateHarnessApplying // harness install/update in progress + updateStateDone // all done ) func progressText(b update.UpdateStatus) string { @@ -54,53 +50,35 @@ type applyResult struct { } type UpdateStep struct { - cli update.UpdateStatus harness update.UpdateStatus // zero value = no harness to manage - cliResult applyResult harnessResult applyResult state updateState skipSelected bool spinner spinner.Model } -func NewUpdateStep(cli, harness update.UpdateStatus) *UpdateStep { +func NewUpdateStep(harness update.UpdateStatus) *UpdateStep { s := spinner.New() s.Spinner = spinner.Dot s.Style = Styles.Spinner - initialState := updateStateChoice - if !cli.HasUpdate && (harness.HasUpdate || harness.Installed()) { - // CLI is up to date, go directly to harness handling. - initialState = updateStateHarnessPrompt + initialState := updateStateHarnessPrompt + if !harness.HasUpdate && harness.Installed() { + // Harness is up to date and installed, nothing to do. + initialState = updateStateDone } return &UpdateStep{ - cli: cli, harness: harness, state: initialState, spinner: s, } } -// needsHarnessStep reports whether there is harness work to consider (update or installed). -func (s *UpdateStep) needsHarnessStep() bool { - return s.harness.HasUpdate || s.harness.Installed() -} - func (s *UpdateStep) Init() tea.Cmd { return nil } func (s *UpdateStep) Update(msg tea.Msg) (Step, tea.Cmd) { switch msg := msg.(type) { - case cliApplyMsg: - s.cliResult.err = msg.err - s.cliResult.applied = msg.err == nil - if s.needsHarnessStep() { - s.state = updateStateCLIDone - return s, nil - } - s.state = updateStateDone - return s, nil - case harnessApplyMsg: s.harnessResult.err = msg.err s.harnessResult.applied = msg.err == nil @@ -108,7 +86,7 @@ func (s *UpdateStep) Update(msg tea.Msg) (Step, tea.Cmd) { return s, nil case spinner.TickMsg: - if s.state == updateStateApplying || s.state == updateStateHarnessApplying { + if s.state == updateStateHarnessApplying { var cmd tea.Cmd s.spinner, cmd = s.spinner.Update(msg) return s, cmd @@ -116,10 +94,6 @@ func (s *UpdateStep) Update(msg tea.Msg) (Step, tea.Cmd) { case tea.KeyMsg: switch s.state { - case updateStateChoice: - return s.handleChoiceKey(msg) - case updateStateCLIDone: - return s.handleCLIDoneKey(msg) case updateStateHarnessPrompt: return s.handleHarnessPromptKey(msg) case updateStateDone: @@ -129,41 +103,6 @@ func (s *UpdateStep) Update(msg tea.Msg) (Step, tea.Cmd) { return s, nil } -func (s *UpdateStep) handleChoiceKey(msg tea.KeyMsg) (Step, tea.Cmd) { - if cmd, ok := s.handleQuit(msg); ok { - return s, cmd - } - switch msg.String() { - case "up", "k", "down", "j": - s.skipSelected = !s.skipSelected - case "enter": - if s.skipSelected { - // Skip CLI update. - if s.needsHarnessStep() { - s.state = updateStateHarnessPrompt - s.skipSelected = false - return s, nil - } - return s, func() tea.Msg { return NextStepMsg{} } - } - s.state = updateStateApplying - return s, tea.Batch(s.spinner.Tick, s.applyCLIUpdate()) - } - return s, nil -} - -func (s *UpdateStep) handleCLIDoneKey(msg tea.KeyMsg) (Step, tea.Cmd) { - if cmd, ok := s.handleQuit(msg); ok { - return s, cmd - } - switch msg.String() { - case "enter", " ": - s.transitionToHarness() - return s, nil - } - return s, nil -} - func (s *UpdateStep) handleHarnessPromptKey(msg tea.KeyMsg) (Step, tea.Cmd) { if cmd, ok := s.handleQuit(msg); ok { return s, cmd @@ -173,12 +112,7 @@ func (s *UpdateStep) handleHarnessPromptKey(msg tea.KeyMsg) (Step, tea.Cmd) { s.skipSelected = !s.skipSelected case "enter": if s.skipSelected { - // Skip harness — if nothing was applied or errored, go straight to next step. - if !s.cliResult.applied && s.cliResult.err == nil { - return s, func() tea.Msg { return NextStepMsg{} } - } - s.state = updateStateDone - return s, nil + return s, func() tea.Msg { return NextStepMsg{} } } s.state = updateStateHarnessApplying return s, tea.Batch(s.spinner.Tick, s.applyHarnessUpdate()) @@ -192,10 +126,6 @@ func (s *UpdateStep) handleDoneKey(msg tea.KeyMsg) (Step, tea.Cmd) { } switch msg.String() { case "enter", " ": - if s.cliResult.applied { - // CLI was updated — exit so the user restarts with the new version. - return s, func() tea.Msg { return AbortMsg{} } - } return s, func() tea.Msg { return NextStepMsg{} } } return s, nil @@ -209,25 +139,6 @@ func (s *UpdateStep) handleQuit(msg tea.KeyMsg) (tea.Cmd, bool) { return nil, false } -func (s *UpdateStep) transitionToHarness() tea.Cmd { - if !s.needsHarnessStep() || (s.harness.Installed() && !s.harness.HasUpdate) { - // Nothing to do for harness — skip directly to done. - s.state = updateStateDone - return nil - } - s.state = updateStateHarnessPrompt - s.skipSelected = false - return nil -} - -func (s *UpdateStep) applyCLIUpdate() tea.Cmd { - return func() tea.Msg { - wf := update.NewCLIWorkflow() - _, err := wf.Run(context.Background()) - return cliApplyMsg{err: err} - } -} - func (s *UpdateStep) applyHarnessUpdate() tea.Cmd { return func() tea.Msg { wf := update.NewHarnessWorkflow() @@ -238,55 +149,18 @@ func (s *UpdateStep) applyHarnessUpdate() tea.Cmd { func (s *UpdateStep) View() string { switch s.state { - case updateStateApplying: - return s.spinner.View() + " " + progressText(s.cli) - - case updateStateCLIDone: - view := "" - if s.cliResult.err != nil { - view += Styles.Error.Render("CLI update failed: "+s.cliResult.err.Error()) + "\n\n" - } else { - view += Styles.Success.Render(resultText(s.cli)) + "\n\n" - } - view += Styles.Desc.Render("Press Enter to continue to coding harness...") - return view - case updateStateHarnessPrompt: return s.viewHarnessPrompt() - case updateStateHarnessApplying: - return s.viewCLISummary() + - s.spinner.View() + " " + progressText(s.harness) - + return s.spinner.View() + " " + progressText(s.harness) case updateStateDone: return s.viewDone() - default: - return s.viewCLIChoice() - } -} - -func (s *UpdateStep) viewCLIChoice() string { - updateLabel := " Update now " - skipLabel := " Skip " - updateStyle := Styles.Item - skipStyle := Styles.Item - if !s.skipSelected { - updateLabel = "► Update now " - updateStyle = Styles.Selected - } else { - skipLabel = "► Skip " - skipStyle = Styles.Selected + return "" } - - return Styles.Warning.Render(fmt.Sprintf("Update available: v%s → v%s", s.cli.CurrentVersion, s.cli.LatestVersion)) + "\n\n" + - updateStyle.Render(updateLabel) + "\n" + - skipStyle.Render(skipLabel) } func (s *UpdateStep) viewHarnessPrompt() string { - view := s.viewCLISummary() - label := "Update" if !s.harness.Installed() { label = "Install" @@ -304,43 +178,22 @@ func (s *UpdateStep) viewHarnessPrompt() string { skipStyle = Styles.Selected } + var view string if s.harness.Installed() { - view += Styles.Warning.Render(fmt.Sprintf("Coding harness update: v%s → v%s", s.harness.CurrentVersion, s.harness.LatestVersion)) + "\n\n" + view = Styles.Warning.Render(fmt.Sprintf("Coding harness update: v%s → v%s", s.harness.CurrentVersion, s.harness.LatestVersion)) + "\n\n" } else { - view += Styles.Warning.Render("Coding harness is not installed") + "\n\n" + view = Styles.Warning.Render("Coding harness is not installed") + "\n\n" } view += installStyle.Render(installLabel) + "\n" + skipStyle.Render(skipLabel) return view } -func (s *UpdateStep) viewCLISummary() string { - if !s.cli.HasUpdate { - return "" - } - if s.cliResult.applied { - return Styles.Success.Render(resultText(s.cli)) + "\n\n" - } - if s.cliResult.err != nil { - return Styles.Error.Render("CLI update failed: "+s.cliResult.err.Error()) + "\n\n" - } - return Styles.Desc.Render("CLI update skipped") + "\n\n" -} - func (s *UpdateStep) viewDone() string { view := "" - // CLI status. - if s.cli.HasUpdate { - if s.cliResult.applied { - view += Styles.Success.Render(resultText(s.cli)) + "\n" - } else if s.cliResult.err != nil { - view += Styles.Error.Render("CLI update failed: "+s.cliResult.err.Error()) + "\n" - } - } - // Harness status. - if s.needsHarnessStep() { + if s.harness.HasUpdate || s.harness.Installed() { if s.harnessResult.err != nil { view += Styles.Warning.Render(errorText(s.harness, s.harnessResult)) + "\n" } else if s.harnessResult.applied { @@ -349,9 +202,7 @@ func (s *UpdateStep) viewDone() string { } view += "\n" - if s.cliResult.applied { - view += Styles.Desc.Render("Press Enter to exit. Please re-run kimchi to use the new version.") - } else if s.cliResult.err != nil || s.harnessResult.err != nil { + if s.harnessResult.err != nil { view += Styles.Desc.Render("Press Enter to continue or run `kimchi update` later.") } else { view += Styles.Desc.Render("Press Enter to continue.") @@ -365,9 +216,9 @@ func (s *UpdateStep) Name() string { return "Update" } func (s *UpdateStep) Info() StepInfo { var bindings []KeyBinding switch s.state { - case updateStateApplying, updateStateHarnessApplying: + case updateStateHarnessApplying: bindings = []KeyBinding{BindingsQuit} - case updateStateDone, updateStateCLIDone: + case updateStateDone: bindings = []KeyBinding{BindingsConfirm} default: bindings = []KeyBinding{BindingsNavigate, BindingsConfirm, BindingsQuit} diff --git a/internal/tui/steps/welcome.go b/internal/tui/steps/welcome.go index 7fe68f4..2e37bb1 100644 --- a/internal/tui/steps/welcome.go +++ b/internal/tui/steps/welcome.go @@ -36,16 +36,7 @@ func (s *WelcomeStep) Init() tea.Cmd { msg := updateCheckMsg{} var wg sync.WaitGroup - wg.Add(2) - - go func() { - defer wg.Done() - cli, err := update.CheckCLIUpdate(ctx) - if err != nil { - return - } - msg.cli = *cli - }() + wg.Add(1) go func() { defer wg.Done() diff --git a/internal/tui/wizard.go b/internal/tui/wizard.go index 4fe0266..553f1b3 100644 --- a/internal/tui/wizard.go +++ b/internal/tui/wizard.go @@ -198,10 +198,9 @@ func (w *wizard) collectStepResult() { step := w.stepList[w.current] switch s := step.(type) { case *steps.WelcomeStep: - cli := s.CLI() harness := s.Harness() - if cli.HasUpdate || harness.HasUpdate || harness.Installed() { - w.pendingUpdate = steps.NewUpdateStep(cli, harness) + if harness.HasUpdate || harness.Installed() { + w.pendingUpdate = steps.NewUpdateStep(harness) } case *steps.AuthStep: w.config.APIKey = s.APIKey() diff --git a/internal/update/github_client.go b/internal/update/github_client.go index ab4b9fb..0143886 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-code"} + kimchiDevRepo = Repo{Owner: "castai", Name: "kimchi-dev", Binary: "kimchi"} ) type ReleaseInfo struct { diff --git a/internal/update/harness_test.go b/internal/update/harness_test.go index f5fa28a..4e1b1f2 100644 --- a/internal/update/harness_test.go +++ b/internal/update/harness_test.go @@ -11,7 +11,7 @@ import ( func TestHarnessPathInDir(t *testing.T) { got := HarnessPathInDir("/usr/local/bin") - assert.Equal(t, "/usr/local/bin/kimchi-code", got) + assert.Equal(t, "/usr/local/bin/kimchi", got) } func Test_HarnessInstalled(t *testing.T) { diff --git a/scripts/install.sh b/scripts/install.sh old mode 100644 new mode 100755 index 13f4a56..fdb5b24 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2,83 +2,13 @@ set -e -GREEN='\033[0;32m' -RED='\033[0;31m' +YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' -echo -e "${BLUE}Installing Kimchi...${NC}" - -OS=$(uname -s | tr '[:upper:]' '[:lower:]') -case "$OS" in -darwin*) OS="darwin" ;; -linux*) OS="linux" ;; -*) - echo -e "${RED}Unsupported OS: $OS${NC}" >&2 - exit 1 - ;; -esac - -ARCH=$(uname -m) -case "$ARCH" in -x86_64) ARCH="amd64" ;; -aarch64 | arm64) ARCH="arm64" ;; -*) - echo -e "${RED}Unsupported architecture: $ARCH${NC}" >&2 - exit 1 - ;; -esac - -BINARY_URL="https://github.com/castai/kimchi/releases/latest/download/kimchi_${OS}_${ARCH}.tar.gz" - -TEMP_DIR=$(mktemp -d) -trap "rm -rf $TEMP_DIR" EXIT - -echo -e "${BLUE}Downloading kimchi for ${OS}/${ARCH}...${NC}" -if ! curl -fsSL "$BINARY_URL" | tar -xzf - -C "$TEMP_DIR"; then - echo -e "${RED}Failed to download kimchi${NC}" >&2 - echo "Please check that the release exists at:" - echo " https://github.com/castai/kimchi/releases" - exit 1 -fi - -chmod +x "$TEMP_DIR/kimchi" - -if [ -w /usr/local/bin ]; then - mv "$TEMP_DIR/kimchi" /usr/local/bin/kimchi - INSTALL_PATH="/usr/local/bin/kimchi" -else - mkdir -p ~/.local/bin - mv "$TEMP_DIR/kimchi" ~/.local/bin/kimchi - INSTALL_PATH="$HOME/.local/bin/kimchi" - echo "" - echo -e "${BLUE}Note: ~/.local/bin is not in your PATH.${NC}" - case "${SHELL}" in - */fish*) - echo " Run: fish_add_path ~/.local/bin" - ;; - */zsh*) - echo " Run: echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.zshrc && source ~/.zshrc" - ;; - */bash*) - echo " Run: echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc && source ~/.bashrc" - ;; - *) - echo " Add ~/.local/bin to your PATH in your shell's config file." - ;; - esac -fi - -echo "" -echo -e "${GREEN}✓ Installed kimchi to ${INSTALL_PATH}${NC}" +echo -e "${YELLOW}⚠️ The Kimchi CLI is deprecated.${NC}" +echo -e "${BLUE}Redirecting to install the new coding harness from kimchi-dev...${NC}" echo "" -echo -e "${BLUE}Launching Kimchi...${NC}" -echo "" -if [ -t 0 ]; then - exec "$INSTALL_PATH" -elif [ -c /dev/tty ]; then - exec "$INSTALL_PATH" < /dev/tty -else - echo "" - echo -e "${BLUE}Run 'kimchi' to get started.${NC}" -fi + +# Redirect to the kimchi-dev install script +exec bash <(curl -fsSL https://github.com/castai/kimchi-dev/releases/latest/download/install.sh)