@treeseed/cli 0.4.12 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,14 @@
1
1
  import {
2
+ applyTreeseedConfigValues,
2
3
  applyTreeseedSafeRepairs,
3
4
  collectTreeseedConfigContext,
5
+ ensureTreeseedActVerificationTooling,
6
+ ensureTreeseedSecretSessionForConfig,
4
7
  findNearestTreeseedRoot
5
8
  } from "@treeseed/sdk/workflow-support";
6
9
  import { fail, guidedResult } from "./utils.js";
7
10
  import { buildCliConfigPages, runCliConfigEditor } from "./config-ui.js";
11
+ import { promptForNewPassphrase, promptHidden } from "./secret-prompts.js";
8
12
  import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
9
13
  function normalizeConfigScopes(value) {
10
14
  const requested = Array.isArray(value) ? value.map(String) : typeof value === "string" ? [value] : ["all"];
@@ -31,11 +35,86 @@ function formatPrintEnvReports(payload) {
31
35
  }
32
36
  return lines.filter((line, index, all) => !(line === "" && all[index - 1] === ""));
33
37
  }
38
+ function resolveMouseEnabled(value, env) {
39
+ if (value === true) {
40
+ return true;
41
+ }
42
+ const envValue = env.TREESEED_UI_MOUSE;
43
+ return envValue === "1" || envValue === "true";
44
+ }
45
+ function configReadinessLabel(readiness) {
46
+ if (!readiness) {
47
+ return "pending";
48
+ }
49
+ if (typeof readiness.phase === "string" && readiness.phase.length > 0) {
50
+ return readiness.phase;
51
+ }
52
+ if (readiness.deployable) {
53
+ return "deployable";
54
+ }
55
+ if (readiness.provisioned) {
56
+ return "provisioned";
57
+ }
58
+ if (readiness.configured) {
59
+ return "config_complete";
60
+ }
61
+ return "pending";
62
+ }
63
+ function describeSecretBootstrap(secretSession) {
64
+ if (!secretSession?.status) {
65
+ return "(unknown)";
66
+ }
67
+ if (secretSession.createdWrappedKey) {
68
+ return secretSession.unlockSource === "env" ? "created via env passphrase" : "created via interactive passphrase";
69
+ }
70
+ if (secretSession.migratedWrappedKey) {
71
+ return secretSession.unlockSource === "env" ? "migrated via env passphrase" : "migrated via interactive passphrase";
72
+ }
73
+ if (secretSession.unlockSource === "existing-session") {
74
+ return secretSession.status.unlocked ? "already unlocked" : "locked";
75
+ }
76
+ return secretSession.unlockSource === "env" ? "unlocked via env passphrase" : "unlocked interactively";
77
+ }
78
+ function describeInteractiveSecretBootstrap(secretSession) {
79
+ if (!secretSession?.status) {
80
+ return void 0;
81
+ }
82
+ if (secretSession.createdWrappedKey) {
83
+ return "Wrapped machine key created and unlocked.";
84
+ }
85
+ if (secretSession.migratedWrappedKey) {
86
+ return "Legacy machine key wrapped and unlocked.";
87
+ }
88
+ if (secretSession.unlockSource === "interactive") {
89
+ return "Wrapped machine key unlocked.";
90
+ }
91
+ if (secretSession.unlockSource === "env") {
92
+ return "Wrapped machine key unlocked from TREESEED_KEY_PASSPHRASE.";
93
+ }
94
+ return void 0;
95
+ }
96
+ function describeSharedStorageMigrations(migrations) {
97
+ if (!Array.isArray(migrations) || migrations.length === 0) {
98
+ return void 0;
99
+ }
100
+ return migrations.map((migration) => {
101
+ const label = typeof migration.label === "string" && migration.label.length > 0 ? migration.label : migration.entryId;
102
+ const promotedFrom = typeof migration.promotedFrom === "string" ? migration.promotedFrom : "staging";
103
+ const scopes = Array.isArray(migration.consolidatedScopes) && migration.consolidatedScopes.length > 0 ? migration.consolidatedScopes.join("/") : promotedFrom;
104
+ return migration.hadConflicts ? `${label} consolidated from ${scopes} using ${promotedFrom}` : `${label} promoted from ${promotedFrom}`;
105
+ }).join("; ");
106
+ }
34
107
  function renderConfigResult(commandName, result) {
35
108
  const payload = result.payload;
36
109
  const toolHealth = payload.toolHealth;
110
+ const configContext = payload.context;
111
+ const configReadiness = configContext?.configReadinessByScope?.local ?? {};
37
112
  const readinessByScope = payload.result?.readinessByScope ?? {};
38
- const summary = payload.mode === "print-env-only" ? "Treeseed config environment report completed." : payload.mode === "rotate-machine-key" ? "Treeseed machine key rotated successfully." : "Treeseed config completed successfully.";
113
+ const resourceInventoryByScope = payload.result?.resourceInventoryByScope ?? payload.resourceInventoryByScope ?? {};
114
+ const secretSession = payload.secretSession;
115
+ const sharedStorageMigrations = payload.result?.sharedStorageMigrations;
116
+ const summary = payload.mode === "print-env-only" ? "Treeseed config environment report completed." : payload.mode === "rotate-machine-key" ? "Treeseed machine key rotated successfully." : payload.mode === "connect-market" ? "Knowledge Coop pairing completed successfully." : payload.mode === "bootstrap-preflight" ? "Treeseed bootstrap verification preflight completed." : payload.mode === "bootstrap" ? "Treeseed platform bootstrap completed successfully." : "Treeseed config completed successfully.";
117
+ const market = payload.market;
39
118
  return guidedResult({
40
119
  command: commandName,
41
120
  summary,
@@ -46,15 +125,39 @@ function renderConfigResult(commandName, result) {
46
125
  { label: "Safe repairs", value: Array.isArray(payload.repairs) ? payload.repairs.length : 0 },
47
126
  { label: "Machine config", value: payload.configPath },
48
127
  { label: "Machine key", value: payload.keyPath },
49
- { label: "Local readiness", value: readinessByScope.local?.deployable ? "deployable" : readinessByScope.local?.configured ? "configured" : "pending" },
50
- { label: "Staging readiness", value: readinessByScope.staging?.deployable ? "deployable" : readinessByScope.staging?.provisioned ? "provisioned" : readinessByScope.staging?.configured ? "configured" : "pending" },
51
- { label: "Prod readiness", value: readinessByScope.prod?.deployable ? "deployable" : readinessByScope.prod?.provisioned ? "provisioned" : readinessByScope.prod?.configured ? "configured" : "pending" },
128
+ { label: "Passphrase env", value: payload.passphraseEnv?.configured ? "configured" : "unset" },
129
+ { label: "Secrets session", value: describeSecretBootstrap(secretSession) },
130
+ { label: "Shared consolidations", value: describeSharedStorageMigrations(sharedStorageMigrations) },
131
+ { label: "Deployment key", value: resourceInventoryByScope.staging?.identity?.deploymentKey ?? resourceInventoryByScope.prod?.identity?.deploymentKey ?? "(unset)" },
132
+ { label: "Team", value: resourceInventoryByScope.staging?.identity?.teamId ?? resourceInventoryByScope.prod?.identity?.teamId ?? "(unset)" },
133
+ { label: "Project", value: resourceInventoryByScope.staging?.identity?.projectId ?? resourceInventoryByScope.prod?.identity?.projectId ?? "(unset)" },
134
+ { label: "Local readiness", value: configReadinessLabel(readinessByScope.local) },
135
+ { label: "Staging readiness", value: configReadinessLabel(readinessByScope.staging) },
136
+ { label: "Prod readiness", value: configReadinessLabel(readinessByScope.prod) },
137
+ { label: "Pages project", value: resourceInventoryByScope.staging?.resources?.pagesProject ?? resourceInventoryByScope.prod?.resources?.pagesProject ?? "(unset)" },
138
+ { label: "R2 bucket", value: resourceInventoryByScope.staging?.resources?.contentBucket ?? resourceInventoryByScope.prod?.resources?.contentBucket ?? "(unset)" },
139
+ { label: "GitHub token/config", value: configReadiness.github?.configured ? "configured" : "missing" },
140
+ { label: "Cloudflare token/config", value: configReadiness.cloudflare?.configured ? "configured" : "missing" },
141
+ { label: "Railway token/config", value: configReadiness.railway?.configured ? "configured" : "missing" },
52
142
  { label: "GitHub CLI", value: toolHealth?.githubCli?.available ? "ready" : "missing" },
143
+ { label: "Wrangler CLI", value: toolHealth?.wranglerCli?.available ? "ready" : "missing" },
144
+ { label: "Railway CLI", value: toolHealth?.railwayCli?.available ? "ready" : "missing" },
53
145
  { label: "gh act", value: toolHealth?.ghActExtension?.available ? "ready" : "missing" },
54
146
  { label: "Docker", value: toolHealth?.dockerDaemon?.available ? "ready" : "missing" },
55
- { label: "ACT verify", value: toolHealth?.actVerificationReady ? "ready" : "not ready" }
147
+ { label: "ACT verify", value: toolHealth?.actVerificationReady ? "ready" : "not ready" },
148
+ ...market ? [
149
+ { label: "Market base URL", value: market.baseUrl ?? "(none)" },
150
+ { label: "Market team", value: market.teamSlug ?? market.teamId ?? "(none)" },
151
+ { label: "Market project", value: market.projectSlug ?? market.projectId ?? "(none)" },
152
+ { label: "Hub mode", value: market.hubMode ?? "(unknown)" },
153
+ { label: "Runtime mode", value: market.runtimeMode ?? "(unknown)" },
154
+ { label: "Runtime ready", value: market.runtimeReady ? "yes" : "no" }
155
+ ] : []
156
+ ],
157
+ nextSteps: [
158
+ ...payload.passphraseEnv?.configured ? [] : [payload.passphraseEnv?.recommendedLaunch].filter(Boolean),
159
+ ...renderWorkflowNextSteps(result)
56
160
  ],
57
- nextSteps: renderWorkflowNextSteps(result),
58
161
  report: payload
59
162
  });
60
163
  }
