@treeseed/cli 0.4.11 → 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.
Files changed (40) hide show
  1. package/README.md +35 -8
  2. package/dist/cli/handlers/auth-login.js +58 -46
  3. package/dist/cli/handlers/auth-logout.js +23 -11
  4. package/dist/cli/handlers/auth-whoami.js +27 -16
  5. package/dist/cli/handlers/close.js +9 -4
  6. package/dist/cli/handlers/config-ui.d.ts +65 -9
  7. package/dist/cli/handlers/config-ui.js +561 -175
  8. package/dist/cli/handlers/config.js +164 -10
  9. package/dist/cli/handlers/destroy.js +3 -2
  10. package/dist/cli/handlers/dev.js +6 -1
  11. package/dist/cli/handlers/doctor.js +82 -47
  12. package/dist/cli/handlers/recover.d.ts +2 -0
  13. package/dist/cli/handlers/recover.js +25 -0
  14. package/dist/cli/handlers/release.js +14 -4
  15. package/dist/cli/handlers/resume.d.ts +2 -0
  16. package/dist/cli/handlers/resume.js +23 -0
  17. package/dist/cli/handlers/save.js +9 -3
  18. package/dist/cli/handlers/secret-prompts.d.ts +2 -0
  19. package/dist/cli/handlers/secret-prompts.js +54 -0
  20. package/dist/cli/handlers/secrets.d.ts +7 -0
  21. package/dist/cli/handlers/secrets.js +161 -0
  22. package/dist/cli/handlers/stage.js +10 -4
  23. package/dist/cli/handlers/status.js +37 -5
  24. package/dist/cli/handlers/switch.js +11 -4
  25. package/dist/cli/handlers/tasks.js +1 -0
  26. package/dist/cli/handlers/utils.js +15 -8
  27. package/dist/cli/handlers/workflow.js +74 -3
  28. package/dist/cli/help-ui.js +1 -1
  29. package/dist/cli/operations-registry.js +200 -21
  30. package/dist/cli/registry.d.ts +8 -0
  31. package/dist/cli/registry.js +19 -1
  32. package/dist/cli/repair.js +5 -9
  33. package/dist/cli/ui/framework.d.ts +2 -0
  34. package/dist/cli/ui/framework.js +53 -22
  35. package/dist/cli/ui/mouse.d.ts +3 -1
  36. package/dist/cli/ui/mouse.js +3 -3
  37. package/package.json +7 -6
  38. package/scripts/verify-driver.mjs +34 -0
  39. package/dist/cli/workflow-state.d.ts +0 -58
  40. package/dist/cli/workflow-state.js +0 -195
