diff --git a/completions/trsd.bash b/completions/trsd.bash index 97b33d5..e156baa 100644 --- a/completions/trsd.bash +++ b/completions/trsd.bash @@ -1,6 +1,6 @@ # Generated from treeseed.command-tree/v1. _trsd_complete() { - local paths="inbox\nsend\ntopics list\ntopics show\ntopics subscribe\ntopics unsubscribe\ncapabilities list\ncapabilities show\nauth login\nauth logout\nauth status\nusers create\nteams list\nteams current\nteams use\nsecrets list\nsecrets status\nsecrets unlock\nsecrets lock\nsecrets rotate\nplatform verify\nplatform workset\nplatform project create\ndev host activate\ndev host status\ndev host deactivate\ndev host guest image import\ndev session start\ndev session stop\ndev use\ndev rebuild\ndev restart\ndev status\ndev logs\ndev plan\ndev freeze\ndev verify\nhost status\nhost doctor\nhost plan\nhost apply\nhost reconcile\nhost events\nhost config show\nhost config plan\nhost config apply\nhost config adopt\nhost topology\nhost connections\nhost provider status\nhost provider credentials list\nhost provider credentials status\nhost provider credentials initialize\nhost storage status\nhost storage connect\nhost storage reconcile\nhost storage rotate\nhost storage reset\nhost security plan\nhost security initialize\nhost security status\nhost security verify\nhost security rotate\nhost security recovery verify\nhost sandbox status\nhost sandbox doctor\nhost fleet status\nhost update status\nhost update check\nhost update apply\nhost update channel\nhost update pause\nhost update resume\nhost component list\nhost component status\nhost component enable\nhost component disable\nhost aliases list\nhost recovery status\nhost recovery retry\nhost recovery restore\nhost bootstrap status\nhost bootstrap enroll\nhost reset\nagents list\nagents show\nagents validate\nagents diff\nagents diagnose\nagents classes list\nagents classes show\nagents bindings list\nagents bindings show\nagents bindings explain\nproviders list\nproviders show\nproviders status\nproviders diagnose\nproviders connect\nproviders disconnect\nproviders requests list\nproviders requests show\nproviders requests approve\nproviders requests reject\nproviders credentials status\nproviders credentials rotate\nproviders credentials revoke\nproviders offers show\nproviders offers validate\nproviders offers plan\nproviders offers apply\nseeds validate\nseeds plan\nseeds apply\nseeds show\nseeds verify\ncapacity status\ncapacity explain\ncapacity usage\ncapacity ledger\ncapacity audit\nplans list\nplans show\nplans explain\nplans diff\nworkdays profiles list\nworkdays profiles show\nworkdays profiles validate\nworkdays plan\nworkdays start\nworkdays list\nworkdays show\nworkdays watch\nworkdays pause\nworkdays resume\nworkdays stop\nworkdays cancel\nworkdays schedules list\nworkdays schedules show\nworkdays schedules plan\nworkdays schedules start\nworkdays schedules pause\nworkdays schedules resume\nworkdays schedules retire\nassignments list\nassignments show\nassignments explain\nassignments watch\nassignments retry\nassignments cancel\nassignments artifacts\nprojects treedx show\nprojects treedx bind\nprojects treedx status\nprojects treedx diagnose\nprojects treedx capabilities\nprojects treedx workspaces list\nprojects treedx workspaces show\nprojects treedx workspaces abandon\nai status\nai mode show\nai mode set\nai inference models\nai inference jobs\nai inference rollback\nai training libraries\nai training jobs\nai training runs\nai lab status\nai lab agents\nai lab libraries\nai qualify status\nai qualify run\nai qualify campaigns\nlibrary show\nlibrary status\nlibrary paths\nlibrary read\nlibrary search\nlibrary query\nlibrary context\nlibrary workspace create\nlibrary workspace show\nlibrary workspace read\nlibrary workspace diff\nlibrary workspace write\nlibrary workspace submit\nlibrary workspace abandon\nlibrary reviews list\nlibrary reviews decide\nlibrary reviews publish\nsave\nstage\nrelease\nstatus\ndiagnose" + local paths="inbox\nsend\ntopics list\ntopics show\ntopics subscribe\ntopics unsubscribe\ncapabilities list\ncapabilities show\nauth login\nauth logout\nauth status\nusers create\nteams list\nteams current\nteams use\nsecrets list\nsecrets status\nsecrets unlock\nsecrets lock\nsecrets rotate\nplatform verify\nplatform workset\nplatform project create\ndev host activate\ndev host status\ndev host deactivate\ndev host guest image import\ndev session start\ndev session stop\ndev use\ndev rebuild\ndev restart\ndev status\ndev logs\ndev plan\ndev freeze\ndev verify\nhost status\nhost doctor\nhost plan\nhost apply\nhost reconcile\nhost events\nhost config show\nhost config plan\nhost config apply\nhost config adopt\nhost topology\nhost connections\nhost provider status\nhost provider credentials list\nhost provider credentials status\nhost provider credentials initialize\nhost storage status\nhost storage connect\nhost storage reconcile\nhost storage rotate\nhost storage reset\nhost security plan\nhost security initialize\nhost security status\nhost security verify\nhost security rotate\nhost security recovery verify\nhost sandbox status\nhost sandbox doctor\nhost fleet status\nhost update status\nhost update check\nhost update apply\nhost update channel\nhost update pause\nhost update resume\nhost component list\nhost component status\nhost component enable\nhost component disable\nhost aliases list\nhost recovery status\nhost recovery retry\nhost recovery restore\nhost bootstrap status\nhost bootstrap enroll\nhost reset\nhost uninstall\nagents list\nagents show\nagents validate\nagents diff\nagents diagnose\nagents classes list\nagents classes show\nagents bindings list\nagents bindings show\nagents bindings explain\nproviders list\nproviders show\nproviders status\nproviders diagnose\nproviders connect\nproviders disconnect\nproviders requests list\nproviders requests show\nproviders requests approve\nproviders requests reject\nproviders credentials status\nproviders credentials rotate\nproviders credentials revoke\nproviders offers show\nproviders offers validate\nproviders offers plan\nproviders offers apply\nseeds validate\nseeds plan\nseeds apply\nseeds show\nseeds verify\ncapacity status\ncapacity explain\ncapacity usage\ncapacity ledger\ncapacity audit\nplans list\nplans show\nplans explain\nplans diff\nworkdays profiles list\nworkdays profiles show\nworkdays profiles validate\nworkdays plan\nworkdays start\nworkdays list\nworkdays show\nworkdays watch\nworkdays pause\nworkdays resume\nworkdays stop\nworkdays cancel\nworkdays schedules list\nworkdays schedules show\nworkdays schedules plan\nworkdays schedules start\nworkdays schedules pause\nworkdays schedules resume\nworkdays schedules retire\nassignments list\nassignments show\nassignments explain\nassignments watch\nassignments retry\nassignments cancel\nassignments artifacts\nprojects treedx show\nprojects treedx bind\nprojects treedx status\nprojects treedx diagnose\nprojects treedx capabilities\nprojects treedx workspaces list\nprojects treedx workspaces show\nprojects treedx workspaces abandon\nai status\nai mode show\nai mode set\nai inference models\nai inference jobs\nai inference rollback\nai training libraries\nai training jobs\nai training runs\nai lab status\nai lab agents\nai lab libraries\nai qualify status\nai qualify run\nai qualify campaigns\nlibrary show\nlibrary status\nlibrary paths\nlibrary read\nlibrary search\nlibrary query\nlibrary context\nlibrary workspace create\nlibrary workspace show\nlibrary workspace read\nlibrary workspace diff\nlibrary workspace write\nlibrary workspace submit\nlibrary workspace abandon\nlibrary reviews list\nlibrary reviews decide\nlibrary reviews publish\nsave\nstage\nrelease\nstatus\ndiagnose" COMPREPLY=( $(compgen -W "$paths" -- "${COMP_WORDS[*]:1}") ) } complete -F _trsd_complete trsd diff --git a/docs/command-reference.md b/docs/command-reference.md index 9357a2e..a07880b 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1063,6 +1063,20 @@ Execution: `local.host.reset`. - `--plan`: Return the exact proposed outcome without mutation. - `--confirm`: Confirm deletion of all manager-owned component data and receipts. +### trsd host uninstall + +Plan or remove every inventoried TreeSeed-owned host resource while preserving unrelated infrastructure and source repositories. + +Operation: mutation. Result schema: `treeseed.host-uninstall-result/v1`. +Execution: `local.host.uninstall`. + +- `--server `: Control-plane server profile or URL. +- `--yes`: Confirm authorized automation. +- `--json`: Emit the stable JSON envelope. +- `--plan`: Return the exact proposed outcome without mutation. +- `--confirm`: Confirm removal of the reviewed TreeSeed resource inventory. +- `--purge-security`: Separately select destruction of encrypted state, credentials, users, and groups. + ## trsd agents Agents operations. diff --git a/package-lock.json b/package-lock.json index fb0c95b..3943f93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@treeseed/cli", - "version": "0.13.0-rc.40", + "version": "0.13.0-rc.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@treeseed/cli", - "version": "0.13.0-rc.40", + "version": "0.13.0-rc.41", "bundleDependencies": [ "ink", "react", @@ -15,7 +15,7 @@ ], "license": "Apache-2.0", "dependencies": { - "@treeseed/sdk": "0.13.0-rc.63", + "@treeseed/sdk": "0.13.0-rc.64", "ink": "^7.1.1", "react": "^19.2.8", "string-width": "^8.2.2", @@ -492,9 +492,9 @@ } }, "node_modules/@treeseed/sdk": { - "version": "0.13.0-rc.63", - "resolved": "https://registry.npmjs.org/@treeseed/sdk/-/sdk-0.13.0-rc.63.tgz", - "integrity": "sha512-YfZVOb2Q1+8hvyzBYd/swi32vGIwY5PjlYK8J8i93LsJEgqKdxmGbOT/gFEf0XPJoQAffiN2vEVONAUX/YEjNw==", + "version": "0.13.0-rc.64", + "resolved": "https://registry.npmjs.org/@treeseed/sdk/-/sdk-0.13.0-rc.64.tgz", + "integrity": "sha512-Z8jXhAzixvU1yztCC0InEjXFiSWXRzh50qDwB/wclWpcyEsmp1a2J/EkAdjbjqj3RCufCt5SnprRdEYktWKk9w==", "dependencies": { "@treeseed/treedx": "0.3.0-rc.4", "esbuild": "^0.28.0", diff --git a/package.json b/package.json index bb60560..6560379 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@treeseed/cli", - "version": "0.13.0-rc.40", + "version": "0.13.0-rc.41", "description": "Operator-facing Treeseed CLI package.", "license": "Apache-2.0", "repository": { @@ -50,7 +50,7 @@ "release:custody": "node --import tsx ./scripts/packages/release-custody.ts" }, "dependencies": { - "@treeseed/sdk": "0.13.0-rc.63", + "@treeseed/sdk": "0.13.0-rc.64", "ink": "^7.1.1", "react": "^19.2.8", "string-width": "^8.2.2", diff --git a/schemas/command-tree.json b/schemas/command-tree.json index a7a4579..908074e 100644 --- a/schemas/command-tree.json +++ b/schemas/command-tree.json @@ -2394,6 +2394,43 @@ "kind": "local", "handlerId": "local.host.reset" } + }, + { + "nodeType": "leaf", + "segment": "uninstall", + "description": "Plan or remove every inventoried TreeSeed-owned host resource while preserving unrelated infrastructure and source repositories.", + "kind": "mutation", + "options": [ + { + "name": "--plan", + "description": "Return the exact proposed outcome without mutation.", + "type": "boolean" + }, + { + "name": "--confirm", + "description": "Confirm removal of the reviewed TreeSeed resource inventory.", + "type": "boolean" + }, + { + "name": "--purge-security", + "description": "Separately select destruction of encrypted state, credentials, users, and groups.", + "type": "boolean" + }, + { + "name": "--yes", + "description": "Confirm non-interactive execution after reviewing the plan.", + "type": "boolean" + } + ], + "authorization": { + "capability": "command.uninstall", + "confirmation": "irreversible" + }, + "resultSchemaId": "treeseed.host-uninstall-result/v1", + "execution": { + "kind": "local", + "handlerId": "local.host.uninstall" + } } ] }, diff --git a/src/cli/commands/host.ts b/src/cli/commands/host.ts index 8a57918..b757183 100644 --- a/src/cli/commands/host.ts +++ b/src/cli/commands/host.ts @@ -41,6 +41,13 @@ function activeTeam(invocation: ParsedInvocation, context: CommandContext) { async function input(invocation: ParsedInvocation, context: CommandContext) { if (invocation.command.execution.kind !== 'local') throw new Error('Host command is not locally bound.'); const { server: _server, json: _json, yes: _yes, ...options } = invocation.options; + if (invocation.command.name === 'host uninstall' && invocation.options.plan !== true) { + if (invocation.options.confirm !== true || invocation.options.yes !== true) { + throw Object.assign(new Error('Host uninstall execution requires both --confirm and --yes after reviewing the plan.'), { + category: 'confirmation_required', code: 'confirmation_required', + }); + } + } if (invocation.command.name === 'host provider credentials initialize') { const initializerId = invocation.arguments[0]; if (!initializerId) throw new Error('A registered provider credential initializer is required. Run `trsd host provider credentials list`.'); @@ -123,7 +130,7 @@ async function input(invocation: ParsedInvocation, context: CommandContext) { export function hostUsesProtectedLocalTransport(invocation: Pick) { return invocation.command.name === 'host config adopt' || invocation.command.name === 'host bootstrap enroll' - || invocation.command.name === 'host reset' || invocation.command.name.startsWith('host storage ') + || invocation.command.name === 'host reset' || invocation.command.name === 'host uninstall' || invocation.command.name.startsWith('host storage ') || invocation.command.name.startsWith('host security ') || invocation.command.name.startsWith('host sandbox ') || invocation.command.name.startsWith('host provider credentials '); } diff --git a/tests/contract/package/thin-package.test.ts b/tests/contract/package/thin-package.test.ts index db842ef..6751c1f 100644 --- a/tests/contract/package/thin-package.test.ts +++ b/tests/contract/package/thin-package.test.ts @@ -9,7 +9,7 @@ test('package has one executable and only its declared CLI runtime dependencies' assert.equal(pkg.types, undefined); assert.equal(pkg.files.some((path: string) => path.startsWith('scripts/')), false); assert.equal(pkg.dependencies['@treeseed/agent'], undefined); - assert.deepEqual(pkg.dependencies, { '@treeseed/sdk': '0.13.0-rc.63', ink: '^7.1.1', react: '^19.2.8', 'string-width': '^8.2.2', yaml: '2.9.0' }); + assert.deepEqual(pkg.dependencies, { '@treeseed/sdk': '0.13.0-rc.64', ink: '^7.1.1', react: '^19.2.8', 'string-width': '^8.2.2', yaml: '2.9.0' }); }); test('built package contains executable runtime only', () => { diff --git a/tests/unit/command-boundary/host-storage-reset.test.ts b/tests/unit/command-boundary/host-storage-reset.test.ts index 611ac04..59504ac 100644 --- a/tests/unit/command-boundary/host-storage-reset.test.ts +++ b/tests/unit/command-boundary/host-storage-reset.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'; import { resolve } from 'node:path'; import test from 'node:test'; import { runCommandLine } from '../../../src/cli/runtime.ts'; +import { hostUsesProtectedLocalTransport } from '../../../src/cli/commands/host.ts'; import { saveServerSession } from '../../../src/cli/support/server-custody.ts'; test('host storage reset is destructive, environment-bounded, and uses retained manager authority', async () => { @@ -24,3 +25,44 @@ test('host storage reset is destructive, environment-bounded, and uses retained assert.equal(payload.bootstrapToken, undefined); } finally { rmSync(root, { recursive: true, force: true }); } }); + +test('host uninstall plan is non-destructive and reaches the protected manager boundary', async () => { + const calls: any[] = []; const output: string[] = []; + const exit = await runCommandLine(['host', 'uninstall', '--plan', '--json'], { + interactiveUi: false, + hostInvoke: async (input) => { calls.push(input); return { schemaVersion: 'treeseed.host-uninstall-result/v1', mode: 'plan', resources: [] }; }, + write: (value) => output.push(value), + }); + assert.equal(exit, 0); + assert.deepEqual(calls, [{ handlerId: 'local.host.uninstall', arguments: [], options: { plan: true } }]); + assert.equal(JSON.parse(output[0]!).mode, 'plan'); + assert.equal(hostUsesProtectedLocalTransport({ command: { name: 'host uninstall' } as any }), true); +}); + +test('host uninstall rejects incomplete execution confirmation before manager invocation', async () => { + for (const argv of [ + ['host', 'uninstall', '--yes', '--json'], + ['host', 'uninstall', '--confirm', '--json'], + ]) { + let invocations = 0; const output: string[] = []; + const exit = await runCommandLine(argv, { + interactiveUi: false, + hostInvoke: async () => { invocations += 1; }, + write: (value) => output.push(value), + }); + assert.equal(exit, 1); + assert.equal(invocations, 0); + assert.equal(JSON.parse(output[0]!).error.category, 'confirmation_required'); + } +}); + +test('host uninstall preserves explicit security purge authorization', async () => { + const calls: any[] = []; + const exit = await runCommandLine(['host', 'uninstall', '--confirm', '--purge-security', '--yes', '--json'], { + interactiveUi: false, + hostInvoke: async (input) => { calls.push(input); return { schemaVersion: 'treeseed.host-uninstall-result/v1', mode: 'execute', removed: [] }; }, + write() {}, + }); + assert.equal(exit, 0); + assert.deepEqual(calls, [{ handlerId: 'local.host.uninstall', arguments: [], options: { confirm: true, purgeSecurity: true } }]); +});