@@ -66,35 +169,86 @@ const handleConfig = async (invocation, context) => {
66
169
  });
67
170
  const scopes = normalizeConfigScopes(invocation.args.environment);
68
171
  const sync = invocation.args.sync;
69
- const interactive = context.outputFormat !== "json" && process.stdin.isTTY && process.stdout.isTTY;
70
- if (interactive && invocation.args.printEnvOnly !== true && invocation.args.rotateMachineKey !== true) {
172
+ const interactive = context.outputFormat !== "json" && context.interactiveUi !== false && process.stdin.isTTY && process.stdout.isTTY;
173
+ const nonInteractive = invocation.args.nonInteractive === true || context.outputFormat === "json";
174
+ const operationalMode = invocation.args.printEnvOnly === true || invocation.args.rotateMachineKey === true || invocation.args.connectMarket === true || invocation.args.bootstrap === true;
175
+ if (!interactive && !nonInteractive && !operationalMode) {
176
+ return fail("Treeseed config requires a TTY for the interactive editor. Re-run in a terminal, or use --non-interactive, --json, --bootstrap, --print-env-only, --rotate-machine-key, or --connect-market.");
177
+ }
178
+ if (interactive && !nonInteractive && !operationalMode) {
71
179
  const tenantRoot = findNearestTreeseedRoot(context.cwd) ?? context.cwd;
72
180
  if (!tenantRoot) {
73
181
  return fail("Treeseed config requires a Treeseed project. Run the command from inside a tenant or initialize one first.");
74
182
  }
75
183
  applyTreeseedSafeRepairs(tenantRoot);
184
+ const toolAvailability = ensureTreeseedActVerificationTooling({
185
+ tenantRoot,
186
+ installIfMissing: invocation.args.installMissingTooling === true,
187
+ env: context.env,
188
+ write: context.write
189
+ });
190
+ const secretSession = await ensureTreeseedSecretSessionForConfig({
191
+ tenantRoot,
192
+ interactive: true,
193
+ env: context.env,
194
+ promptForPassphrase: () => promptHidden("Treeseed passphrase: "),
195
+ promptForNewPassphrase
196
+ });
76
197
  const configContext = collectTreeseedConfigContext({
77
198
  tenantRoot,
78
199
  scopes,
79
200
  env: context.env
80
201
  });
202
+ const initialViewMode = (() => {
203
+ if (invocation.args.full === true) {
204
+ return "full";
205
+ }
206
+ return buildCliConfigPages(configContext, "local", {}, "startup").length > 0 ? "startup" : "full";
207
+ })();
81
208
  const editorResult = await runCliConfigEditor(configContext, {
82
- initialViewMode: invocation.args.full === true ? "full" : "startup"
209
+ initialViewMode,
210
+ mouseEnabled: resolveMouseEnabled(invocation.args.mouse, context.env),
211
+ initialStatusMessage: describeInteractiveSecretBootstrap(secretSession),
212
+ toolAvailability,
213
+ secretSession,
214
+ onCommit: async (update) => {
215
+ applyTreeseedConfigValues({
216
+ tenantRoot,
217
+ updates: [{
218
+ scope: update.scope,
219
+ entryId: update.entryId,
220
+ value: update.value,
221
+ reused: false
222
+ }]
223
+ });
224
+ return collectTreeseedConfigContext({
225
+ tenantRoot,
226
+ scopes,
227
+ env: context.env
228
+ });
229
+ }
83
230
  });
84
231
  if (editorResult === null) {
85
232
  return fail("Treeseed config canceled.");
86
233
  }
87
- const updates = buildCliConfigPages(configContext, "all", editorResult.overrides, "full").map((page) => ({
234
+ const refreshedContext = collectTreeseedConfigContext({
235
+ tenantRoot,
236
+ scopes,
237
+ env: context.env
238
+ });
239
+ const updates = refreshedContext.scopes.flatMap((scope) => buildCliConfigPages(refreshedContext, scope, editorResult.overrides, "full")).filter((page, index, allPages) => allPages.findIndex((candidate) => candidate.key === page.key) === index).map((page) => ({
88
240
  scope: page.scope,
89
241
  entryId: page.entry.id,
90
242
  value: page.finalValue,
91
243
  reused: !(page.key in editorResult.overrides)
92
244
  }));
245
+ context.write("Applying config updates, validating environments, and syncing managed providers...", "stdout");
93
246
  const result2 = await workflow.config({
94
247
  environment: scopes,
95
248
  sync,
96
249
  printEnv: invocation.args.printEnv === true,
97
250
  showSecrets: invocation.args.showSecrets === true,
251
+ installMissingTooling: invocation.args.installMissingTooling === true,
98
252
  nonInteractive: true,
99
253
  updates
100
254
  });
@@ -103,11 +257,23 @@ const handleConfig = async (invocation, context) => {
103
257
  const result = await workflow.config({
104
258
  environment: invocation.args.environment,
105
259
  sync,
260
+ bootstrap: invocation.args.bootstrap === true,
261
+ preflight: invocation.args.preflight === true,
106
262
  printEnv: invocation.args.printEnv === true,
107
263
  printEnvOnly: invocation.args.printEnvOnly === true,
108
264
  showSecrets: invocation.args.showSecrets === true,
109
265
  rotateMachineKey: invocation.args.rotateMachineKey === true,
110
- nonInteractive: context.outputFormat === "json"
266
+ connectMarket: invocation.args.connectMarket === true,
267
+ marketBaseUrl: typeof invocation.args.marketBaseUrl === "string" ? invocation.args.marketBaseUrl : void 0,
268
+ marketTeamId: typeof invocation.args.marketTeamId === "string" ? invocation.args.marketTeamId : void 0,
269
+ marketTeamSlug: typeof invocation.args.marketTeamSlug === "string" ? invocation.args.marketTeamSlug : void 0,
270
+ marketProjectId: typeof invocation.args.marketProjectId === "string" ? invocation.args.marketProjectId : void 0,
271
+ marketProjectSlug: typeof invocation.args.marketProjectSlug === "string" ? invocation.args.marketProjectSlug : void 0,
272
+ marketProjectApiBaseUrl: typeof invocation.args.marketProjectApiBaseUrl === "string" ? invocation.args.marketProjectApiBaseUrl : void 0,
273
+ marketAccessToken: typeof invocation.args.marketAccessToken === "string" ? invocation.args.marketAccessToken : void 0,
274
+ rotateRunnerToken: invocation.args.rotateRunnerToken === true,
275
+ installMissingTooling: invocation.args.installMissingTooling === true,
276
+ nonInteractive
111
277
  });
112
278
  if (context.outputFormat !== "json" && result.payload.mode === "print-env-only") {
113
279
  return {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, resolve } from "node:path";
4
+ import { resolveTreeseedLaunchEnvironment } from "@treeseed/sdk/workflow-support";
4
5
  import { workflowErrorResult } from "./workflow.js";
5
6
  const require2 = createRequire(import.meta.url);
6
7
  function resolveCoreDevEntrypoint(cwd) {
@@ -46,7 +47,11 @@ const handleDev = async (invocation, context) => {
46
47
  const args = watch ? [...resolved.args, "--watch"] : resolved.args;
47
48
  const result = context.spawn(resolved.command, args, {
48
49
  cwd: context.cwd,
49
- env: context.env,
50
+ env: resolveTreeseedLaunchEnvironment({
51
+ tenantRoot: context.cwd,
52
+ scope: "local",
53
+ baseEnv: { ...process.env, ...context.env ?? {} }
54
+ }),
50
55
  stdio: "inherit"
51
56
  });
52
57
  return {
@@ -19,6 +19,8 @@ const handleDoctor = async (invocation, context) => {
19
19
  if (preflight.missingCommands.includes("git")) mustFixNow.push("Install Git.");
20
20
  if (preflight.missingCommands.includes("npm")) mustFixNow.push("Install npm 10 or newer.");
21
21
  if (!state.files.machineConfig) mustFixNow.push("Run `treeseed config --environment local` to create the local machine config.");
22
+ if (!state.secrets.wrappedKeyPresent) mustFixNow.push("Run `treeseed secrets:unlock` to create the wrapped machine key used for local secret storage.");
23
+ if (state.secrets.migrationRequired) mustFixNow.push("Run `treeseed secrets:migrate-key` to replace the legacy plaintext machine key with the wrapped format.");
22
24
  if (state.packageSync.blockers.length > 0) mustFixNow.push(...state.packageSync.blockers);
23
25
  if (state.workflowControl.lock.active && state.workflowControl.lock.runId) {
24
26
  mustFixNow.push(`Active workflow lock detected for run ${state.workflowControl.lock.runId}. Use \`treeseed recover\` before starting another mutating command.`);
@@ -40,16 +42,20 @@ const handleDoctor = async (invocation, context) => {
40
42
  mustFixNow.push(`Missing root workflow contract .github/workflows/${workflowName}.`);
41
43
  }
42
44
  }
43
- if (!state.files.envLocal) optional.push("Create `.env.local` or run `treeseed config --environment local` to generate it.");
44
- if (!state.files.devVars) optional.push("Generate `.dev.vars` by running `treeseed config --environment local`.");
45
- if (!state.auth.gh) optional.push("Configure `GH_TOKEN` for GitHub CLI automation and Copilot-backed workflows.");
46
- if (!state.auth.wrangler) optional.push("Configure `CLOUDFLARE_API_TOKEN` before staging, preview, or production deployment work.");
45
+ if (!state.auth.gh) optional.push("Configure GitHub token/config (`GH_TOKEN`) for GitHub CLI automation and Copilot-backed workflows.");
46
+ if (!state.auth.wrangler) optional.push("Configure Cloudflare token/config (`CLOUDFLARE_API_TOKEN`) before staging, preview, or production deployment work.");
47
47
  if (!state.auth.railway && railwayManagedServicesEnabled) {
48
- optional.push("Configure `RAILWAY_API_TOKEN` before deploying the managed Railway services.");
48
+ optional.push("Configure Railway token/config (`RAILWAY_API_TOKEN`) before deploying the managed Railway services.");
49
49
  }
50
50
  if (!state.auth.remoteApi && state.managedServices.api.enabled) {
51
51
  optional.push("Run `treeseed auth:login` so the CLI can use the configured remote API.");
52
52
  }
53
+ if (state.secrets.wrappedKeyPresent && !state.secrets.keyAgentUnlocked) {
54
+ optional.push("Run `treeseed secrets:unlock` before starting secret-backed dev, deploy, or runner commands.");
55
+ }
56
+ if (!state.secrets.keyAgentRunning) {
57
+ optional.push("The Treeseed key-agent is not running yet. It will start automatically when you unlock the secret session.");
58
+ }
53
59
  if (!state.auth.copilot) optional.push("Configure `GH_TOKEN` if you rely on local Copilot-assisted workflows.");
54
60
  return guidedResult({
55
61
  command: "doctor",
@@ -0,0 +1,2 @@
1
+ export declare function promptHidden(question: string): Promise<string>;
2
+ export declare function promptForNewPassphrase(): Promise<string>;
@@ -0,0 +1,54 @@
1
+ function promptHidden(question) {
2
+ return new Promise((resolvePromise) => {
3
+ const stdin = process.stdin;
4
+ const stdout = process.stdout;
5
+ let value = "";
6
+ function cleanup() {
7
+ stdin.removeListener("data", onData);
8
+ if (stdin.isTTY) {
9
+ stdin.setRawMode(false);
10
+ }
11
+ stdout.write("\n");
12
+ }
13
+ function onData(chunk) {
14
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
15
+ for (const char of text) {
16
+ if (char === "\n" || char === "\r") {
17
+ cleanup();
18
+ resolvePromise(value);
19
+ return;
20
+ }
21
+ if (char === "") {
22
+ cleanup();
23
+ process.exit(1);
24
+ }
25
+ if (char === "\x7F") {
26
+ value = value.slice(0, -1);
27
+ continue;
28
+ }
29
+ value += char;
30
+ }
31
+ }
32
+ stdout.write(question);
33
+ if (stdin.isTTY) {
34
+ stdin.setRawMode(true);
35
+ }
36
+ stdin.resume();
37
+ stdin.on("data", onData);
38
+ });
39
+ }
40
+ async function promptForNewPassphrase() {
41
+ const passphrase = (await promptHidden("New Treeseed passphrase: ")).trim();
42
+ if (!passphrase) {
43
+ throw new Error("A non-empty passphrase is required.");
44
+ }
45
+ const confirmation = (await promptHidden("Confirm passphrase: ")).trim();
46
+ if (passphrase !== confirmation) {
47
+ throw new Error("The passphrase confirmation did not match.");
48
+ }
49
+ return passphrase;
50
+ }
51
+ export {
52
+ promptForNewPassphrase,
53
+ promptHidden
54
+ };
@@ -0,0 +1,7 @@
1
+ import type { TreeseedCommandHandler } from '../types.js';
2
+ export declare const handleSecretsStatus: TreeseedCommandHandler;
3
+ export declare const handleSecretsUnlock: TreeseedCommandHandler;
4
+ export declare const handleSecretsLock: TreeseedCommandHandler;
5
+ export declare const handleSecretsMigrateKey: TreeseedCommandHandler;
6
+ export declare const handleSecretsRotatePassphrase: TreeseedCommandHandler;
7
+ export declare const handleSecretsRotateMachineKey: TreeseedCommandHandler;
@@ -0,0 +1,175 @@
1
+ import {
2
+ inspectTreeseedKeyAgentStatus,
3
+ inspectTreeseedKeyAgentTransportDiagnostic,
4
+ inspectTreeseedPassphraseEnvDiagnostic,
5
+ lockTreeseedSecretSession,
6
+ migrateTreeseedMachineKeyToWrapped,
7
+ rotateTreeseedMachineKey,
8
+ rotateTreeseedMachineKeyPassphrase,
9
+ TREESEED_MACHINE_KEY_PASSPHRASE_ENV,
10
+ TreeseedKeyAgentError,
11
+ unlockTreeseedSecretSessionFromEnv,
12
+ unlockTreeseedSecretSessionInteractive
13
+ } from "@treeseed/sdk/workflow-support";
14
+ import { fail, guidedResult } from "./utils.js";
15
+ import { promptForNewPassphrase } from "./secret-prompts.js";
16
+ async function renderStatus(command, tenantRoot) {
17
+ const status = inspectTreeseedKeyAgentStatus(tenantRoot);
18
+ const passphraseEnv = inspectTreeseedPassphraseEnvDiagnostic();
19
+ const transport = await inspectTreeseedKeyAgentTransportDiagnostic();
20
+ return guidedResult({
21
+ command,
22
+ summary: status.unlocked ? "Treeseed secrets are unlocked." : "Treeseed secrets are locked.",
23
+ facts: [
24
+ { label: "Key agent", value: status.running ? "running" : "stopped" },
25
+ { label: "Wrapped key", value: status.wrappedKeyPresent ? "present" : "missing" },
26
+ { label: "Migration required", value: status.migrationRequired ? "yes" : "no" },
27
+ { label: "Socket", value: transport.socketPresent ? "present" : "missing" },
28
+ { label: "Socket connect", value: transport.socketConnectable ? "yes" : "no" },
29
+ { label: "Socket health", value: transport.healthOk ? "ok" : "failed" },
30
+ { label: "Idle timeout", value: `${Math.round(status.idleTimeoutMs / 1e3)}s` },
31
+ { label: "Idle remaining", value: `${Math.round(status.idleRemainingMs / 1e3)}s` },
32
+ { label: "Passphrase env", value: passphraseEnv.configured ? "configured" : "unset" },
33
+ { label: "Key path", value: status.keyPath },
34
+ { label: "Socket path", value: transport.socketPath }
35
+ ],
36
+ report: {
37
+ status,
38
+ passphraseEnv,
39
+ transport
40
+ },
41
+ nextSteps: [
42
+ ...passphraseEnv.configured ? [] : [passphraseEnv.recommendedLaunch],
43
+ ...transport.lastError ? [`Key-agent transport error: ${transport.lastError}`] : []
44
+ ]
45
+ });
46
+ }
47
+ function keyErrorResult(command, error) {
48
+ if (error instanceof TreeseedKeyAgentError) {
49
+ return guidedResult({
50
+ command,
51
+ summary: error.message,
52
+ exitCode: 1,
53
+ report: {
54
+ code: error.code,
55
+ details: error.details ?? null
56
+ },
57
+ nextSteps: error.code === "interactive_required" ? ["Run this command in a TTY or set TREESEED_KEY_PASSPHRASE for the startup unlock path."] : void 0
58
+ });
59
+ }
60
+ return fail(error instanceof Error ? error.message : String(error));
61
+ }
62
+ const handleSecretsStatus = async (_invocation, context) => renderStatus("secrets:status", context.cwd);
63
+ const handleSecretsUnlock = async (invocation, context) => {
64
+ try {
65
+ const fromEnv = invocation.args.fromEnv === true;
66
+ const status = fromEnv ? unlockTreeseedSecretSessionFromEnv(context.cwd, {
67
+ allowMigration: invocation.args.allowMigration !== false,
68
+ createIfMissing: invocation.args.createIfMissing !== false
69
+ }) : (() => {
70
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
71
+ throw new TreeseedKeyAgentError(
72
+ "interactive_required",
73
+ "Treeseed secrets:unlock requires a TTY unless you use --from-env."
74
+ );
75
+ }
76
+ return unlockTreeseedSecretSessionInteractive(context.cwd);
77
+ })();
78
+ return guidedResult({
79
+ command: "secrets:unlock",
80
+ summary: "Treeseed secrets unlocked.",
81
+ facts: [
82
+ { label: "Key agent", value: status.running ? "running" : "stopped" },
83
+ { label: "Idle remaining", value: `${Math.round(status.idleRemainingMs / 1e3)}s` },
84
+ { label: "Wrapped key", value: status.wrappedKeyPresent ? "present" : "missing" }
85
+ ],
86
+ report: { status }
87
+ });
88
+ } catch (error) {
89
+ return keyErrorResult("secrets:unlock", error);
90
+ }
91
+ };
92
+ const handleSecretsLock = async (_invocation, context) => {
93
+ try {
94
+ const status = lockTreeseedSecretSession(context.cwd);
95
+ return guidedResult({
96
+ command: "secrets:lock",
97
+ summary: "Treeseed secrets locked.",
98
+ facts: [
99
+ { label: "Key agent", value: status.running ? "running" : "stopped" },
100
+ { label: "Wrapped key", value: status.wrappedKeyPresent ? "present" : "missing" }
101
+ ],
102
+ report: { status }
103
+ });
104
+ } catch (error) {
105
+ return keyErrorResult("secrets:lock", error);
106
+ }
107
+ };
108
+ const handleSecretsMigrateKey = async (_invocation, context) => {
109
+ try {
110
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
111
+ throw new TreeseedKeyAgentError("interactive_required", "Treeseed secrets:migrate-key requires a TTY.");
112
+ }
113
+ const passphrase = await promptForNewPassphrase().catch((error) => {
114
+ throw new TreeseedKeyAgentError("unlock_failed", error instanceof Error ? error.message : String(error));
115
+ });
116
+ const result = migrateTreeseedMachineKeyToWrapped(context.cwd, passphrase);
117
+ return guidedResult({
118
+ command: "secrets:migrate-key",
119
+ summary: result.alreadyWrapped ? "Treeseed machine key is already wrapped." : "Treeseed machine key migrated to the wrapped format.",
120
+ facts: [
121
+ { label: "Key path", value: result.keyPath },
122
+ { label: "Migrated", value: result.migrated ? "yes" : "no" }
123
+ ],
124
+ report: result
125
+ });
126
+ } catch (error) {
127
+ return keyErrorResult("secrets:migrate-key", error);
128
+ }
129
+ };
130
+ const handleSecretsRotatePassphrase = async (_invocation, context) => {
131
+ try {
132
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
133
+ throw new TreeseedKeyAgentError("interactive_required", "Treeseed secrets:rotate-passphrase requires a TTY.");
134
+ }
135
+ const passphrase = await promptForNewPassphrase().catch((error) => {
136
+ throw new TreeseedKeyAgentError("unlock_failed", error instanceof Error ? error.message : String(error));
137
+ });
138
+ const result = rotateTreeseedMachineKeyPassphrase(context.cwd, passphrase);
139
+ return guidedResult({
140
+ command: "secrets:rotate-passphrase",
141
+ summary: "Treeseed machine-key passphrase rotated.",
142
+ facts: [{ label: "Key path", value: result.keyPath }],
143
+ report: result
144
+ });
145
+ } catch (error) {
146
+ return keyErrorResult("secrets:rotate-passphrase", error);
147
+ }
148
+ };
149
+ const handleSecretsRotateMachineKey = async (_invocation, context) => {
150
+ try {
151
+ if (!process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV]?.trim()) {
152
+ throw new TreeseedKeyAgentError(
153
+ "interactive_required",
154
+ `Set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} before rotating the machine key.`
155
+ );
156
+ }
157
+ const result = rotateTreeseedMachineKey(context.cwd);
158
+ return guidedResult({
159
+ command: "secrets:rotate-machine-key",
160
+ summary: "Treeseed machine key rotated and re-encrypted.",
161
+ facts: [{ label: "Key path", value: result.keyPath }],
162
+ report: result
163
+ });
164
+ } catch (error) {
165
+ return keyErrorResult("secrets:rotate-machine-key", error);
166
+ }
167
+ };
168
+ export {
169
+ handleSecretsLock,
170
+ handleSecretsMigrateKey,
171
+ handleSecretsRotateMachineKey,
172
+ handleSecretsRotatePassphrase,
173
+ handleSecretsStatus,
174
+ handleSecretsUnlock
175
+ };
@@ -19,17 +19,43 @@ const handleStatus = async (_invocation, context) => {
19
19
  { label: "Package branch aligned", value: state.packageSync.aligned ? "yes" : "no" },
20
20
  { label: "Dirty package repos", value: state.packageSync.dirty ? "yes" : "no" },
21
21
  { label: "Package blockers", value: state.packageSync.blockers.length > 0 ? state.packageSync.blockers.join(" | ") : "(none)" },
22
+ { label: "Local state", value: state.persistentEnvironments.local.phase },
23
+ { label: "Staging state", value: state.persistentEnvironments.staging.phase },
24
+ { label: "Prod state", value: state.persistentEnvironments.prod.phase },
22
25
  { label: "Local initialized", value: state.persistentEnvironments.local.initialized ? "yes" : "no" },
23
26
  { label: "Staging initialized", value: state.persistentEnvironments.staging.initialized ? "yes" : "no" },
24
27
  { label: "Prod initialized", value: state.persistentEnvironments.prod.initialized ? "yes" : "no" },
28
+ { label: "Staging blockers", value: state.persistentEnvironments.staging.blockers.length > 0 ? state.persistentEnvironments.staging.blockers.join(" | ") : "(none)" },
29
+ { label: "Prod blockers", value: state.persistentEnvironments.prod.blockers.length > 0 ? state.persistentEnvironments.prod.blockers.join(" | ") : "(none)" },
25
30
  { label: "Preview enabled", value: state.preview.enabled ? "yes" : "no" },
26
31
  { label: "Preview URL", value: state.preview.url ?? "(none)" },
27
- { label: "GH_TOKEN", value: state.auth.gh ? "ready" : "missing" },
28
- { label: "CLOUDFLARE_API_TOKEN", value: state.auth.wrangler ? "ready" : "missing" },
29
- { label: "RAILWAY_API_TOKEN", value: state.auth.railway ? "ready" : "missing" },
32
+ { label: "GitHub token/config", value: state.auth.gh ? "configured" : "missing" },
33
+ { label: "Cloudflare token/config", value: state.auth.wrangler ? "configured" : "missing" },
34
+ { label: "Railway token/config", value: state.auth.railway ? "configured" : "missing" },
30
35
  { label: "Remote API auth", value: state.auth.remoteApi ? "ready" : "not ready" },
31
- { label: "API service", value: state.managedServices.api.enabled ? `${state.managedServices.api.initialized ? "initialized" : "not initialized"}${state.managedServices.api.lastDeployedUrl ? ` (${state.managedServices.api.lastDeployedUrl})` : ""}` : "disabled" },
32
- { label: "Agents service", value: state.managedServices.agents.enabled ? `${state.managedServices.agents.initialized ? "initialized" : "not initialized"}${state.managedServices.agents.lastDeployedUrl ? ` (${state.managedServices.agents.lastDeployedUrl})` : ""}` : "disabled" }
36
+ { label: "Wrapped machine key", value: state.secrets.wrappedKeyPresent ? "present" : "missing" },
37
+ { label: "Key migration", value: state.secrets.migrationRequired ? "required" : "not needed" },
38
+ { label: "Key agent", value: state.secrets.keyAgentRunning ? state.secrets.keyAgentUnlocked ? "running/unlocked" : "running/locked" : "stopped" },
39
+ { label: "Startup passphrase env", value: state.secrets.startupPassphraseConfigured ? "configured" : "unset" },
40
+ { label: "Market project", value: state.marketConnection.projectSlug ?? state.marketConnection.projectId ?? "(not paired)" },
41
+ { label: "Market team", value: state.marketConnection.teamSlug ?? state.marketConnection.teamId ?? "(not paired)" },
42
+ { label: "Market mode", value: state.marketConnection.connectionMode ?? "(not paired)" },
43
+ { label: "Hub mode", value: state.marketConnection.hubMode ?? "(unknown)" },
44
+ { label: "Runtime mode", value: state.marketConnection.runtimeMode ?? "(unknown)" },
45
+ { label: "Runtime registration", value: state.marketConnection.runtimeRegistration ?? "(none)" },
46
+ { label: "Runtime ready", value: state.marketConnection.runtimeReady ? "yes" : "no" },
47
+ { label: "Web cache host", value: state.webCache.webHost ?? "(none)" },
48
+ { label: "Content cache host", value: state.webCache.contentHost ?? "(none)" },
49
+ { label: "Source page cache", value: state.webCache.sourcePagePolicy ?? "(none)" },
50
+ { label: "Content page cache", value: state.webCache.contentPagePolicy ?? "(none)" },
51
+ { label: "R2 object cache", value: state.webCache.r2ObjectPolicy ?? "(none)" },
52
+ { label: "Cloudflare cache rules", value: state.webCache.cloudflareRulesManaged ? "managed" : "not managed" },
53
+ { label: "Last deploy purge", value: state.webCache.lastDeployPurgeAt ? `${state.webCache.lastDeployPurgeAt} (${state.webCache.lastDeployPurgeCount ?? 0} urls)` : "(none)" },
54
+ { label: "Last content purge", value: state.webCache.lastContentPurgeAt ? `${state.webCache.lastContentPurgeAt} (${state.webCache.lastContentPurgeCount ?? 0} urls)` : "(none)" },
55
+ { label: "Current workstream", value: state.marketConnection.currentWorkstreamId ?? "(none)" },
56
+ { label: "Approval blockers", value: state.marketConnection.approvalBlockers.length > 0 ? state.marketConnection.approvalBlockers.join(" | ") : "(none)" },
57
+ { label: "API service", value: state.managedServices.api.enabled ? `${state.managedServices.api.initialized ? "deployed" : "not deployed"}${state.managedServices.api.lastDeployedUrl ? ` (${state.managedServices.api.lastDeployedUrl})` : ""}` : "disabled" },
58
+ { label: "Worker service", value: state.managedServices.worker.enabled ? `${state.managedServices.worker.initialized ? "deployed" : "not deployed"}${state.managedServices.worker.lastDeployedUrl ? ` (${state.managedServices.worker.lastDeployedUrl})` : ""}` : "disabled" }
33
59
  ],
34
60
  nextSteps: renderWorkflowNextSteps(result),
35
61
  report: {