@@ -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
+ };
@@ -3,23 +3,29 @@ import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from
3
3
  const handleStage = async (invocation, context) => {
4
4
  try {
5
5
  const result = await createWorkflowSdk(context).stage({
6
- message: invocation.positionals.join(" ").trim()
6
+ message: invocation.positionals.join(" ").trim(),
7
+ plan: invocation.args.plan === true || invocation.args.dryRun === true,
8
+ dryRun: invocation.args.dryRun === true
7
9
  });
8
10
  const payload = result.payload;
11
+ const mergedPackages = payload.repos.filter((repo) => repo.merged).length;
9
12
  return guidedResult({
10
13
  command: invocation.commandName || "stage",
11
- summary: "Treeseed stage completed successfully.",
14
+ summary: result.executionMode === "plan" ? "Treeseed stage plan ready." : "Treeseed stage completed successfully.",
12
15
  facts: [
16
+ { label: "Mode", value: payload.mode },
13
17
  { label: "Merged branch", value: payload.branchName },
14
18
  { label: "Merge target", value: payload.mergeTarget },
19
+ { label: "Merge strategy", value: payload.mergeStrategy },
15
20
  { label: "Auto-saved", value: payload.autoSaved ? "yes" : "no" },
16
- { label: "Deprecated tag", value: payload.deprecatedTag.tagName },
21
+ { label: "Deprecated tag", value: payload.rootRepo.tagName ?? payload.deprecatedTag.tagName },
22
+ { label: "Package merges", value: String(mergedPackages) },
17
23
  { label: "Staging wait", value: payload.stagingWait.status },
18
24
  { label: "Preview cleanup", value: payload.previewCleanup.performed ? "performed" : "not needed" },
19
25
  { label: "Final branch", value: payload.finalBranch }
20
26
  ],
21
27
  nextSteps: renderWorkflowNextSteps(result),
22
- report: payload
28
+ report: result
23
29
  });
24
30
  } catch (error) {
25
31
  return workflowErrorResult(error);
@@ -14,20 +14,52 @@ const handleStatus = async (_invocation, context) => {
14
14
  { label: "Branch role", value: state.branchRole },
15
15
  { label: "Mapped environment", value: state.environment },
16
16
  { label: "Dirty worktree", value: state.dirtyWorktree ? "yes" : "no" },
17
+ { label: "Package mode", value: state.packageSync.mode },
18
+ { label: "Full package checkout", value: state.packageSync.completeCheckout ? "yes" : "no" },
19
+ { label: "Package branch aligned", value: state.packageSync.aligned ? "yes" : "no" },
20
+ { label: "Dirty package repos", value: state.packageSync.dirty ? "yes" : "no" },
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 },
17
25
  { label: "Local initialized", value: state.persistentEnvironments.local.initialized ? "yes" : "no" },
18
26
  { label: "Staging initialized", value: state.persistentEnvironments.staging.initialized ? "yes" : "no" },
19
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)" },
20
30
  { label: "Preview enabled", value: state.preview.enabled ? "yes" : "no" },
21
31
  { label: "Preview URL", value: state.preview.url ?? "(none)" },
22
- { label: "GH_TOKEN", value: state.auth.gh ? "ready" : "missing" },
23
- { label: "CLOUDFLARE_API_TOKEN", value: state.auth.wrangler ? "ready" : "missing" },
24
- { 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" },
25
35
  { label: "Remote API auth", value: state.auth.remoteApi ? "ready" : "not ready" },
26
- { label: "API service", value: state.managedServices.api.enabled ? `${state.managedServices.api.initialized ? "initialized" : "not initialized"}${state.managedServices.api.lastDeployedUrl ? ` (${state.managedServices.api.lastDeployedUrl})` : ""}` : "disabled" },
27
- { 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" }
28
59
  ],
29
60
  nextSteps: renderWorkflowNextSteps(result),
30
61
  report: {
62
+ ...result,
31
63
  state
32
64
  }
33
65
  });
@@ -5,20 +5,27 @@ const handleSwitch = async (invocation, context) => {
5
5
  const branch = invocation.positionals[0];
6
6
  const result = await createWorkflowSdk(context).switchTask({
7
7
  branch,
8
- preview: invocation.args.preview === true
8
+ preview: invocation.args.preview === true,
9
+ plan: invocation.args.plan === true || invocation.args.dryRun === true,
10
+ dryRun: invocation.args.dryRun === true
9
11
  });
10
12
  const payload = result.payload;
13
+ const packageCreated = payload.repos.filter((repo) => repo.created).length;
14
+ const packageResumed = payload.repos.filter((repo) => repo.resumed).length;
11
15
  return guidedResult({
12
16
  command: invocation.commandName || "switch",
13
- summary: payload.created ? `Created task branch ${payload.branchName}.` : payload.resumed ? `Switched to task branch ${payload.branchName}.` : `Task branch ${payload.branchName} is ready.`,
17
+ summary: result.executionMode === "plan" ? `Switch plan for ${payload.branchName}.` : payload.rootRepo.created ? `Created task branch ${payload.branchName}.` : payload.rootRepo.resumed ? `Switched to task branch ${payload.branchName}.` : `Task branch ${payload.branchName} is ready.`,
14
18
  facts: [
19
+ { label: "Mode", value: payload.mode },
15
20
  { label: "Branch", value: payload.branchName },
16
- { label: "Created", value: payload.created ? "yes" : "no" },
21
+ { label: "Market created", value: payload.rootRepo.created ? "yes" : "no" },
22
+ { label: "Package branches created", value: String(packageCreated) },
23
+ { label: "Package branches resumed", value: String(packageResumed) },
17
24
  { label: "Preview", value: payload.preview.enabled ? "enabled" : "disabled" },
18
25
  { label: "Preview URL", value: payload.preview.url ?? "(none)" }
19
26
  ],
20
27
  nextSteps: renderWorkflowNextSteps(result),
21
- report: payload
28
+ report: result
22
29
  });
23
30
  } catch (error) {
24
31
  return workflowErrorResult(error);
@@ -19,6 +19,7 @@ const handleTasks = async (_invocation, context) => {
19
19
  })),
