@treeseed/cli 0.4.12 → 0.5.1

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,85 @@ 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 secretSession = payload.secretSession;
114
+ const sharedStorageMigrations = payload.result?.sharedStorageMigrations;
115
+ 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." : "Treeseed config completed successfully.";
116
+ const market = payload.market;
39
117
  return guidedResult({
40
118
  command: commandName,
41
119
  summary,
@@ -46,13 +124,28 @@ function renderConfigResult(commandName, result) {
46
124
  { label: "Safe repairs", value: Array.isArray(payload.repairs) ? payload.repairs.length : 0 },
47
125
  { label: "Machine config", value: payload.configPath },
48
126
  { 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" },
127
+ { label: "Secrets session", value: describeSecretBootstrap(secretSession) },
128
+ { label: "Shared consolidations", value: describeSharedStorageMigrations(sharedStorageMigrations) },
129
+ { label: "Local readiness", value: configReadinessLabel(readinessByScope.local) },
130
+ { label: "Staging readiness", value: configReadinessLabel(readinessByScope.staging) },
131
+ { label: "Prod readiness", value: configReadinessLabel(readinessByScope.prod) },
132
+ { label: "GitHub token/config", value: configReadiness.github?.configured ? "configured" : "missing" },
133
+ { label: "Cloudflare token/config", value: configReadiness.cloudflare?.configured ? "configured" : "missing" },
134
+ { label: "Railway token/config", value: configReadiness.railway?.configured ? "configured" : "missing" },
52
135
  { label: "GitHub CLI", value: toolHealth?.githubCli?.available ? "ready" : "missing" },
136
+ { label: "Wrangler CLI", value: toolHealth?.wranglerCli?.available ? "ready" : "missing" },
137
+ { label: "Railway CLI", value: toolHealth?.railwayCli?.available ? "ready" : "missing" },
53
138
  { label: "gh act", value: toolHealth?.ghActExtension?.available ? "ready" : "missing" },
54
139
  { label: "Docker", value: toolHealth?.dockerDaemon?.available ? "ready" : "missing" },
55
- { label: "ACT verify", value: toolHealth?.actVerificationReady ? "ready" : "not ready" }
140
+ { label: "ACT verify", value: toolHealth?.actVerificationReady ? "ready" : "not ready" },
141
+ ...market ? [
142
+ { label: "Market base URL", value: market.baseUrl ?? "(none)" },
143
+ { label: "Market team", value: market.teamSlug ?? market.teamId ?? "(none)" },
144
+ { label: "Market project", value: market.projectSlug ?? market.projectId ?? "(none)" },
145
+ { label: "Hub mode", value: market.hubMode ?? "(unknown)" },
146
+ { label: "Runtime mode", value: market.runtimeMode ?? "(unknown)" },
147
+ { label: "Runtime ready", value: market.runtimeReady ? "yes" : "no" }
148
+ ] : []
56
149
  ],
57
150
  nextSteps: renderWorkflowNextSteps(result),
58
151
  report: payload
@@ -66,35 +159,86 @@ const handleConfig = async (invocation, context) => {
66
159
  });
67
160
  const scopes = normalizeConfigScopes(invocation.args.environment);
68
161
  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) {
162
+ const interactive = context.outputFormat !== "json" && context.interactiveUi !== false && process.stdin.isTTY && process.stdout.isTTY;
163
+ const nonInteractive = invocation.args.nonInteractive === true || context.outputFormat === "json";
164
+ const operationalMode = invocation.args.printEnvOnly === true || invocation.args.rotateMachineKey === true || invocation.args.connectMarket === true;
165
+ if (!interactive && !nonInteractive && !operationalMode) {
166
+ return fail("Treeseed config requires a TTY for the interactive editor. Re-run in a terminal, or use --non-interactive, --json, --print-env-only, --rotate-machine-key, or --connect-market.");
167
+ }
168
+ if (interactive && !nonInteractive && !operationalMode) {
71
169
  const tenantRoot = findNearestTreeseedRoot(context.cwd) ?? context.cwd;
72
170
  if (!tenantRoot) {
73
171
  return fail("Treeseed config requires a Treeseed project. Run the command from inside a tenant or initialize one first.");
74
172
  }
75
173
  applyTreeseedSafeRepairs(tenantRoot);
174
+ const toolAvailability = ensureTreeseedActVerificationTooling({
175
+ tenantRoot,
176
+ installIfMissing: invocation.args.installMissingTooling === true,
177
+ env: context.env,
178
+ write: context.write
179
+ });
180
+ const secretSession = await ensureTreeseedSecretSessionForConfig({
181
+ tenantRoot,
182
+ interactive: true,
183
+ env: context.env,
184
+ promptForPassphrase: () => promptHidden("Treeseed passphrase: "),
185
+ promptForNewPassphrase
186
+ });
76
187
  const configContext = collectTreeseedConfigContext({
77
188
  tenantRoot,
78
189
  scopes,
79
190
  env: context.env
80
191
  });
192
+ const initialViewMode = (() => {
193
+ if (invocation.args.full === true) {
194
+ return "full";
195
+ }
196
+ return buildCliConfigPages(configContext, "local", {}, "startup").length > 0 ? "startup" : "full";
197
+ })();
81
198
  const editorResult = await runCliConfigEditor(configContext, {
82
- initialViewMode: invocation.args.full === true ? "full" : "startup"
199
+ initialViewMode,
200
+ mouseEnabled: resolveMouseEnabled(invocation.args.mouse, context.env),
201
+ initialStatusMessage: describeInteractiveSecretBootstrap(secretSession),
202
+ toolAvailability,
203
+ secretSession,
204
+ onCommit: async (update) => {
205
+ applyTreeseedConfigValues({
206
+ tenantRoot,
207
+ updates: [{
208
+ scope: update.scope,
209
+ entryId: update.entryId,
210
+ value: update.value,
211
+ reused: false
212
+ }]
213
+ });
214
+ return collectTreeseedConfigContext({
215
+ tenantRoot,
216
+ scopes,
217
+ env: context.env
218
+ });
219
+ }
83
220
  });
84
221
  if (editorResult === null) {
85
222
  return fail("Treeseed config canceled.");
86
223
  }
87
- const updates = buildCliConfigPages(configContext, "all", editorResult.overrides, "full").map((page) => ({
224
+ const refreshedContext = collectTreeseedConfigContext({
225
+ tenantRoot,
226
+ scopes,
227
+ env: context.env
228
+ });
229
+ 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
230
  scope: page.scope,
89
231
  entryId: page.entry.id,
90
232
  value: page.finalValue,
91
233
  reused: !(page.key in editorResult.overrides)
92
234
  }));
235
+ context.write("Applying config updates, validating environments, and syncing managed providers...", "stdout");
93
236
  const result2 = await workflow.config({
94
237
  environment: scopes,
95
238
  sync,
96
239
  printEnv: invocation.args.printEnv === true,
97
240
  showSecrets: invocation.args.showSecrets === true,
241
+ installMissingTooling: invocation.args.installMissingTooling === true,
98
242
  nonInteractive: true,
99
243
  updates
100
244
  });
@@ -107,7 +251,17 @@ const handleConfig = async (invocation, context) => {
107
251
  printEnvOnly: invocation.args.printEnvOnly === true,
108
252
  showSecrets: invocation.args.showSecrets === true,
109
253
  rotateMachineKey: invocation.args.rotateMachineKey === true,
110
- nonInteractive: context.outputFormat === "json"
254
+ connectMarket: invocation.args.connectMarket === true,
255
+ marketBaseUrl: typeof invocation.args.marketBaseUrl === "string" ? invocation.args.marketBaseUrl : void 0,
256
+ marketTeamId: typeof invocation.args.marketTeamId === "string" ? invocation.args.marketTeamId : void 0,
257
+ marketTeamSlug: typeof invocation.args.marketTeamSlug === "string" ? invocation.args.marketTeamSlug : void 0,
258
+ marketProjectId: typeof invocation.args.marketProjectId === "string" ? invocation.args.marketProjectId : void 0,
259
+ marketProjectSlug: typeof invocation.args.marketProjectSlug === "string" ? invocation.args.marketProjectSlug : void 0,
260
+ marketProjectApiBaseUrl: typeof invocation.args.marketProjectApiBaseUrl === "string" ? invocation.args.marketProjectApiBaseUrl : void 0,
261
+ marketAccessToken: typeof invocation.args.marketAccessToken === "string" ? invocation.args.marketAccessToken : void 0,
262
+ rotateRunnerToken: invocation.args.rotateRunnerToken === true,
263
+ installMissingTooling: invocation.args.installMissingTooling === true,
264
+ nonInteractive
111
265
  });
112
266
  if (context.outputFormat !== "json" && result.payload.mode === "print-env-only") {
113
267
  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,161 @@
1
+ import {
2
+ inspectTreeseedKeyAgentStatus,
3
+ lockTreeseedSecretSession,
4
+ migrateTreeseedMachineKeyToWrapped,
5
+ rotateTreeseedMachineKey,
6
+ rotateTreeseedMachineKeyPassphrase,
7
+ TREESEED_MACHINE_KEY_PASSPHRASE_ENV,
8
+ TreeseedKeyAgentError,
9
+ unlockTreeseedSecretSessionFromEnv,
10
+ unlockTreeseedSecretSessionInteractive
11
+ } from "@treeseed/sdk/workflow-support";
12
+ import { fail, guidedResult } from "./utils.js";
13
+ import { promptForNewPassphrase } from "./secret-prompts.js";
14
+ function renderStatus(command, tenantRoot) {
15
+ const status = inspectTreeseedKeyAgentStatus(tenantRoot);
16
+ return guidedResult({
17
+ command,
18
+ summary: status.unlocked ? "Treeseed secrets are unlocked." : "Treeseed secrets are locked.",
19
+ facts: [
20
+ { label: "Key agent", value: status.running ? "running" : "stopped" },
21
+ { label: "Wrapped key", value: status.wrappedKeyPresent ? "present" : "missing" },
22
+ { label: "Migration required", value: status.migrationRequired ? "yes" : "no" },
23
+ { label: "Idle timeout", value: `${Math.round(status.idleTimeoutMs / 1e3)}s` },
24
+ { label: "Idle remaining", value: `${Math.round(status.idleRemainingMs / 1e3)}s` },
25
+ { label: "Passphrase env", value: process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV]?.trim() ? "configured" : "unset" },
26
+ { label: "Key path", value: status.keyPath }
27
+ ],
28
+ report: {
29
+ status
30
+ }
31
+ });
32
+ }
33
+ function keyErrorResult(command, error) {
34
+ if (error instanceof TreeseedKeyAgentError) {
35
+ return guidedResult({
36
+ command,
37
+ summary: error.message,
38
+ exitCode: 1,
39
+ report: {
40
+ code: error.code,
41
+ details: error.details ?? null
42
+ },
43
+ nextSteps: error.code === "interactive_required" ? ["Run this command in a TTY or set TREESEED_KEY_PASSPHRASE for the startup unlock path."] : void 0
44
+ });
45
+ }
46
+ return fail(error instanceof Error ? error.message : String(error));
47
+ }
48
+ const handleSecretsStatus = async (_invocation, context) => renderStatus("secrets:status", context.cwd);
49
+ const handleSecretsUnlock = async (invocation, context) => {
50
+ try {
51
+ const fromEnv = invocation.args.fromEnv === true;
52
+ const status = fromEnv ? unlockTreeseedSecretSessionFromEnv(context.cwd, {
53
+ allowMigration: invocation.args.allowMigration !== false,
54
+ createIfMissing: invocation.args.createIfMissing !== false
55
+ }) : (() => {
56
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
57
+ throw new TreeseedKeyAgentError(
58
+ "interactive_required",
59
+ "Treeseed secrets:unlock requires a TTY unless you use --from-env."
60
+ );
61
+ }
62
+ return unlockTreeseedSecretSessionInteractive(context.cwd);
63
+ })();
64
+ return guidedResult({
65
+ command: "secrets:unlock",
66
+ summary: "Treeseed secrets unlocked.",
67
+ facts: [
68
+ { label: "Key agent", value: status.running ? "running" : "stopped" },
69
+ { label: "Idle remaining", value: `${Math.round(status.idleRemainingMs / 1e3)}s` },
70
+ { label: "Wrapped key", value: status.wrappedKeyPresent ? "present" : "missing" }
71
+ ],
72
+ report: { status }
73
+ });
74
+ } catch (error) {
75
+ return keyErrorResult("secrets:unlock", error);
76
+ }
77
+ };
78
+ const handleSecretsLock = async (_invocation, context) => {
79
+ try {
80
+ const status = lockTreeseedSecretSession(context.cwd);
81
+ return guidedResult({
82
+ command: "secrets:lock",
83
+ summary: "Treeseed secrets locked.",
84
+ facts: [
85
+ { label: "Key agent", value: status.running ? "running" : "stopped" },
86
+ { label: "Wrapped key", value: status.wrappedKeyPresent ? "present" : "missing" }
87
+ ],
88
+ report: { status }
89
+ });
90
+ } catch (error) {
91
+ return keyErrorResult("secrets:lock", error);
92
+ }
93
+ };
94
+ const handleSecretsMigrateKey = async (_invocation, context) => {
95
+ try {
96
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
97
+ throw new TreeseedKeyAgentError("interactive_required", "Treeseed secrets:migrate-key requires a TTY.");
98
+ }
99
+ const passphrase = await promptForNewPassphrase().catch((error) => {
100
+ throw new TreeseedKeyAgentError("unlock_failed", error instanceof Error ? error.message : String(error));
101
+ });
102
+ const result = migrateTreeseedMachineKeyToWrapped(context.cwd, passphrase);
103
+ return guidedResult({
104
+ command: "secrets:migrate-key",
105
+ summary: result.alreadyWrapped ? "Treeseed machine key is already wrapped." : "Treeseed machine key migrated to the wrapped format.",
106
+ facts: [
107
+ { label: "Key path", value: result.keyPath },
108
+ { label: "Migrated", value: result.migrated ? "yes" : "no" }
109
+ ],
110
+ report: result
111
+ });
112
+ } catch (error) {
113
+ return keyErrorResult("secrets:migrate-key", error);
114
+ }
115
+ };
116
+ const handleSecretsRotatePassphrase = async (_invocation, context) => {
117
+ try {
118
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
119
+ throw new TreeseedKeyAgentError("interactive_required", "Treeseed secrets:rotate-passphrase requires a TTY.");
120
+ }
121
+ const passphrase = await promptForNewPassphrase().catch((error) => {
122
+ throw new TreeseedKeyAgentError("unlock_failed", error instanceof Error ? error.message : String(error));
123
+ });
124
+ const result = rotateTreeseedMachineKeyPassphrase(context.cwd, passphrase);
125
+ return guidedResult({
126
+ command: "secrets:rotate-passphrase",
127
+ summary: "Treeseed machine-key passphrase rotated.",
128
+ facts: [{ label: "Key path", value: result.keyPath }],
129
+ report: result
130
+ });
131
+ } catch (error) {
132
+ return keyErrorResult("secrets:rotate-passphrase", error);
133
+ }
134
+ };
135
+ const handleSecretsRotateMachineKey = async (_invocation, context) => {
136
+ try {
137
+ if (!process.env[TREESEED_MACHINE_KEY_PASSPHRASE_ENV]?.trim()) {
138
+ throw new TreeseedKeyAgentError(
139
+ "interactive_required",
140
+ `Set ${TREESEED_MACHINE_KEY_PASSPHRASE_ENV} before rotating the machine key.`
141
+ );
142
+ }
143
+ const result = rotateTreeseedMachineKey(context.cwd);
144
+ return guidedResult({
145
+ command: "secrets:rotate-machine-key",
146
+ summary: "Treeseed machine key rotated and re-encrypted.",
147
+ facts: [{ label: "Key path", value: result.keyPath }],
148
+ report: result
149
+ });
150
+ } catch (error) {
151
+ return keyErrorResult("secrets:rotate-machine-key", error);
152
+ }
153
+ };
154
+ export {
155
+ handleSecretsLock,
156
+ handleSecretsMigrateKey,
157
+ handleSecretsRotateMachineKey,
158
+ handleSecretsRotatePassphrase,
159
+ handleSecretsStatus,
160
+ handleSecretsUnlock
161
+ };
@@ -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: {
@@ -1,4 +1,5 @@
1
1
  import { TreeseedWorkflowError, TreeseedWorkflowSdk } from "@treeseed/sdk/workflow";
2
+ import { TreeseedKeyAgentError } from "@treeseed/sdk/workflow-support";
2
3
  function createWorkflowSdk(context, overrides = {}) {
3
4
  return new TreeseedWorkflowSdk({
4
5
  cwd: context.cwd,
@@ -40,6 +41,36 @@ function workflowErrorResult(error) {
40
41
  }
41
42
  };
42
43
  }
44
+ if (error instanceof TreeseedKeyAgentError) {
45
+ return {
46
+ exitCode: 1,
47
+ stderr: [error.message],
48
+ report: {
49
+ schemaVersion: 1,
50
+ kind: "treeseed.workflow.result",
51
+ command: "status",
52
+ executionMode: "execute",
53
+ runId: null,
54
+ ok: false,
55
+ operation: "status",
56
+ summary: error.message,
57
+ facts: [],
58
+ error: error.message,
59
+ code: error.code,
60
+ payload: null,
61
+ result: null,
62
+ nextSteps: [],
63
+ recovery: null,
64
+ errors: [
65
+ {
66
+ code: error.code,
67
+ message: error.message,
68
+ details: error.details ?? null
69
+ }
70
+ ]
71
+ }
72
+ };
73
+ }
43
74
  const message = error instanceof Error ? error.message : String(error);
44
75
  return {
45
76
  exitCode: 1,
@@ -482,7 +482,7 @@ function canRenderInkHelp(context) {
482
482
  );
483
483
  }
484
484
  function isNonHumanInteractiveEnvironment() {
485
- return process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true" || process.env.ACT === "true" || process.env.TREESEED_VERIFY_DRIVER === "act";
485
+ return process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true" || process.env.ACT === "true" || typeof process.env.TREESEED_VERIFY_DRIVER === "string";
486
486
  }
487
487
  export {
488
488
  renderTreeseedHelpInk,