Merge pull request #2235 from Koshkoshinsk/migration-fixes-combined

fix: migration UX improvements + legacy OneCLI container cleanup
This commit is contained in:
gavrielc
2026-05-04 10:05:35 +03:00
committed by GitHub
4 changed files with 85 additions and 7 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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<void> {
}
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,
});

View File

@@ -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;