20
20
  nextSteps: renderWorkflowNextSteps(result),
21
21
  report: {
22
+ ...result,
22
23
  tasks
23
24
  }
24
25
  });
@@ -19,18 +19,25 @@ function guidedResult(options) {
19
19
  lines.push(`- ${step}`);
20
20
  }
21
21
  }
22
+ const report = options.report && typeof options.report === "object" ? {
23
+ ...options.report,
24
+ command: options.command,
25
+ ok: (options.exitCode ?? 0) === 0,
26
+ summary: options.summary,
27
+ facts: facts.map((fact) => ({ label: fact.label, value: fact.value })),
28
+ nextSteps: options.nextSteps ?? []
29
+ } : {
30
+ command: options.command,
31
+ ok: (options.exitCode ?? 0) === 0,
32
+ summary: options.summary,
33
+ facts: facts.map((fact) => ({ label: fact.label, value: fact.value })),
34
+ nextSteps: options.nextSteps ?? []
35
+ };
22
36
  return {
23
37
  exitCode: options.exitCode ?? 0,
24
38
  stdout: lines,
25
39
  stderr: options.stderr,
26
- report: {
27
- command: options.command,
28
- ok: (options.exitCode ?? 0) === 0,
29
- summary: options.summary,
30
- facts: facts.map((fact) => ({ label: fact.label, value: fact.value })),
31
- nextSteps: options.nextSteps ?? [],
32
- ...options.report ?? {}
33
- }
40
+ report
34
41
  };
35
42
  }
36
43
  function writeResult(result, context) {
@@ -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,
@@ -10,15 +11,63 @@ function createWorkflowSdk(context, overrides = {}) {
10
11
  }
11
12
  function workflowErrorResult(error) {
12
13
  if (error instanceof TreeseedWorkflowError) {
14
+ const recovery = error.details?.recovery ?? null;
13
15
  return {
14
16
  exitCode: error.exitCode ?? (error.code === "merge_conflict" ? 12 : 1),
15
17
  stderr: [error.message],
16
18
  report: {
19
+ schemaVersion: 1,
20
+ kind: "treeseed.workflow.result",
21
+ command: error.operation,
22
+ executionMode: "execute",
23
+ runId: typeof recovery?.runId === "string" ? recovery.runId : null,
17
24
  ok: false,
25
+ operation: error.operation,
26
+ summary: error.message,
27
+ facts: [],
18
28
  error: error.message,
19
29
  code: error.code,
20
- operation: error.operation,
21
- details: error.details ?? null
30
+ payload: null,
31
+ result: null,
32
+ nextSteps: [],
33
+ recovery,
34
+ errors: [
35
+ {
36
+ code: error.code,
37
+ message: error.message,
38
+ details: error.details ?? null
39
+ }
40
+ ]
41
+ }
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
+ ]
22
71
  }
23
72
  };
24
73
  }
@@ -27,8 +76,26 @@ function workflowErrorResult(error) {
27
76
  exitCode: 1,
28
77
  stderr: [message],
29
78
  report: {
79
+ schemaVersion: 1,
80
+ kind: "treeseed.workflow.result",
81
+ command: "status",
82
+ executionMode: "execute",
83
+ runId: null,
30
84
  ok: false,
31
- error: message
85
+ operation: "status",
86
+ summary: message,
87
+ facts: [],
88
+ error: message,
89
+ payload: null,
90
+ result: null,
91
+ nextSteps: [],
92
+ recovery: null,
93
+ errors: [
94
+ {
95
+ code: "unsupported_state",
96
+ message
97
+ }
98
+ ]
32
99
  }
33
100
  };
34
101
  }
@@ -45,6 +112,10 @@ function renderWorkflowNextStep(step) {
45
112
  return `treeseed stage "${String(input.message ?? "describe the resolution")}"`;
46
113
  case "release":
47
114
  return `treeseed release --${String(input.bump ?? "patch")}`;
115
+ case "resume":
116
+ return `treeseed resume ${String(input.runId ?? "<run-id>")}`;
117
+ case "recover":
118
+ return "treeseed recover";
48
119
  case "config": {
49
120
  const environments = Array.isArray(input.environment) ? input.environment : Array.isArray(input.target) ? input.target : null;
50
121
  return environments?.length ? `treeseed config --environment ${environments[0]}` : "treeseed config";
@@ -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,