diff --git a/CLAUDE.md b/CLAUDE.md index e65515a..c17001b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,8 +6,8 @@ This is NanoClaw v2, a ground-up rewrite with breaking changes throughout. It ca **Do this instead:** 1. Abort the merge: `git merge --abort` (or `git reset --hard ORIG_HEAD` if the merge already completed). -2. Tell the user, verbatim: *"This is the v2 rewrite — it can't be merged into your existing install. Run `bash migrate-v2.sh` to migrate instead."* -3. Wait for the user to confirm before doing anything else. Do not attempt the migration yourself. +2. Tell the user, verbatim: *"This is the v2 rewrite — it can't be merged into your existing install. Exit Claude Code (or open a separate terminal) and run `bash migrate-v2.sh` from the shell."* +3. Wait for the user to confirm before doing anything else. Do not run the migration script yourself — it requires an interactive terminal and cannot be run from within Claude Code. If you are a fresh install (you ran `git clone`, not `git pull`) and there are no conflicts, ignore this banner and continue below. diff --git a/migrate-v2.sh b/migrate-v2.sh index eb5a381..f06a548 100644 --- a/migrate-v2.sh +++ b/migrate-v2.sh @@ -5,6 +5,8 @@ # Run from the v2 directory: # bash migrate-v2.sh # +# If you're in Claude Code, exit first or open a separate terminal. +# # Finds v1 automatically (sibling directory, or $NANOCLAW_V1_PATH). # Installs prerequisites (Node, pnpm, deps) via the existing setup.sh # bootstrap, then runs the migration steps. @@ -17,6 +19,19 @@ set -uo pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$PROJECT_ROOT" +# This script has interactive prompts (channel selection, service switchover) +# and streams progress output — it must run in a real terminal, not inside +# a tool subprocess (e.g. Claude Code's Bash tool, which collapses output). +if ! [ -t 0 ] || ! [ -t 1 ]; then + echo "This script requires an interactive terminal." + echo "" + echo "If you're in Claude Code, exit first or open a separate terminal," + echo "then run:" + echo " bash migrate-v2.sh" + echo "" + exit 1 +fi + LOGS_DIR="$PROJECT_ROOT/logs" STEPS_DIR="$LOGS_DIR/migrate-steps" MIGRATE_LOG="$LOGS_DIR/migrate-v2.log" @@ -547,6 +562,26 @@ echo echo "$(bold 'Service switchover')" echo +# Disable the v1 service so it doesn't auto-start, but leave the unit file +# on disk so the user can rollback with: systemctl --user start nanoclaw +# Idempotent — safe to call multiple times. +disable_v1_service() { + if [ "$PLATFORM_SERVICE" = "systemd" ]; then + local v1_file="$HOME/.config/systemd/user/${V1_SERVICE}.service" + if [ -f "$v1_file" ] || [ -L "$v1_file" ]; then + systemctl --user stop "$V1_SERVICE" 2>/dev/null || true + systemctl --user disable "$V1_SERVICE" 2>/dev/null || true + step_ok "Disabled $V1_SERVICE (unit file kept for rollback)" + fi + elif [ "$PLATFORM_SERVICE" = "launchd" ]; then + local v1_plist="$HOME/Library/LaunchAgents/${V1_SERVICE}.plist" + if [ -f "$v1_plist" ] || [ -L "$v1_plist" ]; then + launchctl unload "$v1_plist" 2>/dev/null || true + step_ok "Unloaded $V1_SERVICE (plist kept for rollback)" + fi + fi +} + # Detect platform and service names V1_SERVICE="" V2_SERVICE="" @@ -635,16 +670,14 @@ if [ "$V1_RUNNING" = "true" ]; then SERVICE_SWITCHED=false else step_ok "Keeping v2 service" - # Disable v1 from auto-starting - if [ "$PLATFORM_SERVICE" = "systemd" ]; then - systemctl --user disable "$V1_SERVICE" 2>/dev/null || true - fi + disable_v1_service fi else step_skip "Service switchover skipped" fi else step_skip "v1 service not running — nothing to switch" + disable_v1_service fi echo @@ -676,6 +709,16 @@ echo " $(green '✓') Channels installed: ${SELECTED_CHANNELS[*]}" fi echo " $(green '✓') Container skills copied" echo " $(green '✓') Container image built" +if [ "$SERVICE_SWITCHED" = "true" ] && [ -n "$V2_SERVICE" ]; then +echo " $(green '✓') Service switched to v2 $(dim "($V2_SERVICE)")" +echo +echo " $(bold 'Rollback to v1:')" +if [ "$PLATFORM_SERVICE" = "systemd" ]; then +echo " $(dim '$') systemctl --user stop $V2_SERVICE && systemctl --user start $V1_SERVICE" +elif [ "$PLATFORM_SERVICE" = "launchd" ]; then +echo " $(dim '$') launchctl unload ~/Library/LaunchAgents/${V2_SERVICE}.plist && launchctl load ~/Library/LaunchAgents/${V1_SERVICE}.plist" +fi +fi echo echo " $(bold 'What still needs a human:')" if [ "$ONECLI_OK" = "false" ]; then diff --git a/setup/migrate-v2/select-channels.ts b/setup/migrate-v2/select-channels.ts index eecf1ab..a2c8b21 100644 --- a/setup/migrate-v2/select-channels.ts +++ b/setup/migrate-v2/select-channels.ts @@ -12,6 +12,7 @@ import fs from 'fs'; import * as p from '@clack/prompts'; +import { styleText } from 'node:util'; const CHANNELS = [ { value: 'telegram', label: 'Telegram' }, @@ -47,7 +48,7 @@ async function main(): Promise { } const selected = await p.multiselect({ - message: 'Which channels do you want to set up?', + message: 'Which channels do you want to set up?\n' + styleText('dim', ' space to select, enter to confirm') + '\n', options: CHANNELS, required: false, }); diff --git a/setup/onecli.ts b/setup/onecli.ts index fbf76a9..8f758bb 100644 --- a/setup/onecli.ts +++ b/setup/onecli.ts @@ -115,9 +115,43 @@ function installOnecliCliOnly(): { stdout: string; ok: boolean } { return { stdout: upstream.stdout + (upstream.stderr ?? '') + '\n' + fallback.stdout, ok: fallback.ok }; } +// Remove containers in the "onecli" compose project whose service name isn't +// in the v2 set. Pre-v2 OneCLI used service "app" (container onecli-app-1); +// v2 uses "onecli". Compose flags the old container as an orphan but won't +// stop it without --remove-orphans, leaving port 10254 bound and crashing +// the new bring-up. Filed upstream; this is the downstream workaround. +function removeLegacyOnecliContainers(): string { + const out: string[] = []; + let list = ''; + try { + list = execSync( + `docker ps -a --filter "label=com.docker.compose.project=onecli" --format '{{.Names}}|{{.Label "com.docker.compose.service"}}'`, + { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }, + ).trim(); + } catch { + return ''; + } + if (!list) return ''; + const v2Services = new Set(['onecli', 'postgres']); + for (const line of list.split('\n')) { + const [name, service] = line.split('|'); + if (!name || !service || v2Services.has(service)) continue; + out.push(`Removing legacy OneCLI container: ${name} (service=${service})`); + try { + execSync(`docker rm -f ${JSON.stringify(name)}`, { stdio: ['ignore', 'pipe', 'pipe'] }); + } catch (err) { + out.push(` rm failed (continuing): ${(err as Error).message}`); + } + } + return out.join('\n'); +} + function installOnecli(): { stdout: string; ok: boolean } { let stdout = ''; + const cleanup = removeLegacyOnecliContainers(); + if (cleanup) stdout += cleanup + '\n'; + // Gateway install (docker-compose based, no rate-limit concerns). const gw = runInstall('curl -fsSL onecli.sh/install | sh'); stdout += gw.stdout;