@treeseed/cli 0.4.10 → 0.4.12

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.
package/README.md CHANGED
@@ -21,11 +21,14 @@ npm install @treeseed/cli @treeseed/core @treeseed/sdk
21
21
 
22
22
  Workflow guarantees:
23
23
 
24
- - `treeseed init`, `treeseed config`, and `treeseed release` resolve the project from nested directories and do not rely on the currently checked-out task branch.
25
- - `treeseed switch` requires a clean worktree before leaving the current branch and creates new task branches from the latest `staging`.
26
- - `treeseed save` is the canonical checkpoint command: it syncs the current branch with origin, succeeds even when no new changes exist, and can create or refresh preview deployments with `--preview`.
27
- - `treeseed stage` and `treeseed close` auto-save meaningful uncommitted task-branch changes before merge or cleanup, then leave the repository on `staging`.
28
- - `treeseed release` completes on `staging` after promoting `staging` into `main` and pushing the release tag.
24
+ - `treeseed` is the only supported project-management surface for market and any checked-out `packages/sdk`, `packages/core`, and `packages/cli` repos.
25
+ - `treeseed switch` requires clean worktrees, mirrors the task branch into checked-out package repos, and only pushes the market branch on branch creation.
26
+ - `treeseed save` is the canonical recursive checkpoint command: it verifies, commits, and pushes dirty package repos in dependency order before saving the market repo.
27
+ - `treeseed stage` squash-merges task branches into `staging` across package repos first, refreshes market submodule pointers to package `staging` heads, then stages the market repo.
28
+ - `treeseed close` recursively archives and deletes matching task branches across market and checked-out package repos.
29
+ - `treeseed release` only bumps, tags, and publishes changed packages plus internal dependents, then syncs market production to package `main` heads.
30
+ - Every mutating workflow command supports `--plan`; `--dry-run` is only an alias where it still exists for compatibility.
31
+ - Interrupted workflow runs are journaled under `.treeseed/workflow`; use `treeseed recover` to inspect them and `treeseed resume <run-id>` to continue a resumable run.
29
32
 
30
33
  After installation, the published binary is available as:
31
34
 
@@ -42,11 +45,13 @@ The main workflow commands exposed by the current CLI are:
42
45
  - `treeseed tasks [--json]`
43
46
  - `treeseed switch <branch-name> [--preview]`
44
47
  - `treeseed dev`
45
- - `treeseed save [--preview] "<commit message>"`
48
+ - `treeseed save [--preview] [--plan] "<commit message>"`
46
49
  - `treeseed stage "<resolution message>"`
47
50
  - `treeseed close "<close reason>"`
48
- - `treeseed release --major|--minor|--patch`
49
- - `treeseed destroy --environment <local|staging|prod>`
51
+ - `treeseed release --major|--minor|--patch [--plan]`
52
+ - `treeseed resume <run-id>`
53
+ - `treeseed recover`
54
+ - `treeseed destroy --environment <local|staging|prod> [--plan]`
50
55
 
51
56
  Support utilities such as `treeseed rollback`, `treeseed doctor`, `treeseed auth:*`, `treeseed template`, `treeseed sync`, `treeseed lint`, `treeseed test`, `treeseed build`, service helpers, and `treeseed agents ...` remain available.
52
57
 
@@ -57,14 +62,36 @@ Use `treeseed help` for the full command list and `treeseed help <command>` for
57
62
  ```bash
58
63
  treeseed status
59
64
  treeseed config
65
+ treeseed switch feature/search-improvements --plan
60
66
  treeseed switch feature/search-improvements --preview
61
67
  treeseed dev
62
68
  treeseed save --preview "feat: add search filters"
63
69
  treeseed stage "feat: add search filters"
64
70
  treeseed release --patch
71
+ treeseed recover
65
72
  treeseed status --json
66
73
  ```
67
74
 
75
+ ## Agent-Safe Workflow
76
+
77
+ Use planning mode before any destructive or multi-repo mutation:
78
+
79
+ ```bash
80
+ treeseed switch feature/search-improvements --plan --json
81
+ treeseed save --plan "feat: add search filters" --json
82
+ treeseed stage --plan "feat: add search filters" --json
83
+ treeseed release --patch --plan --json
84
+ ```
85
+
86
+ If a workflow stops partway through, inspect the journaled state and resume from the recorded run:
87
+
88
+ ```bash
89
+ treeseed recover
90
+ treeseed resume <run-id>
91
+ ```
92
+
93
+ In a full checked-out workspace, `treeseed tasks`, `treeseed status`, and `treeseed doctor` also report package-branch drift, dirty embedded repos, active workflow locks, and interrupted runs.
94
+
68
95
  ## Maintainer Workflow
69
96
 
70
97
  All package maintenance commands are npm-based and run from the `cli/` package root. This package verifies the published command surface, parser/help behavior, and packaged artifact shape.
