mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-06 18:52:03 +08:00
d8748e3a45
- .env backup and removal are now one atomic action: a failed backup throws into executePlan's catch and the deletion never runs (the bash original's set -e gave the same guarantee; the port had lost it) - containers are re-listed by install label at removal time instead of removed from scan-time ids — the live host can spawn containers during the confirm phase - uninstall telemetry no longer creates data/install-id (persistId:false on emit), so --dry-run truly changes nothing and the already-clean exit can fire - runtime-tail failure notes are printed before the Done line instead of being discarded - uninstall.sh translates the old short flags (-n/-y) instead of silently dropping them (-n used to fall through to a real interactive uninstall) - nanoclaw.sh gates the TS uninstaller on node (tsx's interpreter), not pnpm, which the direct-exec path never uses - detectExistingInstall also checks the system-level systemd unit - a delete-onecli-agent spawn failure now notes the manual command instead of claiming the agent was already gone - setupLog.userInput is skipped when logs/ is absent so the uninstall doesn't recreate it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
/**
|
|
* Pure removal planner: inventory + per-group decisions → ordered actions.
|
|
*
|
|
* The order is load-bearing:
|
|
* 1. Service / processes / containers / image / symlink — stop the host
|
|
* first so it can't respawn containers mid-removal.
|
|
* 2. OneCLI agent deletions — before the data group, which removes the
|
|
* data/v2.db the mine/orphan split was computed from.
|
|
* 3. Data group, with the .env backup strictly before its deletion.
|
|
* 4. User group (groups/, store/).
|
|
* 5. Runtime tail: dist/ then node_modules/ — ALWAYS last. The uninstaller
|
|
* runs on tsx out of node_modules; nothing may load after this.
|
|
*/
|
|
import path from 'path';
|
|
|
|
import type { VaultAgent } from './onecli-agents.js';
|
|
import type { Inventory, PathItem } from './scan.js';
|
|
|
|
export interface Decisions {
|
|
service: boolean;
|
|
data: boolean;
|
|
user: boolean;
|
|
onecliDelete: VaultAgent[];
|
|
}
|
|
|
|
export type RemovalAction =
|
|
| {
|
|
kind: 'unload-service';
|
|
flavor: 'launchd' | 'systemd-user' | 'systemd-system';
|
|
unitPath: string;
|
|
/** systemd unit name without .service (unused for launchd). */
|
|
unitName: string;
|
|
}
|
|
| { kind: 'kill-pid'; pidFile: string }
|
|
| { kind: 'pkill-host'; pattern: string }
|
|
/**
|
|
* Containers are re-listed by label at removal time, not removed from
|
|
* scan-time ids — the host stays alive through the whole confirm phase
|
|
* and can spawn new containers after the scan.
|
|
*/
|
|
| { kind: 'rm-containers'; runtime: string; labelFilter: string }
|
|
| { kind: 'rmi'; runtime: string; image: string }
|
|
| { kind: 'rm-ncl-symlink'; linkPath: string }
|
|
| { kind: 'delete-onecli-agent'; agent: VaultAgent }
|
|
/**
|
|
* Backs up AND removes .env as one atomic action: a failed backup must
|
|
* never be followed by the deletion (the backup is the user's only copy
|
|
* of their API keys). .env is deliberately excluded from `delete-path`.
|
|
*/
|
|
| { kind: 'backup-env'; envPath: string }
|
|
| { kind: 'delete-path'; item: PathItem }
|
|
| { kind: 'delete-runtime-path'; item: PathItem };
|
|
|
|
export function buildRemovalPlan(inv: Inventory, d: Decisions): RemovalAction[] {
|
|
const actions: RemovalAction[] = [];
|
|
|
|
if (d.service) {
|
|
const s = inv.service;
|
|
if (s.launchdPlist) {
|
|
actions.push({
|
|
kind: 'unload-service',
|
|
flavor: 'launchd',
|
|
unitPath: s.launchdPlist,
|
|
unitName: path.basename(s.launchdPlist, '.plist'),
|
|
});
|
|
}
|
|
if (s.systemdUserUnit) {
|
|
actions.push({
|
|
kind: 'unload-service',
|
|
flavor: 'systemd-user',
|
|
unitPath: s.systemdUserUnit,
|
|
unitName: path.basename(s.systemdUserUnit, '.service'),
|
|
});
|
|
}
|
|
if (s.systemdSystemUnit) {
|
|
actions.push({
|
|
kind: 'unload-service',
|
|
flavor: 'systemd-system',
|
|
unitPath: s.systemdSystemUnit,
|
|
unitName: path.basename(s.systemdSystemUnit, '.service'),
|
|
});
|
|
}
|
|
if (s.pidFile) actions.push({ kind: 'kill-pid', pidFile: s.pidFile });
|
|
actions.push({
|
|
kind: 'pkill-host',
|
|
pattern: `${inv.projectRoot}/dist/index.js`,
|
|
});
|
|
// Unconditional (like pkill): the scan may have found zero containers
|
|
// while the still-running host spawned one since.
|
|
actions.push({
|
|
kind: 'rm-containers',
|
|
runtime: inv.containerRuntime,
|
|
labelFilter: `nanoclaw-install=${inv.slug}`,
|
|
});
|
|
if (s.image) {
|
|
actions.push({ kind: 'rmi', runtime: inv.containerRuntime, image: s.image });
|
|
}
|
|
if (s.nclSymlink) {
|
|
actions.push({ kind: 'rm-ncl-symlink', linkPath: s.nclSymlink });
|
|
}
|
|
}
|
|
|
|
for (const agent of d.onecliDelete) {
|
|
actions.push({ kind: 'delete-onecli-agent', agent });
|
|
}
|
|
|
|
if (d.data) {
|
|
const env = inv.data.find((i) => path.basename(i.path) === '.env');
|
|
if (env) actions.push({ kind: 'backup-env', envPath: env.path });
|
|
for (const item of inv.data) {
|
|
if (item === env) continue; // removed by backup-env, never a bare delete
|
|
actions.push({ kind: 'delete-path', item });
|
|
}
|
|
}
|
|
|
|
if (d.user) {
|
|
for (const item of inv.user) actions.push({ kind: 'delete-path', item });
|
|
}
|
|
|
|
if (d.data) {
|
|
const tail = [...inv.runtime].sort(
|
|
(a, b) =>
|
|
Number(path.basename(a.path) === 'node_modules') -
|
|
Number(path.basename(b.path) === 'node_modules'),
|
|
);
|
|
for (const item of tail) actions.push({ kind: 'delete-runtime-path', item });
|
|
}
|
|
|
|
return actions;
|
|
}
|