Files
nanoclaw/setup/uninstall/onecli-agents.ts
T
Amit Shafnir 41a720dd59 feat: port uninstaller to TS, wire nanoclaw.sh --uninstall, detect existing installs in setup
Replaces the standalone bash uninstall.sh with a TypeScript flow inside the
setup driver (setup/uninstall/): scan (slug-scoped inventory), plan (pure
ordered removal actions), remove (per-action executor that absorbs failures
into notes), and flow (clack UI). uninstall.sh is now a 3-line pointer that
execs nanoclaw.sh --uninstall.

- nanoclaw.sh --uninstall short-circuits before diagnostics/bootstrap; with
  no node_modules it prints manual cleanup commands and exits 1
- setup:auto routes --uninstall before initProgressionLog so an uninstall
  never resets logs/setup.log
- fresh setup runs detect an existing install (service registration or
  data/v2.db) and offer keep-and-continue (default) or uninstall-and-exit;
  suppressed on fail()-retry and sg re-exec resumes
- self-deletion safety: static imports only, dist/ + node_modules/ removed
  dead last, nothing but console.log after the runtime tail
- --yes never deletes orphan ag-* vault agents; their manual delete
  commands (by vault uuid) are printed instead

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:50:12 +03:00

142 lines
4.4 KiB
TypeScript

/**
* OneCLI vault-agent inventory for the uninstaller.
*
* Vault agents split into two sets: MINE (identifier matches an agent-group
* id in this copy's data/v2.db) and ORPHANS (NanoClaw-style `ag-*`
* identifiers not in our DB — possibly another copy's). Deletion is always
* by the vault's internal uuid: the agent-group id is NOT a valid
* `onecli agents delete --id` value (see src/container-runner.ts).
*/
import fs from 'fs';
import Database from 'better-sqlite3';
export interface VaultAgent {
/** Internal vault uuid — the only valid `onecli agents delete --id` value. */
uuid: string;
/** What the agent was registered under, e.g. a NanoClaw agent-group id (`ag-*`). */
identifier: string;
name: string;
}
export type RunCommand = (
cmd: string,
args: string[],
) => { status: number | null; stdout: string };
/**
* List non-default vault agents via `onecli agents list`. `available: false`
* means the vault couldn't be read at all (binary missing, command failed,
* or unparseable output) — distinct from an empty vault.
*/
export function listVaultAgents(run: RunCommand): {
available: boolean;
agents: VaultAgent[];
} {
let result: { status: number | null; stdout: string };
try {
result = run('onecli', ['agents', 'list']);
} catch {
return { available: false, agents: [] };
}
if (result.status !== 0) return { available: false, agents: [] };
let parsed: unknown;
try {
parsed = JSON.parse(result.stdout);
} catch {
return { available: false, agents: [] };
}
const data =
parsed !== null && typeof parsed === 'object' && 'data' in parsed
? (parsed as { data: unknown }).data
: null;
if (!Array.isArray(data)) return { available: false, agents: [] };
const agents: VaultAgent[] = [];
for (const entry of data) {
if (entry === null || typeof entry !== 'object') continue;
const a = entry as Record<string, unknown>;
if (a.isDefault === true) continue;
const identifier = typeof a.identifier === 'string' ? a.identifier : '';
const uuid = typeof a.id === 'string' ? a.id : '';
if (!identifier || identifier === 'default' || !uuid) continue;
agents.push({
uuid,
identifier,
name: typeof a.name === 'string' ? a.name : '',
});
}
return { available: true, agents };
}
/**
* Read this copy's agent-group ids from data/v2.db (readonly).
*
* `known: false` distinguishes "we couldn't read the DB at all" from "this
* copy has zero agent groups" — without it every ag-* vault agent would be
* mislabeled an orphan and --yes would silently leave this copy's agents
* behind.
*/
export function readAgentGroupIds(dbPath: string): {
ids: Set<string>;
known: boolean;
} {
if (!fs.existsSync(dbPath)) return { ids: new Set(), known: false };
let db: Database.Database | null = null;
try {
db = new Database(dbPath, { readonly: true });
const rows = db.prepare('SELECT id FROM agent_groups').all() as {
id: string;
}[];
return { ids: new Set(rows.map((r) => r.id)), known: true };
} catch {
return { ids: new Set(), known: false };
} finally {
db?.close();
}
}
/**
* Split vault agents into MINE (identifier ∈ ids) and ORPHANS (ag-* not in
* ids). Non-NanoClaw identifiers are ignored entirely. With `known: false`
* nothing can be MINE, so every ag-* agent lands in ORPHANS — the caller is
* responsible for warning that the labels are unreliable.
*/
export function splitVaultAgents(
agents: VaultAgent[],
ids: Set<string>,
known: boolean,
): { mine: VaultAgent[]; orphans: VaultAgent[] } {
const mine: VaultAgent[] = [];
const orphans: VaultAgent[] = [];
for (const agent of agents) {
if (known && ids.has(agent.identifier)) {
mine.push(agent);
} else if (agent.identifier.startsWith('ag-')) {
orphans.push(agent);
}
}
return { mine, orphans };
}
/**
* Resolve the vault-agent delete set from the user's answers. Under --yes
* (`assumeYes`) MINE is always deleted but ORPHANS never are — deleting
* what may be another copy's agents requires explicit human intent.
*/
export function resolveOnecliDeletions(input: {
mine: VaultAgent[];
orphans: VaultAgent[];
assumeYes: boolean;
deleteMine: boolean;
deleteOrphans: boolean;
}): VaultAgent[] {
const out: VaultAgent[] = [];
if (input.assumeYes || input.deleteMine) out.push(...input.mine);
if (!input.assumeYes && input.deleteOrphans) out.push(...input.orphans);
return out;
}