@@ -3,21 +3,26 @@ import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from
3
3
  const handleClose = async (invocation, context) => {
4
4
  try {
5
5
  const result = await createWorkflowSdk(context).close({
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 deletedPackages = payload.repos.filter((repo) => repo.deletedLocal || repo.deletedRemote).length;
9
12
  return guidedResult({
10
13
  command: invocation.commandName || "close",
11
- summary: "Treeseed close completed successfully.",
14
+ summary: result.executionMode === "plan" ? "Treeseed close plan ready." : "Treeseed close completed successfully.",
12
15
  facts: [
16
+ { label: "Mode", value: payload.mode },
13
17
  { label: "Closed branch", value: payload.branchName },
14
18
  { label: "Auto-saved", value: payload.autoSaved ? "yes" : "no" },
15
- { label: "Deprecated tag", value: payload.deprecatedTag.tagName },
19
+ { label: "Deprecated tag", value: payload.rootRepo.tagName ?? payload.deprecatedTag.tagName },
20
+ { label: "Package branches cleaned", value: String(deletedPackages) },
16
21
  { label: "Preview cleanup", value: payload.previewCleanup.performed ? "performed" : "not needed" },
17
22
  { label: "Final branch", value: payload.finalBranch }
18
23
  ],
19
24
  nextSteps: renderWorkflowNextSteps(result),
20
- report: payload
25
+ report: result
21
26
  });
22
27
  } catch (error) {
23
28
  return workflowErrorResult(error);
@@ -34,6 +34,7 @@ function formatPrintEnvReports(payload) {
34
34
  function renderConfigResult(commandName, result) {
35
35
  const payload = result.payload;
36
36
  const toolHealth = payload.toolHealth;
37
+ const readinessByScope = payload.result?.readinessByScope ?? {};
37
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.";
38
39
  return guidedResult({
39
40
  command: commandName,
@@ -45,6 +46,9 @@ function renderConfigResult(commandName, result) {
45
46
  { label: "Safe repairs", value: Array.isArray(payload.repairs) ? payload.repairs.length : 0 },
46
47
  { label: "Machine config", value: payload.configPath },
47
48
  { 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" },
48
52
  { label: "GitHub CLI", value: toolHealth?.githubCli?.available ? "ready" : "missing" },
49
53
  { label: "gh act", value: toolHealth?.ghActExtension?.available ? "ready" : "missing" },
50
54
  { label: "Docker", value: toolHealth?.dockerDaemon?.available ? "ready" : "missing" },
@@ -24,6 +24,7 @@ const handleDestroy = async (invocation, context) => {
24
24
  }
25
25
  }).destroy({
26
26
  environment: String(invocation.args.environment),
27
+ plan: invocation.args.plan === true || invocation.args.dryRun === true,
27
28
  dryRun: invocation.args.dryRun === true,
28
29
  force: invocation.args.force === true,
29
30
  removeBuildArtifacts: invocation.args.removeBuildArtifacts === true
@@ -31,14 +32,14 @@ const handleDestroy = async (invocation, context) => {
31
32
  const payload = result.payload;
32
33
  return guidedResult({
33
34
  command: invocation.commandName || "destroy",
34
- summary: payload.dryRun ? "Treeseed destroy dry run completed." : "Treeseed destroy completed successfully.",
35
+ summary: result.executionMode === "plan" ? "Treeseed destroy plan ready." : "Treeseed destroy completed successfully.",
35
36
  facts: [
36
37
  { label: "Environment", value: payload.scope },
37
38
  { label: "Dry run", value: payload.dryRun ? "yes" : "no" },
38
39
  { label: "Removed build artifacts", value: payload.removeBuildArtifacts ? "yes" : "no" }
39
40
  ],
40
41
  nextSteps: renderWorkflowNextSteps(result),
41
- report: payload
42
+ report: result
42
43
  });
43
44
  } catch (error) {
44
45
  return workflowErrorResult(error);
@@ -1,54 +1,83 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
1
3
  import { collectCliPreflight } from "@treeseed/sdk/workflow-support";
2
4
  import { guidedResult } from "./utils.js";
3
- import { resolveTreeseedWorkflowState } from "../workflow-state.js";
4
5
  import { applyTreeseedSafeRepairs } from "../repair.js";
5
- const handleDoctor = (invocation, context) => {
6
- const performedFixes = invocation.args.fix === true && resolveTreeseedWorkflowState(context.cwd).deployConfigPresent ? applyTreeseedSafeRepairs(context.cwd) : [];
7
- const state = resolveTreeseedWorkflowState(context.cwd);
8
- const preflight = collectCliPreflight({ cwd: context.cwd, requireAuth: false });
9
- const railwayManagedServicesEnabled = Object.values(state.managedServices).some((service) => service.enabled);
10
- const mustFixNow = [];
11
- const optional = [];
12
- if (!state.workspaceRoot) mustFixNow.push("Run Treeseed inside a Treeseed workspace so package commands and workflow state can resolve correctly.");
13
- if (!state.repoRoot) mustFixNow.push("Initialize or clone the git repository before using save, close, stage, or release flows.");
14
- if (!state.deployConfigPresent) mustFixNow.push("Create or restore treeseed.site.yaml so the tenant contract can be loaded.");
15
- if (preflight.missingCommands.includes("git")) mustFixNow.push("Install Git.");
16
- if (preflight.missingCommands.includes("npm")) mustFixNow.push("Install npm 10 or newer.");
17
- if (!state.files.machineConfig) mustFixNow.push("Run `treeseed config --environment local` to create the local machine config.");
18
- if (!state.files.envLocal) optional.push("Create `.env.local` or run `treeseed config --environment local` to generate it.");
19
- if (!state.files.devVars) optional.push("Generate `.dev.vars` by running `treeseed config --environment local`.");
20
- if (!state.auth.gh) optional.push("Configure `GH_TOKEN` for GitHub CLI automation and Copilot-backed workflows.");
21
- if (!state.auth.wrangler) optional.push("Configure `CLOUDFLARE_API_TOKEN` before staging, preview, or production deployment work.");
22
- if (!state.auth.railway && railwayManagedServicesEnabled) {
23
- optional.push("Configure `RAILWAY_API_TOKEN` before deploying the managed Railway services.");
6
+ import { createWorkflowSdk, workflowErrorResult } from "./workflow.js";
7
+ const handleDoctor = async (invocation, context) => {
8
+ try {
9
+ const status = await createWorkflowSdk(context).status();
10
+ const state = status.payload;
11
+ const performedFixes = invocation.args.fix === true && state.deployConfigPresent ? applyTreeseedSafeRepairs(state.cwd) : [];
12
+ const preflight = collectCliPreflight({ cwd: context.cwd, requireAuth: false });
13
+ const railwayManagedServicesEnabled = Object.values(state.managedServices).some((service) => service.enabled);
14
+ const mustFixNow = [];
15
+ const optional = [];
16
+ if (!state.workspaceRoot) mustFixNow.push("Run Treeseed inside a Treeseed workspace so package commands and workflow state can resolve correctly.");
17
+ if (!state.repoRoot) mustFixNow.push("Initialize or clone the git repository before using save, close, stage, or release flows.");
18
+ if (!state.deployConfigPresent) mustFixNow.push("Create or restore treeseed.site.yaml so the tenant contract can be loaded.");
19
+ if (preflight.missingCommands.includes("git")) mustFixNow.push("Install Git.");
20
+ if (preflight.missingCommands.includes("npm")) mustFixNow.push("Install npm 10 or newer.");
21
+ if (!state.files.machineConfig) mustFixNow.push("Run `treeseed config --environment local` to create the local machine config.");
22
+ if (state.packageSync.blockers.length > 0) mustFixNow.push(...state.packageSync.blockers);
23
+ if (state.workflowControl.lock.active && state.workflowControl.lock.runId) {
24
+ mustFixNow.push(`Active workflow lock detected for run ${state.workflowControl.lock.runId}. Use \`treeseed recover\` before starting another mutating command.`);
25
+ }
26
+ if (state.workflowControl.interruptedRuns.length > 0) {
27
+ mustFixNow.push(`Interrupted workflow runs detected. Resume the latest run with \`treeseed resume ${state.workflowControl.interruptedRuns[0].runId}\` or inspect \`treeseed recover\`.`);
28
+ }
29
+ if (state.packageSync.completeCheckout) {
30
+ for (const repo of state.packageSync.repos) {
31
+ const publishWorkflowPath = resolve(state.cwd, repo.path, ".github", "workflows", "publish.yml");
32
+ if (!existsSync(publishWorkflowPath)) {
33
+ mustFixNow.push(`${repo.name} is missing .github/workflows/publish.yml required for recursive release.`);
34
+ }
35
+ }
36
+ }
37
+ for (const workflowName of ["verify.yml", "deploy.yml"]) {
38
+ const workflowPath = resolve(state.cwd, ".github", "workflows", workflowName);
39
+ if (!existsSync(workflowPath)) {
40
+ mustFixNow.push(`Missing root workflow contract .github/workflows/${workflowName}.`);
41
+ }
42
+ }
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.");
47
+ if (!state.auth.railway && railwayManagedServicesEnabled) {
48
+ optional.push("Configure `RAILWAY_API_TOKEN` before deploying the managed Railway services.");
49
+ }
50
+ if (!state.auth.remoteApi && state.managedServices.api.enabled) {
51
+ optional.push("Run `treeseed auth:login` so the CLI can use the configured remote API.");
52
+ }
53
+ if (!state.auth.copilot) optional.push("Configure `GH_TOKEN` if you rely on local Copilot-assisted workflows.");
54
+ return guidedResult({
55
+ command: "doctor",
56
+ summary: mustFixNow.length === 0 ? "Treeseed doctor found no blocking issues." : "Treeseed doctor found issues that need attention.",
57
+ facts: [
58
+ { label: "Must fix now", value: mustFixNow.length },
59
+ { label: "Optional follow-up", value: optional.length },
60
+ { label: "Safe fixes applied", value: performedFixes.length },
61
+ { label: "Branch", value: state.branchName ?? "(none)" },
62
+ { label: "Workspace root", value: state.workspaceRoot ? "yes" : "no" }
63
+ ],
64
+ nextSteps: [
65
+ ...mustFixNow.map((item) => item),
66
+ ...mustFixNow.length === 0 ? optional : optional.map((item) => `Optional: ${item}`)
67
+ ],
68
+ report: {
69
+ ...status,
70
+ state,
71
+ preflight,
72
+ performedFixes,
73
+ mustFixNow,
74
+ optional
75
+ },
76
+ exitCode: mustFixNow.length === 0 ? 0 : 1
77
+ });
78
+ } catch (error) {
79
+ return workflowErrorResult(error);
24
80
  }
25
- if (!state.auth.remoteApi && state.managedServices.api.enabled) {
26
- optional.push("Run `treeseed auth:login` so the CLI can use the configured remote API.");
27
- }
28
- if (!state.auth.copilot) optional.push("Configure `GH_TOKEN` if you rely on local Copilot-assisted workflows.");
29
- return guidedResult({
30
- command: "doctor",
31
- summary: mustFixNow.length === 0 ? "Treeseed doctor found no blocking issues." : "Treeseed doctor found issues that need attention.",
32
- facts: [
33
- { label: "Must fix now", value: mustFixNow.length },
34
- { label: "Optional follow-up", value: optional.length },
35
- { label: "Safe fixes applied", value: performedFixes.length },
36
- { label: "Branch", value: state.branchName ?? "(none)" },
37
- { label: "Workspace root", value: state.workspaceRoot ? "yes" : "no" }
38
- ],
39
- nextSteps: [
40
- ...mustFixNow.map((item) => item),
41
- ...mustFixNow.length === 0 ? optional : optional.map((item) => `Optional: ${item}`)
42
- ],
43
- report: {
44
- state,
45
- preflight,
46
- performedFixes,
47
- mustFixNow,
48
- optional
49
- },
50
- exitCode: mustFixNow.length === 0 ? 0 : 1
51
- });
52
81
  };
53
82
  export {
54
83
  handleDoctor
@@ -0,0 +1,2 @@
1
+ import type { TreeseedCommandHandler } from '../types.js';
2
+ export declare const handleRecover: TreeseedCommandHandler;
@@ -0,0 +1,25 @@
1
+ import { guidedResult } from "./utils.js";
2
+ import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
3
+ const handleRecover = async (_invocation, context) => {
4
+ try {
5
+ const result = await createWorkflowSdk(context).recover({});
6
+ const payload = result.payload;
7
+ return guidedResult({
8
+ command: "recover",
9
+ summary: payload.interruptedRuns.length > 0 || payload.lock.active ? "Treeseed recover found workflow state that may need attention." : "Treeseed recover found no active locks or interrupted runs.",
10
+ facts: [
11
+ { label: "Active lock", value: payload.lock.active ? "yes" : "no" },
12
+ { label: "Stale lock", value: payload.lock.stale ? "yes" : "no" },
13
+ { label: "Interrupted runs", value: payload.interruptedRuns.length },
14
+ { label: "Recorded runs", value: payload.runCount }
15
+ ],
16
+ nextSteps: renderWorkflowNextSteps(result),
17
+ report: result
18
+ });
19
+ } catch (error) {
20
+ return workflowErrorResult(error);
21
+ }
22
+ };
23
+ export {
24
+ handleRecover
25
+ };
@@ -3,23 +3,33 @@ import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from
3
3
  const handleRelease = async (invocation, context) => {
4
4
  try {
5
5
  const bump = ["major", "minor", "patch"].find((candidate) => invocation.args[candidate] === true) ?? "patch";
6
- const result = await createWorkflowSdk(context).release({ bump });
6
+ const result = await createWorkflowSdk(context).release({
7
+ bump,
8
+ plan: invocation.args.plan === true || invocation.args.dryRun === true,
9
+ dryRun: invocation.args.dryRun === true
10
+ });
7
11
  const payload = result.payload;
12
+ const completedPublishes = payload.publishWait.filter((entry) => entry.status === "completed").length;
8
13
  return guidedResult({
9
14
  command: invocation.commandName || "release",
10
- summary: "Treeseed release completed successfully.",
15
+ summary: result.executionMode === "plan" ? "Treeseed release plan ready." : "Treeseed release completed successfully.",
11
16
  facts: [
17
+ { label: "Mode", value: payload.mode },
12
18
  { label: "Staging branch", value: payload.stagingBranch },
13
19
  { label: "Production branch", value: payload.productionBranch },
20
+ { label: "Merge strategy", value: payload.mergeStrategy },
14
21
  { label: "Release level", value: payload.level },
15
22
  { label: "Root version", value: payload.rootVersion },
16
23
  { label: "Release tag", value: payload.releaseTag },
17
24
  { label: "Released commit", value: payload.releasedCommit.slice(0, 12) },
18
- { label: "Updated packages", value: payload.touchedPackages.length },
25
+ { label: "Changed packages", value: String(payload.packageSelection.changed.length) },
26
+ { label: "Dependent packages", value: String(payload.packageSelection.dependents.length) },
27
+ { label: "Released packages", value: String(payload.touchedPackages.length) },
28
+ { label: "Publish waits", value: String(completedPublishes) },
19
29
  { label: "Final branch", value: payload.finalBranch }
20
30
  ],
21
31
  nextSteps: renderWorkflowNextSteps(result),
22
- report: payload
32
+ report: result
23
33
  });
24
34
  } catch (error) {
25
35
  return workflowErrorResult(error);
@@ -0,0 +1,2 @@
1
+ import type { TreeseedCommandHandler } from '../types.js';
2
+ export declare const handleResume: TreeseedCommandHandler;
@@ -0,0 +1,23 @@
1
+ import { guidedResult } from "./utils.js";
2
+ import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
3
+ const handleResume = async (invocation, context) => {
4
+ try {
5
+ const runId = invocation.positionals[0] ?? "";
6
+ const result = await createWorkflowSdk(context).resume({ runId });
7
+ return guidedResult({
8
+ command: invocation.commandName || "resume",
9
+ summary: `Resumed workflow run ${runId}.`,
10
+ facts: [
11
+ { label: "Run", value: result.runId ?? runId },
12
+ { label: "Command", value: result.command }
13
+ ],
14
+ nextSteps: renderWorkflowNextSteps(result),
15
+ report: result
16
+ });
17
+ } catch (error) {
18
+ return workflowErrorResult(error);
19
+ }
20
+ };
21
+ export {
22
+ handleResume
23
+ };
@@ -5,22 +5,28 @@ const handleSave = async (invocation, context) => {
5
5
  const result = await createWorkflowSdk(context).save({
6
6
  message: invocation.positionals.join(" ").trim(),
7
7
  hotfix: invocation.args.hotfix === true,
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 savedRepos = (payload.repos ?? []).filter((repo) => repo.committed || repo.pushed).map((repo) => `${repo.name}@${String(repo.commitSha ?? "").slice(0, 12)}`).join(", ");
11
14
  return guidedResult({
12
15
  command: invocation.commandName || "save",
13
- summary: payload.noChanges ? "Treeseed save found no new changes and confirmed branch sync." : "Treeseed save completed successfully.",
16
+ summary: result.executionMode === "plan" ? "Treeseed save plan ready." : payload.noChanges ? "Treeseed save found no new changes and confirmed branch sync." : "Treeseed save completed successfully.",
14
17
  facts: [
18
+ { label: "Mode", value: payload.mode },
15
19
  { label: "Branch", value: payload.branch },
16
20
  { label: "Environment scope", value: payload.scope },
17
21
  { label: "Hotfix", value: payload.hotfix ? "yes" : "no" },
18
22
  { label: "Commit", value: payload.commitSha.slice(0, 12) },
19
23
  { label: "Commit created", value: payload.commitCreated ? "yes" : "no" },
24
+ { label: "Workspace repos", value: savedRepos || ((payload.repos ?? []).length > 0 ? "none saved" : "not applicable") },
25
+ { label: "Market pushed", value: payload.rootRepo?.pushed ? "yes" : "no" },
20
26
  { label: "Preview action", value: payload.previewAction?.status ?? "skipped" }
21
27
  ],
22
28
  nextSteps: renderWorkflowNextSteps(result),
23
- report: payload
29
+ report: result
24
30
  });
25
31
  } catch (error) {
26
32
  return workflowErrorResult(error);
@@ -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,6 +14,11 @@ 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)" },
17
22
  { label: "Local initialized", value: state.persistentEnvironments.local.initialized ? "yes" : "no" },
18
23
  { label: "Staging initialized", value: state.persistentEnvironments.staging.initialized ? "yes" : "no" },
19
24
  { label: "Prod initialized", value: state.persistentEnvironments.prod.initialized ? "yes" : "no" },
@@ -28,6 +33,7 @@ const handleStatus = async (_invocation, context) => {
28
33
  ],
29
34
  nextSteps: renderWorkflowNextSteps(result),
30
35
  report: {
36
+ ...result,
31
37
  state
32
38
  }
33
39
  });
@@ -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) {
@@ -10,15 +10,33 @@ function createWorkflowSdk(context, overrides = {}) {
10
10
  }
11
11
  function workflowErrorResult(error) {
12
12
  if (error instanceof TreeseedWorkflowError) {
13
+ const recovery = error.details?.recovery ?? null;
13
14
  return {
14
15
  exitCode: error.exitCode ?? (error.code === "merge_conflict" ? 12 : 1),
15
16
  stderr: [error.message],
16
17
  report: {
18
+ schemaVersion: 1,
19
+ kind: "treeseed.workflow.result",
20
+ command: error.operation,
21
+ executionMode: "execute",
22
+ runId: typeof recovery?.runId === "string" ? recovery.runId : null,
17
23
  ok: false,
24
+ operation: error.operation,
25
+ summary: error.message,
26
+ facts: [],
18
27
  error: error.message,
19
28
  code: error.code,
20
- operation: error.operation,
21
- details: error.details ?? null
29
+ payload: null,
30
+ result: null,
31
+ nextSteps: [],
32
+ recovery,
33
+ errors: [
34
+ {
35
+ code: error.code,
36
+ message: error.message,
37
+ details: error.details ?? null
38
+ }
39
+ ]
22
40
  }
23
41
  };
24
42
  }
@@ -27,8 +45,26 @@ function workflowErrorResult(error) {
27
45
  exitCode: 1,
28
46
  stderr: [message],
29
47
  report: {
48
+ schemaVersion: 1,
49
+ kind: "treeseed.workflow.result",
50
+ command: "status",
51
+ executionMode: "execute",
52
+ runId: null,
30
53
  ok: false,
31
- error: message
54
+ operation: "status",
55
+ summary: message,
56
+ facts: [],
57
+ error: message,
58
+ payload: null,
59
+ result: null,
60
+ nextSteps: [],
61
+ recovery: null,
62
+ errors: [
63
+ {
64
+ code: "unsupported_state",
65
+ message
66
+ }
67
+ ]
32
68
  }
33
69
  };
34
70
  }
@@ -45,6 +81,10 @@ function renderWorkflowNextStep(step) {
45
81
  return `treeseed stage "${String(input.message ?? "describe the resolution")}"`;
46
82
  case "release":
47
83
  return `treeseed release --${String(input.bump ?? "patch")}`;
84
+ case "resume":
85
+ return `treeseed resume ${String(input.runId ?? "<run-id>")}`;
86
+ case "recover":
87
+ return "treeseed recover";
48
88
  case "config": {
49
89
  const environments = Array.isArray(input.environment) ? input.environment : Array.isArray(input.target) ? input.target : null;
50
90
  return environments?.length ? `treeseed config --environment ${environments[0]}` : "treeseed config";
@@ -202,9 +202,11 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
202
202
  arguments: [{ name: "branch-name", description: "Task branch to create or resume.", required: true }],
203
203
  options: [
204
204
  { name: "preview", flags: "--preview", description: "Provision or refresh a branch-scoped Cloudflare preview environment.", kind: "boolean" },
205
+ { name: "plan", flags: "--plan", description: "Compute the recursive branch switch plan without mutating any repo.", kind: "boolean" },
206
+ { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
205
207
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
206
208
  ],
207
- examples: ["treeseed switch feature/search-improvements", "treeseed switch feature/search-improvements --preview"],
209
+ examples: ["treeseed switch feature/search-improvements", "treeseed switch feature/search-improvements --preview", "treeseed switch feature/search-improvements --plan"],
208
210
  help: {
209
211
  workflowPosition: "start work",
210
212
  longSummary: [
@@ -252,9 +254,11 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
252
254
  options: [
253
255
  { name: "hotfix", flags: "--hotfix", description: "Allow save on main for an explicit hotfix.", kind: "boolean" },
254
256
  { name: "preview", flags: "--preview", description: "Create or refresh the branch preview during save.", kind: "boolean" },
257
+ { name: "plan", flags: "--plan", description: "Compute the recursive save plan without mutating any repo.", kind: "boolean" },
258
+ { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
255
259
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
256
260
  ],
257
- examples: ['treeseed save "feat: add search filters"', 'treeseed save --preview "feat: add search filters"', 'treeseed save --hotfix "fix: unblock production form submit"'],
261
+ examples: ['treeseed save "feat: add search filters"', 'treeseed save --preview "feat: add search filters"', 'treeseed save --plan "feat: add search filters"', 'treeseed save --hotfix "fix: unblock production form submit"'],
258
262
  help: {
259
263
  workflowPosition: "checkpoint work",
260
264
  longSummary: [
@@ -294,8 +298,12 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
294
298
  })],
295
299
  ["close", command({
296
300
  arguments: [{ name: "message", description: "Reason for closing the task without staging it.", required: true, kind: "message_tail" }],
297
- options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
298
- examples: ['treeseed close "superseded by feature/search-v2"'],
301
+ options: [
302
+ { name: "plan", flags: "--plan", description: "Compute the recursive close plan without mutating any repo.", kind: "boolean" },
303
+ { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
304
+ { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
305
+ ],
306
+ examples: ['treeseed close "superseded by feature/search-v2"', 'treeseed close --plan "superseded by feature/search-v2"'],
299
307
  notes: ["Auto-saves meaningful uncommitted task-branch changes before cleanup unless disabled in the workflow API."],
300
308
  help: {
301
309
  workflowPosition: "abandon task",
@@ -328,8 +336,12 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
328
336
  })],
329
337
  ["stage", command({
330
338
  arguments: [{ name: "message", description: "Resolution message for the staged task.", required: true, kind: "message_tail" }],
331
- options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
332
- examples: ['treeseed stage "feat: add search filters"'],
339
+ options: [
340
+ { name: "plan", flags: "--plan", description: "Compute the recursive staging plan without mutating any repo.", kind: "boolean" },
341
+ { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
342
+ { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
343
+ ],
344
+ examples: ['treeseed stage "feat: add search filters"', 'treeseed stage --plan "feat: add search filters"'],
333
345
  notes: ["Auto-saves meaningful uncommitted task-branch changes before merging into staging."],
334
346
  help: {
335
347
  workflowPosition: "merge to staging",
@@ -358,6 +370,56 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
358
370
  executionMode: "handler",
359
371
  handlerName: "stage"
360
372
  })],
373
+ ["resume", command({
374
+ arguments: [{ name: "run-id", description: "Interrupted workflow run id to resume.", required: true }],
375
+ options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
376
+ examples: ["treeseed resume save-abcd12", "treeseed resume stage-ef3456 --json"],
377
+ help: {
378
+ workflowPosition: "recover",
379
+ longSummary: [
380
+ "Resume continues a previously interrupted journaled workflow run from its next incomplete step."
381
+ ],
382
+ whenToUse: [
383
+ "Use this after `treeseed recover` or a workflow failure tells you a run is resumable."
384
+ ],
385
+ beforeYouRun: [
386
+ "Confirm the run id from `treeseed recover` and repair any missing remotes, credentials, or package drift that caused the interruption."
387
+ ],
388
+ outcomes: [
389
+ "Re-enters the original workflow command using its recorded input and journal."
390
+ ],
391
+ automationNotes: [
392
+ "`resume --json` preserves the versioned workflow result envelope so agents can continue from a known run id without reparsing a different shape."
393
+ ]
394
+ },
395
+ executionMode: "handler",
396
+ handlerName: "resume"
397
+ })],
398
+ ["recover", command({
399
+ options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
400
+ examples: ["treeseed recover", "treeseed recover --json"],
401
+ help: {
402
+ workflowPosition: "recover",
403
+ longSummary: [
404
+ "Recover lists the active workflow lock plus resumable interrupted runs so humans and agents can decide whether to resume, wait, or repair manually."
405
+ ],
406
+ whenToUse: [
407
+ "Use this before starting a new mutating workflow when you suspect another run may already hold the workspace lock.",
408
+ "Use this after any interrupted recursive save, stage, close, release, or destroy command."
409
+ ],
410
+ beforeYouRun: [
411
+ "Run it from the market workspace root or anywhere inside the tenant so the CLI can inspect the correct `.treeseed/workflow` journal directory."
412
+ ],
413
+ outcomes: [
414
+ "Reports the active workflow lock, interrupted runs, and the exact `treeseed resume <run-id>` command for resumable runs."
415
+ ],
416
+ automationNotes: [
417
+ "`recover --json` is the supported discovery entrypoint for agents that need to inspect lock state and resumable run ids safely before mutating the workspace."
418
+ ]
419
+ },
420
+ executionMode: "handler",
421
+ handlerName: "recover"
422
+ })],
361
423
  ["rollback", command({
362
424
  arguments: [{ name: "environment", description: "The persistent environment to roll back.", required: true }],
363
425
  options: [
@@ -668,9 +730,11 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
668
730
  { name: "major", flags: "--major", description: "Bump to the next major version.", kind: "boolean" },
669
731
  { name: "minor", flags: "--minor", description: "Bump to the next minor version.", kind: "boolean" },
670
732
  { name: "patch", flags: "--patch", description: "Bump to the next patch version.", kind: "boolean" },
733
+ { name: "plan", flags: "--plan", description: "Compute the recursive release plan without mutating any repo.", kind: "boolean" },
734
+ { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
671
735
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
672
736
  ],
673
- examples: ["treeseed release --patch", "treeseed release --minor"],
737
+ examples: ["treeseed release --patch", "treeseed release --minor", "treeseed release --patch --plan"],
674
738
  notes: ["Requires exactly one bump flag."],
675
739
  help: {
676
740
  workflowPosition: "promote to production",
@@ -707,17 +771,18 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
707
771
  handlerName: "release"
708
772
  })],
709
773
  ["destroy", command({
710
- usage: "treeseed destroy --environment <local|staging|prod> [--dry-run] [--force] [--skip-confirmation] [--confirm <slug>] [--remove-build-artifacts]",
774
+ usage: "treeseed destroy --environment <local|staging|prod> [--plan|--dry-run] [--force] [--skip-confirmation] [--confirm <slug>] [--remove-build-artifacts]",
711
775
  options: [
712
776
  { name: "environment", flags: "--environment <scope>", description: "Select the persistent environment to destroy.", kind: "enum", values: ["local", "staging", "prod"] },
713
- { name: "dryRun", flags: "--dry-run", description: "Preview the destroy operation.", kind: "boolean" },
777
+ { name: "plan", flags: "--plan", description: "Compute the destroy plan without mutating the environment.", kind: "boolean" },
778
+ { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
714
779
  { name: "force", flags: "--force", description: "Force worker deletion when supported.", kind: "boolean" },
715
780
  { name: "skipConfirmation", flags: "--skip-confirmation", description: "Skip the interactive confirmation prompt.", kind: "boolean" },
716
781
  { name: "confirm", flags: "--confirm <slug>", description: "Provide the expected slug confirmation non-interactively.", kind: "string" },
717
782
  { name: "removeBuildArtifacts", flags: "--remove-build-artifacts", description: "Also remove local build artifacts after destroy.", kind: "boolean" },
718
783
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
719
784
  ],
720
- examples: ["treeseed destroy --environment staging --dry-run", "treeseed destroy --environment prod --confirm example --skip-confirmation"],
785
+ examples: ["treeseed destroy --environment staging --plan", "treeseed destroy --environment prod --confirm example --skip-confirmation"],
721
786
  notes: ["Only for persistent environments. Task cleanup belongs to treeseed close.", "This command is destructive and requires explicit confirmation."],
722
787
  help: {
723
788
  workflowPosition: "tear down environment",
@@ -726,7 +791,7 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
726
791
  ],
727
792
  whenToUse: [
728
793
  "Use this when a persistent environment should be intentionally removed rather than rolled back or updated.",
729
- "Use `--dry-run` first when you want to inspect the destroy plan without committing to it."
794
+ "Use `--plan` first when you want to inspect the destroy plan without committing to it."
730
795
  ],
731
796
  beforeYouRun: [
732
797
  "Confirm the environment scope exactly. This command does not target task-branch cleanup; it targets persistent environments only.",
@@ -737,7 +802,7 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
737
802
  "Optionally removes local build artifacts if requested."
738
803
  ],
739
804
  examples: [
740
- example("treeseed destroy --environment staging --dry-run", "Preview the destroy plan", "Inspect what would be removed from staging without actually performing the destroy."),
805
+ example("treeseed destroy --environment staging --plan", "Preview the destroy plan", "Inspect what would be removed from staging without actually performing the destroy."),
741
806
  example("treeseed destroy --environment prod --confirm example --skip-confirmation", "Run a deliberate non-interactive destroy", "Provide the expected slug explicitly when operating in a scripted or no-prompt environment."),
742
807
  example("treeseed destroy --environment local --remove-build-artifacts", "Remove a local environment and its artifacts", "Destroy the local environment and also delete local build outputs.")
743
808
  ],
@@ -16,6 +16,8 @@ export declare const COMMAND_HANDLERS: {
16
16
  readonly tasks: import("./operations-types.js").TreeseedCommandHandler;
17
17
  readonly switch: import("./operations-types.js").TreeseedCommandHandler;
18
18
  readonly stage: import("./operations-types.js").TreeseedCommandHandler;
19
+ readonly resume: import("./operations-types.js").TreeseedCommandHandler;
20
+ readonly recover: import("./operations-types.js").TreeseedCommandHandler;
19
21
  readonly export: import("./operations-types.js").TreeseedCommandHandler;
20
22
  readonly 'auth:login': import("./operations-types.js").TreeseedCommandHandler;
21
23
  readonly 'auth:logout': import("./operations-types.js").TreeseedCommandHandler;
@@ -22,6 +22,8 @@ import { handleTasks } from "./handlers/tasks.js";
22
22
  import { handleSwitch } from "./handlers/switch.js";
23
23
  import { handleStage } from "./handlers/stage.js";
24
24
  import { handleExport } from "./handlers/export.js";
25
+ import { handleResume } from "./handlers/resume.js";
26
+ import { handleRecover } from "./handlers/recover.js";
25
27
  const COMMAND_HANDLERS = {
26
28
  init: handleInit,
27
29
  config: handleConfig,
@@ -39,6 +41,8 @@ const COMMAND_HANDLERS = {
39
41
  tasks: handleTasks,
40
42
  switch: handleSwitch,
41
43
  stage: handleStage,
44
+ resume: handleResume,
45
+ recover: handleRecover,
42
46
  export: handleExport,
43
47
  "auth:login": handleAuthLogin,
44
48
  "auth:logout": handleAuthLogout,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/cli",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Operator-facing Treeseed CLI package.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  "release:publish": "node ./scripts/run-ts.mjs ./scripts/publish-package.ts"
44
44
  },
45
45
  "dependencies": {
46
- "@treeseed/sdk": "^0.4.9",
46
+ "@treeseed/sdk": "^0.4.13",
47
47
  "ink": "^7.0.0",
48
48
  "react": "^19.2.5"
49
49
  },
@@ -1,58 +0,0 @@
1
- export type TreeseedBranchRole = 'feature' | 'staging' | 'main' | 'detached' | 'none';
2
- export type TreeseedWorkflowRecommendation = {
3
- command: string;
4
- reason: string;
5
- };
6
- export type TreeseedWorkflowState = {
7
- cwd: string;
8
- workspaceRoot: boolean;
9
- tenantRoot: boolean;
10
- deployConfigPresent: boolean;
11
- repoRoot: string | null;
12
- branchName: string | null;
13
- branchRole: TreeseedBranchRole;
14
- environment: 'local' | 'staging' | 'prod' | 'none';
15
- dirtyWorktree: boolean;
16
- preview: {
17
- enabled: boolean;
18
- url: string | null;
19
- lastDeploymentTimestamp: string | null;
20
- };
21
- persistentEnvironments: Record<string, {
22
- initialized: boolean;
23
- lastValidatedAt: string | null;
24
- lastDeploymentTimestamp: string | null;
25
- lastDeployedUrl: string | null;
26
- }>;
27
- auth: {
28
- gh: boolean;
29
- wrangler: boolean;
30
- railway: boolean;
31
- copilot: boolean;
32
- remoteApi: boolean;
33
- };
34
- managedServices: Record<string, {
35
- enabled: boolean;
36
- initialized: boolean;
37
- lastDeploymentTimestamp: string | null;
38
- lastDeployedUrl: string | null;
39
- provider: string | null;
40
- }>;
41
- files: {
42
- treeseedConfig: boolean;
43
- machineConfig: boolean;
44
- machineKey: boolean;
45
- envLocal: boolean;
46
- devVars: boolean;
47
- };
48
- releaseReady: boolean;
49
- rollbackCandidates: Array<{
50
- scope: 'staging' | 'prod';
51
- commit: string | null;
52
- timestamp: string | null;
53
- url: string | null;
54
- }>;
55
- recommendations: TreeseedWorkflowRecommendation[];
56
- };
57
- export declare function resolveTreeseedWorkflowState(cwd: string): TreeseedWorkflowState;
58
- export declare function recommendTreeseedNextSteps(state: TreeseedWorkflowState): TreeseedWorkflowRecommendation[];
@@ -1,195 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { getTreeseedMachineConfigPaths, resolveTreeseedRemoteSession } from "@treeseed/sdk/workflow-support";
4
- import {
5
- createBranchPreviewDeployTarget,
6
- createPersistentDeployTarget,
7
- loadDeployState
8
- } from "@treeseed/sdk/workflow-support";
9
- import { PRODUCTION_BRANCH, STAGING_BRANCH } from "@treeseed/sdk/workflow-support";
10
- import { loadCliDeployConfig } from "@treeseed/sdk/workflow-support";
11
- import { collectCliPreflight } from "@treeseed/sdk/workflow-support";
12
- import { currentBranch, gitStatusPorcelain, repoRoot } from "@treeseed/sdk/workflow-support";
13
- import { isWorkspaceRoot } from "@treeseed/sdk/workflow-support";
14
- function safeResolveRepoRoot(cwd) {
15
- try {
16
- return repoRoot(cwd);
17
- } catch {
18
- return null;
19
- }
20
- }
21
- function branchRoleFor(branchName) {
22
- if (!branchName) return "none";
23
- if (branchName === STAGING_BRANCH) return "staging";
24
- if (branchName === PRODUCTION_BRANCH) return "main";
25
- return "feature";
26
- }
27
- function environmentForBranchRole(branchRole) {
28
- if (branchRole === "staging") return "staging";
29
- if (branchRole === "main") return "prod";
30
- if (branchRole === "feature") return "local";
31
- return "none";
32
- }
33
- function emptyPersistentEnvironments() {
34
- return {
35
- local: { initialized: false, lastValidatedAt: null, lastDeploymentTimestamp: null, lastDeployedUrl: null },
36
- staging: { initialized: false, lastValidatedAt: null, lastDeploymentTimestamp: null, lastDeployedUrl: null },
37
- prod: { initialized: false, lastValidatedAt: null, lastDeploymentTimestamp: null, lastDeployedUrl: null }
38
- };
39
- }
40
- function resolveTreeseedWorkflowState(cwd) {
41
- const workspaceRoot = isWorkspaceRoot(cwd);
42
- const treeseedConfigPath = resolve(cwd, "treeseed.site.yaml");
43
- const tenantRoot = existsSync(treeseedConfigPath);
44
- const root = safeResolveRepoRoot(cwd);
45
- const branchName = root ? currentBranch(root) || null : null;
46
- const branchRole = branchRoleFor(branchName);
47
- const dirtyWorktree = root ? gitStatusPorcelain(root).length > 0 : false;
48
- const preflight = collectCliPreflight({ cwd, requireAuth: false });
49
- const { configPath, keyPath } = getTreeseedMachineConfigPaths(cwd);
50
- const state = {
51
- cwd,
52
- workspaceRoot,
53
- tenantRoot,
54
- deployConfigPresent: tenantRoot,
55
- repoRoot: root,
56
- branchName,
57
- branchRole,
58
- environment: environmentForBranchRole(branchRole),
59
- dirtyWorktree,
60
- preview: {
61
- enabled: false,
62
- url: null,
63
- lastDeploymentTimestamp: null
64
- },
65
- persistentEnvironments: emptyPersistentEnvironments(),
66
- auth: {
67
- gh: preflight.checks.auth.gh?.authenticated === true,
68
- wrangler: preflight.checks.auth.wrangler?.authenticated === true,
69
- railway: preflight.checks.auth.railway?.authenticated === true,
70
- copilot: preflight.checks.auth.copilot?.configured === true,
71
- remoteApi: Boolean(resolveTreeseedRemoteSession(cwd))
72
- },
73
- managedServices: {
74
- api: { enabled: false, initialized: false, lastDeploymentTimestamp: null, lastDeployedUrl: null, provider: null },
75
- agents: { enabled: false, initialized: false, lastDeploymentTimestamp: null, lastDeployedUrl: null, provider: null },
76
- manager: { enabled: false, initialized: false, lastDeploymentTimestamp: null, lastDeployedUrl: null, provider: null },
77
- worker: { enabled: false, initialized: false, lastDeploymentTimestamp: null, lastDeployedUrl: null, provider: null },
78
- workdayStart: { enabled: false, initialized: false, lastDeploymentTimestamp: null, lastDeployedUrl: null, provider: null },
79
- workdayReport: { enabled: false, initialized: false, lastDeploymentTimestamp: null, lastDeployedUrl: null, provider: null }
80
- },
81
- files: {
82
- treeseedConfig: tenantRoot,
83
- machineConfig: existsSync(configPath),
84
- machineKey: existsSync(keyPath),
85
- envLocal: existsSync(resolve(cwd, ".env.local")),
86
- devVars: existsSync(resolve(cwd, ".dev.vars"))
87
- },
88
- releaseReady: branchRole === "staging" && !dirtyWorktree,
89
- rollbackCandidates: [],
90
- recommendations: []
91
- };
92
- if (tenantRoot) {
93
- try {
94
- const deployConfig = loadCliDeployConfig(cwd);
95
- for (const scope of ["local", "staging", "prod"]) {
96
- const deployState = loadDeployState(cwd, deployConfig, { target: createPersistentDeployTarget(scope) });
97
- state.persistentEnvironments[scope] = {
98
- initialized: deployState.readiness?.initialized === true || scope === "local",
99
- lastValidatedAt: deployState.readiness?.lastValidatedAt ?? deployState.readiness?.initializedAt ?? null,
100
- lastDeploymentTimestamp: deployState.lastDeploymentTimestamp ?? null,
101
- lastDeployedUrl: deployState.lastDeployedUrl ?? null
102
- };
103
- if (scope !== "local") {
104
- const history = Array.isArray(deployState.deploymentHistory) ? deployState.deploymentHistory ?? [] : [];
105
- const latestHistory = history.at(-1) ?? null;
106
- state.rollbackCandidates.push({
107
- scope,
108
- commit: typeof latestHistory?.commit === "string" ? latestHistory.commit : deployState.lastDeployedCommit ?? null,
109
- timestamp: typeof latestHistory?.timestamp === "string" ? latestHistory.timestamp : deployState.lastDeploymentTimestamp ?? null,
110
- url: typeof latestHistory?.url === "string" ? latestHistory.url : deployState.lastDeployedUrl ?? null
111
- });
112
- }
113
- for (const serviceKey of ["api", "agents", "manager", "worker", "workdayStart", "workdayReport"]) {
114
- const service = deployState.services?.[serviceKey];
115
- if (!service) continue;
116
- state.managedServices[serviceKey] = {
117
- enabled: service.enabled === true,
118
- initialized: service.initialized === true,
119
- lastDeploymentTimestamp: service.lastDeploymentTimestamp ?? null,
120
- lastDeployedUrl: service.lastDeployedUrl ?? service.publicBaseUrl ?? null,
121
- provider: service.provider ?? null
122
- };
123
- }
124
- }
125
- if (branchRole === "feature" && branchName) {
126
- const previewState = loadDeployState(cwd, deployConfig, { target: createBranchPreviewDeployTarget(branchName) });
127
- state.preview = {
128
- enabled: previewState.previewEnabled === true || previewState.readiness?.initialized === true,
129
- url: previewState.lastDeployedUrl ?? null,
130
- lastDeploymentTimestamp: previewState.lastDeploymentTimestamp ?? null
131
- };
132
- }
133
- } catch {
134
- }
135
- }
136
- state.recommendations = recommendTreeseedNextSteps(state);
137
- return state;
138
- }
139
- function recommendTreeseedNextSteps(state) {
140
- const recommendations = [];
141
- if (!state.workspaceRoot) {
142
- return [{ command: "treeseed status", reason: "Run this command from inside a Treeseed workspace so the CLI can resolve the project root." }];
143
- }
144
- if (!state.deployConfigPresent) {
145
- return [{ command: "treeseed init <directory>", reason: "Create a new Treeseed tenant before configuring or releasing anything." }];
146
- }
147
- if (!state.files.machineConfig) {
148
- recommendations.push({ command: "treeseed status", reason: "Validate tooling, auth, and repository readiness first." });
149
- recommendations.push({ command: "treeseed config", reason: "Bootstrap the local machine config and local environment files." });
150
- return recommendations;
151
- }
152
- if (state.branchRole === "feature") {
153
- if (state.dirtyWorktree) {
154
- recommendations.push({ command: 'treeseed save "describe your change"', reason: "Persist, verify, and push the current task branch before staging or closing it." });
155
- } else {
156
- recommendations.push({ command: 'treeseed stage "describe the resolution"', reason: "Merge this task branch into staging and clean up branch artifacts." });
157
- }
158
- if (state.preview.enabled && state.branchName) {
159
- recommendations.push({ command: 'treeseed save "describe your change"', reason: "Save refreshes the branch preview deployment when one is enabled." });
160
- } else {
161
- recommendations.push({ command: "treeseed dev", reason: "Use the local environment for iterative work on this feature branch." });
162
- }
163
- recommendations.push({ command: 'treeseed close "reason"', reason: "Archive this task without merging if it should be abandoned." });
164
- return recommendations.slice(0, 3);
165
- }
166
- if (state.branchRole === "staging") {
167
- if (!state.persistentEnvironments.staging.initialized) {
168
- recommendations.push({ command: "treeseed config --environment staging", reason: "Initialize the staging environment before releasing." });
169
- } else {
170
- recommendations.push({ command: "treeseed release --patch", reason: "Promote staging into main when the integration branch is ready for production." });
171
- if (state.managedServices.api.enabled || state.managedServices.agents.enabled) {
172
- recommendations.push({ command: "treeseed auth:login", reason: "Keep the local CLI authenticated to the remote API used by managed services." });
173
- }
174
- }
175
- return recommendations.slice(0, 3);
176
- }
177
- if (state.branchRole === "main") {
178
- if (state.dirtyWorktree) {
179
- recommendations.push({ command: 'treeseed save --hotfix "describe the hotfix"', reason: "Only explicit hotfix saves are allowed on main." });
180
- } else if (!state.persistentEnvironments.prod.initialized) {
181
- recommendations.push({ command: "treeseed config --environment prod", reason: "Initialize production before a release requires it." });
182
- } else {
183
- recommendations.push({ command: "treeseed status", reason: "Inspect production state and release readiness." });
184
- recommendations.push({ command: "treeseed rollback prod", reason: "Roll back production to the previous recorded deployment if needed." });
185
- }
186
- return recommendations.slice(0, 3);
187
- }
188
- recommendations.push({ command: "treeseed dev", reason: "Start the local Treeseed development environment." });
189
- recommendations.push({ command: "treeseed switch feature/my-change", reason: "Create a task branch from the latest staging commit." });
190
- return recommendations.slice(0, 3);
191
- }
192
- export {
193
- recommendTreeseedNextSteps,
194
- resolveTreeseedWorkflowState
195
- };