@treeseed/cli 0.6.7 → 0.6.9

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.
@@ -4,6 +4,7 @@ const handleClose = async (invocation, context) => {
4
4
  try {
5
5
  const result = await createWorkflowSdk(context).close({
6
6
  message: invocation.positionals.join(" ").trim(),
7
+ workspaceLinks: typeof invocation.args.workspaceLinks === "string" ? invocation.args.workspaceLinks : void 0,
7
8
  plan: invocation.args.plan === true || invocation.args.dryRun === true,
8
9
  dryRun: invocation.args.dryRun === true
9
10
  });
@@ -4,7 +4,9 @@ import {
4
4
  collectTreeseedConfigContext,
5
5
  ensureTreeseedActVerificationTooling,
6
6
  ensureTreeseedSecretSessionForConfig,
7
- findNearestTreeseedRoot
7
+ findNearestTreeseedRoot,
8
+ formatTreeseedDependencyFailureDetails,
9
+ installTreeseedDependencies
8
10
  } from "@treeseed/sdk/workflow-support";
9
11
  import { fail, guidedResult } from "./utils.js";
10
12
  import { buildCliConfigPages, runCliConfigEditor } from "./config-ui.js";
@@ -192,6 +194,16 @@ const handleConfig = async (invocation, context) => {
192
194
  if (!tenantRoot) {
193
195
  return fail("Treeseed config requires a Treeseed project. Run the command from inside a tenant or initialize one first.");
194
196
  }
197
+ const dependencyInstall = await installTreeseedDependencies({
198
+ tenantRoot,
199
+ force: invocation.args.installMissingTooling === true,
200
+ env: context.env,
201
+ write: context.write
202
+ });
203
+ if (!dependencyInstall.ok) {
204
+ return fail(`Treeseed dependency initialization failed:
205
+ - ${formatTreeseedDependencyFailureDetails(dependencyInstall)}`);
206
+ }
195
207
  applyTreeseedSafeRepairs(tenantRoot);
196
208
  const toolAvailability = ensureTreeseedActVerificationTooling({
197
209
  tenantRoot,
@@ -1,7 +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
+ import { ensureLocalWorkspaceLinks, findNearestTreeseedWorkspaceRoot, resolveTreeseedLaunchEnvironment } from "@treeseed/sdk/workflow-support";
5
5
  import { workflowErrorResult } from "./workflow.js";
6
6
  const require2 = createRequire(import.meta.url);
7
7
  function resolveCoreDevEntrypoint(cwd) {
@@ -43,6 +43,12 @@ function resolveCoreDevEntrypoint(cwd) {
43
43
  const handleDev = async (invocation, context) => {
44
44
  try {
45
45
  const watch = invocation.commandName === "dev:watch" || invocation.args.watch === true;
46
+ const workspaceRoot = findNearestTreeseedWorkspaceRoot(context.cwd);
47
+ const workspaceLinksMode = typeof invocation.args.workspaceLinks === "string" ? invocation.args.workspaceLinks : void 0;
48
+ const workspaceLinks = workspaceRoot ? ensureLocalWorkspaceLinks(workspaceRoot, { env: context.env, mode: workspaceLinksMode }) : null;
49
+ if (workspaceLinks?.created.length) {
50
+ context.write(`[workspace][link] Linked ${workspaceLinks.created.length} local workspace package paths.`, "stdout");
51
+ }
46
52
  const resolved = resolveCoreDevEntrypoint(context.cwd);
47
53
  const args = watch ? [...resolved.args, "--watch"] : resolved.args;
48
54
  const result = context.spawn(resolved.command, args, {
@@ -61,7 +67,8 @@ const handleDev = async (invocation, context) => {
61
67
  ok: (result.status ?? 1) === 0,
62
68
  watch,
63
69
  executable: resolved.command,
64
- args
70
+ args,
71
+ workspaceLinks
65
72
  }
66
73
  };
67
74
  } catch (error) {
@@ -1,15 +1,72 @@
1
1
  import { guidedResult } from "./utils.js";
2
2
  import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
3
+ function formatReleasePlanSections(payload) {
4
+ const sections = [];
5
+ const selection = payload.packageSelection ?? {};
6
+ const selected = selection.selected ?? [];
7
+ if (selected.length > 0 || (selection.changed ?? []).length > 0 || (selection.dependents ?? []).length > 0) {
8
+ sections.push({
9
+ title: "Package selection",
10
+ lines: [
11
+ `Changed: ${(selection.changed ?? []).join(", ") || "none"}`,
12
+ `Dependents: ${(selection.dependents ?? []).join(", ") || "none"}`,
13
+ `Selected: ${selected.join(", ") || "none"}`
14
+ ]
15
+ });
16
+ }
17
+ const versions = Object.entries(payload.plannedVersions ?? {});
18
+ if (versions.length > 0) {
19
+ sections.push({
20
+ title: "Planned versions",
21
+ lines: versions.map(([name, version]) => `- ${name}: ${version}`)
22
+ });
23
+ }
24
+ const rewrites = payload.plannedDevReferenceRewrites ?? [];
25
+ if (rewrites.length > 0) {
26
+ sections.push({
27
+ title: "Dependency rewrites",
28
+ lines: rewrites.map((rewrite) => {
29
+ const target = rewrite.dependencyName ? `${rewrite.field ?? "dependencies"}.${rewrite.dependencyName}` : rewrite.filePath ?? "lockfile";
30
+ return `- ${rewrite.repoName ?? "repo"} ${target}: ${rewrite.reason ?? "dev-ref"} ${rewrite.spec ?? ""}`.trim();
31
+ })
32
+ });
33
+ }
34
+ const waits = payload.plannedPublishWaits ?? [];
35
+ if (waits.length > 0) {
36
+ sections.push({
37
+ title: "Publish waits",
38
+ lines: waits.map((wait) => `- ${wait.name ?? "package"}: ${wait.workflow ?? "publish.yml"} on ${wait.branch ?? "main"} (${wait.status ?? "planned"})`)
39
+ });
40
+ }
41
+ const steps = payload.plannedSteps ?? [];
42
+ if (steps.length > 0) {
43
+ sections.push({
44
+ title: "Execution order",
45
+ lines: steps.map((step, index) => `${index + 1}. ${step.description ?? step.id ?? "step"}`)
46
+ });
47
+ }
48
+ if ((payload.blockers ?? []).length > 0) {
49
+ sections.push({
50
+ title: "Blockers",
51
+ lines: (payload.blockers ?? []).map((blocker) => `- ${blocker}`)
52
+ });
53
+ }
54
+ return sections;
55
+ }
3
56
  const handleRelease = async (invocation, context) => {
4
57
  try {
5
58
  const bump = ["major", "minor", "patch"].find((candidate) => invocation.args[candidate] === true) ?? "patch";
6
59
  const result = await createWorkflowSdk(context).release({
7
60
  bump,
61
+ workspaceLinks: typeof invocation.args.workspaceLinks === "string" ? invocation.args.workspaceLinks : void 0,
8
62
  plan: invocation.args.plan === true || invocation.args.dryRun === true,
9
63
  dryRun: invocation.args.dryRun === true
10
64
  });
11
65
  const payload = result.payload;
12
- const completedPublishes = payload.publishWait.filter((entry) => entry.status === "completed").length;
66
+ const publishWait = payload.publishWait ?? [];
67
+ const completedPublishes = publishWait.filter((entry) => entry.status === "completed").length;
68
+ const plannedPublishes = payload.plannedPublishWaits?.length ?? 0;
69
+ const releasedCommit = typeof payload.releasedCommit === "string" && payload.releasedCommit.length > 0 ? payload.releasedCommit.slice(0, 12) : result.executionMode === "plan" ? "planned" : "not available";
13
70
  return guidedResult({
14
71
  command: invocation.commandName || "release",
15
72
  summary: result.executionMode === "plan" ? "Treeseed release plan ready." : "Treeseed release completed successfully.",
@@ -19,15 +76,16 @@ const handleRelease = async (invocation, context) => {
19
76
  { label: "Production branch", value: payload.productionBranch },
20
77
  { label: "Merge strategy", value: payload.mergeStrategy },
21
78
  { label: "Release level", value: payload.level },
22
- { label: "Root version", value: payload.rootVersion },
23
- { label: "Release tag", value: payload.releaseTag },
24
- { label: "Released commit", value: payload.releasedCommit.slice(0, 12) },
79
+ { label: "Root version", value: payload.rootVersion ?? payload.plannedVersions?.["@treeseed/market"] ?? "(planned)" },
80
+ { label: "Release tag", value: payload.releaseTag ?? payload.rootVersion ?? payload.plannedVersions?.["@treeseed/market"] ?? "(planned)" },
81
+ { label: "Released commit", value: releasedCommit },
25
82
  { label: "Changed packages", value: String(payload.packageSelection.changed.length) },
26
83
  { 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) },
29
- { label: "Final branch", value: payload.finalBranch }
84
+ { label: result.executionMode === "plan" ? "Packages planned" : "Released packages", value: String((payload.touchedPackages ?? payload.packageSelection.selected).length) },
85
+ { label: "Publish waits", value: result.executionMode === "plan" ? String(plannedPublishes) : String(completedPublishes) },
86
+ { label: "Final branch", value: payload.finalBranch ?? (result.executionMode === "plan" ? payload.stagingBranch : "(unknown)") }
30
87
  ],
88
+ sections: result.executionMode === "plan" ? formatReleasePlanSections(payload) : [],
31
89
  nextSteps: renderWorkflowNextSteps(result),
32
90
  report: result
33
91
  });
@@ -1,16 +1,64 @@
1
1
  import { guidedResult } from "./utils.js";
2
2
  import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
3
+ function formatRepoPlanSummary(repo) {
4
+ const branch = repo.currentBranch && repo.targetBranch && repo.currentBranch !== repo.targetBranch ? `${repo.currentBranch} -> ${repo.targetBranch}` : repo.targetBranch ?? repo.currentBranch ?? "unknown";
5
+ const version = repo.plannedVersion ? `, version ${repo.currentVersion ?? "?"} -> ${repo.plannedVersion}` : repo.currentVersion ? `, version ${repo.currentVersion}` : "";
6
+ return `${repo.name} (${repo.kind ?? "repo"}, ${repo.branchMode ?? "unknown"}, ${repo.dirty ? "dirty" : "clean"}, branch ${branch}${version})`;
7
+ }
8
+ function formatSavePlanSections(repositoryPlan) {
9
+ if (!repositoryPlan) return [];
10
+ const reposByName = new Map([
11
+ ...repositoryPlan.repos ?? [],
12
+ ...repositoryPlan.rootRepo ? [repositoryPlan.rootRepo] : []
13
+ ].map((repo) => [repo.name, repo]));
14
+ const sections = [];
15
+ const repoLines = [
16
+ ...(repositoryPlan.repos ?? []).map((repo) => `- ${formatRepoPlanSummary(repo)}`),
17
+ ...repositoryPlan.rootRepo ? [`- ${formatRepoPlanSummary(repositoryPlan.rootRepo)}`] : []
18
+ ];
19
+ if (repoLines.length > 0) {
20
+ sections.push({ title: "Repositories", lines: repoLines });
21
+ }
22
+ const versionEntries = Object.entries(repositoryPlan.plannedVersions ?? {});
23
+ if (versionEntries.length > 0) {
24
+ sections.push({
25
+ title: "Planned package versions",
26
+ lines: versionEntries.map(([name, version]) => `- ${name}: ${version}`)
27
+ });
28
+ }
29
+ const waveLines = [];
30
+ for (const wave of repositoryPlan.waves ?? []) {
31
+ waveLines.push(`Wave ${wave.index}${wave.parallel ? " (parallel, concurrency 3)" : ""}: ${wave.repos.join(", ")}`);
32
+ for (const entry of wave.commands) {
33
+ const repo = reposByName.get(entry.repo);
34
+ waveLines.push(` ${entry.repo}${repo?.plannedDependencySpec ? ` -> ${repo.plannedDependencySpec}` : ""}`);
35
+ entry.commands.forEach((command, index) => {
36
+ waveLines.push(` ${index + 1}. ${command}`);
37
+ });
38
+ }
39
+ }
40
+ if (waveLines.length > 0) {
41
+ sections.push({ title: "Execution order", lines: waveLines });
42
+ }
43
+ return sections;
44
+ }
3
45
  const handleSave = async (invocation, context) => {
4
46
  try {
5
- const result = await createWorkflowSdk(context).save({
47
+ const result = await createWorkflowSdk(context, {
48
+ write: context.outputFormat === "json" ? (() => {
49
+ }) : context.write
50
+ }).save({
6
51
  message: invocation.positionals.join(" ").trim(),
7
52
  hotfix: invocation.args.hotfix === true,
8
53
  preview: invocation.args.preview === true,
54
+ workspaceLinks: typeof invocation.args.workspaceLinks === "string" ? invocation.args.workspaceLinks : void 0,
9
55
  plan: invocation.args.plan === true || invocation.args.dryRun === true,
10
56
  dryRun: invocation.args.dryRun === true
11
57
  });
12
58
  const payload = result.payload;
59
+ const commitSha = typeof payload.commitSha === "string" && payload.commitSha.length > 0 ? payload.commitSha.slice(0, 12) : "not applicable";
13
60
  const savedRepos = (payload.repos ?? []).filter((repo) => repo.committed || repo.pushed).map((repo) => `${repo.name}@${String(repo.commitSha ?? "").slice(0, 12)}`).join(", ");
61
+ const plannedRepos = result.executionMode === "plan" ? (payload.repositoryPlan?.repos ?? payload.repos ?? []).map((repo) => repo.name).join(", ") : "";
14
62
  return guidedResult({
15
63
  command: invocation.commandName || "save",
16
64
  summary: result.executionMode === "plan" ? "Treeseed save plan ready." : payload.noChanges ? "Treeseed save found no new changes and confirmed branch sync." : "Treeseed save completed successfully.",
@@ -19,12 +67,23 @@ const handleSave = async (invocation, context) => {
19
67
  { label: "Branch", value: payload.branch },
20
68
  { label: "Environment scope", value: payload.scope },
21
69
  { label: "Hotfix", value: payload.hotfix ? "yes" : "no" },
22
- { label: "Commit", value: payload.commitSha.slice(0, 12) },
70
+ {
71
+ label: result.executionMode === "plan" ? "Interrupted save" : "Resumed run",
72
+ value: result.executionMode === "plan" ? payload.autoResumeCandidate?.runId ? `will resume ${payload.autoResumeCandidate.runId}` : "none" : payload.resumedRunId ?? "none"
73
+ },
74
+ { label: "Commit", value: commitSha },
23
75
  { label: "Commit created", value: payload.commitCreated ? "yes" : "no" },
24
- { label: "Workspace repos", value: savedRepos || ((payload.repos ?? []).length > 0 ? "none saved" : "not applicable") },
76
+ {
77
+ label: result.executionMode === "plan" ? "Workspace repos planned" : "Workspace repos",
78
+ value: result.executionMode === "plan" ? plannedRepos || "not applicable" : savedRepos || ((payload.repos ?? []).length > 0 ? "none saved" : "not applicable")
79
+ },
25
80
  { label: "Market pushed", value: payload.rootRepo?.pushed ? "yes" : "no" },
26
81
  { label: "Preview action", value: payload.previewAction?.status ?? "skipped" }
27
82
  ],
83
+ sections: result.executionMode === "plan" ? [
84
+ ...payload.plannedSteps?.length ? [{ title: "Dependency mode transitions", lines: payload.plannedSteps.filter((step) => /workspace-(?:link|unlink)/u.test(String(step.id ?? ""))).map((step) => `- ${step.description ?? step.id}`) }] : [],
85
+ ...formatSavePlanSections(payload.repositoryPlan)
86
+ ] : [],
28
87
  nextSteps: renderWorkflowNextSteps(result),
29
88
  report: result
30
89
  });
@@ -4,6 +4,7 @@ const handleStage = async (invocation, context) => {
4
4
  try {
5
5
  const result = await createWorkflowSdk(context).stage({
6
6
  message: invocation.positionals.join(" ").trim(),
7
+ workspaceLinks: typeof invocation.args.workspaceLinks === "string" ? invocation.args.workspaceLinks : void 0,
7
8
  plan: invocation.args.plan === true || invocation.args.dryRun === true,
8
9
  dryRun: invocation.args.dryRun === true
9
10
  });
@@ -0,0 +1,4 @@
1
+ import type { TreeseedCommandContext } from '../types.js';
2
+ type StatusState = Record<string, any>;
3
+ export declare function renderTreeseedStatusInk(state: StatusState, context?: Pick<TreeseedCommandContext, 'outputFormat' | 'interactiveUi'>): Promise<boolean>;
4
+ export {};
@@ -0,0 +1,162 @@
1
+ import { Box, render, Text, useApp, useWindowSize } from "ink";
2
+ import React from "react";
3
+ import { truncateLine, wrapText } from "../ui/framework.js";
4
+ const SCOPES = [
5
+ { id: "local", label: "Local" },
6
+ { id: "staging", label: "Staging" },
7
+ { id: "prod", label: "Production" }
8
+ ];
9
+ function statusTone(ok, warnings = 0) {
10
+ if (!ok) return "red";
11
+ if (warnings > 0) return "yellow";
12
+ return "green";
13
+ }
14
+ function yesNo(value) {
15
+ return value ? "yes" : "no";
16
+ }
17
+ function providerText(provider) {
18
+ if (!provider) return "unknown";
19
+ if (provider.applicable === false) return provider.detail ?? "not applicable";
20
+ const base = provider.configured ? "configured" : "missing";
21
+ if (!provider.live) return base;
22
+ if (provider.live.skipped) return `${base} / ${provider.live.detail}`;
23
+ return `${base} / ${provider.live.ready ? "live ok" : "live failed"}`;
24
+ }
25
+ function providerColor(provider) {
26
+ if (provider?.applicable === false) return "gray";
27
+ if (!provider?.configured) return "red";
28
+ if (provider.live && !provider.live.ready && !provider.live.skipped) return "red";
29
+ if (provider.live?.skipped) return "yellow";
30
+ return "green";
31
+ }
32
+ function envRows(state, scope, width) {
33
+ const env = state.environmentStatus?.[scope] ?? state.persistentEnvironments?.[scope] ?? {};
34
+ const readiness = state.readiness?.[scope] ?? {};
35
+ const provider = state.providerStatus?.[scope] ?? {};
36
+ const blockers = Array.isArray(env.blockers) ? env.blockers : readiness.blockers ?? [];
37
+ const warnings = Array.isArray(env.warnings) ? env.warnings : readiness.warnings ?? [];
38
+ const providerRows = [
39
+ { label: "GitHub", value: providerText(provider.github), color: providerColor(provider.github) },
40
+ { label: "Cloudflare", value: providerText(provider.cloudflare), color: providerColor(provider.cloudflare) },
41
+ { label: "Railway", value: providerText(provider.railway), color: providerColor(provider.railway) }
42
+ ];
43
+ if (scope === "local") {
44
+ providerRows.push({ label: "Local dev", value: providerText(provider.localDevelopment), color: providerColor(provider.localDevelopment) });
45
+ }
46
+ const rows = [
47
+ { label: "Phase", value: env.phase ?? "pending", color: statusTone(Boolean(env.ready), warnings.length) },
48
+ { label: "Ready", value: yesNo(Boolean(env.ready)), color: statusTone(Boolean(env.ready), warnings.length) },
49
+ { label: "Configured", value: yesNo(Boolean(env.configured)), color: env.configured ? "green" : "yellow" },
50
+ { label: "Initialized", value: yesNo(Boolean(env.initialized)), color: env.initialized ? "green" : "yellow" },
51
+ { label: "Provisioned", value: yesNo(Boolean(env.provisioned)), color: scope === "local" || env.provisioned ? "green" : "yellow" },
52
+ { label: "Deployable", value: yesNo(Boolean(env.deployable)), color: env.deployable ? "green" : "yellow" },
53
+ ...providerRows,
54
+ { label: "Last deploy", value: env.lastDeploymentTimestamp ?? "(none)", color: env.lastDeploymentTimestamp ? "white" : "gray" },
55
+ { label: "URL", value: env.lastDeployedUrl ?? "(none)", color: env.lastDeployedUrl ? "cyan" : "gray" }
56
+ ];
57
+ const issueRows = [
58
+ ...blockers.slice(0, 4).map((value) => ({ label: "Blocker", value, color: "red" })),
59
+ ...warnings.slice(0, 3).map((value) => ({ label: "Warning", value, color: "yellow" }))
60
+ ];
61
+ return [...rows, ...issueRows].flatMap((row) => {
62
+ const prefix = `${row.label}: `;
63
+ const wrapped = wrapText(String(row.value), Math.max(1, width - prefix.length));
64
+ return wrapped.map((line, index) => ({
65
+ text: index === 0 ? `${prefix}${line}` : `${" ".repeat(prefix.length)}${line}`,
66
+ color: row.color
67
+ }));
68
+ });
69
+ }
70
+ function SummaryPanel(props) {
71
+ const state = props.state;
72
+ const packageBlockers = state.packageSync?.blockers ?? [];
73
+ const workflowBlockers = state.workflowControl?.blockers ?? [];
74
+ const rows = [
75
+ { label: "Workspace", value: state.workspaceRoot ? state.cwd : "(not a workspace)", color: state.workspaceRoot ? "green" : "red" },
76
+ { label: "Branch", value: `${state.branchName ?? "(none)"} (${state.branchRole})`, color: state.dirtyWorktree ? "yellow" : "green" },
77
+ { label: "Environment", value: state.environment, color: state.environment === "none" ? "yellow" : "cyan" },
78
+ { label: "Worktree", value: state.dirtyWorktree ? "dirty" : "clean", color: state.dirtyWorktree ? "yellow" : "green" },
79
+ { label: "Packages", value: `${state.packageSync?.mode ?? "unknown"} / ${state.packageSync?.dependencyMode ?? "unknown"}`, color: packageBlockers.length > 0 ? "red" : "green" },
80
+ { label: "Workflow", value: workflowBlockers.length > 0 ? workflowBlockers.join(" | ") : "no active blockers", color: workflowBlockers.length > 0 ? "red" : "green" },
81
+ { label: "Secrets", value: state.secrets?.keyAgentRunning ? state.secrets.keyAgentUnlocked ? "agent unlocked" : "agent locked" : "agent stopped", color: state.secrets?.keyAgentUnlocked ? "green" : "yellow" },
82
+ { label: "Market", value: state.marketConnection?.projectSlug ?? state.marketConnection?.projectId ?? "(not paired)", color: state.marketConnection?.configured ? "green" : "gray" }
83
+ ];
84
+ return React.createElement(
85
+ Box,
86
+ { flexDirection: "column", borderStyle: "round", borderColor: "cyan", width: props.width, paddingX: 1 },
87
+ React.createElement(Text, { color: "cyan", bold: true }, truncateLine("Project Status", props.width - 4)),
88
+ ...rows.map((row) => React.createElement(
89
+ Text,
90
+ { key: row.label, color: row.color },
91
+ truncateLine(`${row.label}: ${row.value}`, props.width - 4)
92
+ ))
93
+ );
94
+ }
95
+ function EnvironmentPanel(props) {
96
+ const env = props.state.environmentStatus?.[props.scope] ?? {};
97
+ const blockers = env.blockers ?? [];
98
+ const warnings = env.warnings ?? [];
99
+ const rows = envRows(props.state, props.scope, props.width - 4);
100
+ return React.createElement(
101
+ Box,
102
+ { flexDirection: "column", borderStyle: "round", borderColor: statusTone(Boolean(env.ready), warnings.length), width: props.width, paddingX: 1 },
103
+ React.createElement(Text, { color: statusTone(Boolean(env.ready), warnings.length), bold: true }, truncateLine(`${props.label} ${blockers.length ? "blocked" : warnings.length ? "warning" : "ready"}`, props.width - 4)),
104
+ ...rows.slice(0, 18).map((row, index) => React.createElement(
105
+ Text,
106
+ { key: `${props.scope}-${index}`, color: row.color ?? "white" },
107
+ truncateLine(row.text, props.width - 4)
108
+ ))
109
+ );
110
+ }
111
+ function ServicesPanel(props) {
112
+ const services = Object.entries(props.state.managedServices ?? {});
113
+ const nextSteps = Array.isArray(props.state.recommendations) ? props.state.recommendations : [];
114
+ const serviceRows = services.length > 0 ? services.map(([key, service]) => `${key}: ${service.enabled ? service.initialized ? "deployed" : "not deployed" : "disabled"}${service.lastDeployedUrl ? ` (${service.lastDeployedUrl})` : ""}`) : ["(none)"];
115
+ const nextRows = nextSteps.length > 0 ? nextSteps.map((step) => `${step.operation}: ${step.reason ?? ""}`) : ["No next steps."];
116
+ return React.createElement(
117
+ Box,
118
+ { flexDirection: "column", borderStyle: "round", borderColor: "gray", width: props.width, paddingX: 1 },
119
+ React.createElement(Text, { color: "yellow", bold: true }, truncateLine("Services and Next Steps", props.width - 4)),
120
+ ...serviceRows.slice(0, 6).map((line, index) => React.createElement(Text, { key: `svc-${index}`, color: line.includes("not deployed") ? "yellow" : line.includes("disabled") ? "gray" : "green" }, truncateLine(line, props.width - 4))),
121
+ ...nextRows.slice(0, 4).map((line, index) => React.createElement(Text, { key: `next-${index}`, color: "cyan" }, truncateLine(`Next: ${line}`, props.width - 4)))
122
+ );
123
+ }
124
+ function StatusDashboard(props) {
125
+ const { exit } = useApp();
126
+ const windowSize = useWindowSize();
127
+ const width = Math.max(72, windowSize?.columns ?? 100);
128
+ const stacked = width < 118;
129
+ const columnWidth = stacked ? width : Math.max(28, Math.floor((width - 2) / 3));
130
+ React.useEffect(() => {
131
+ const timer = setTimeout(() => exit(), 20);
132
+ return () => clearTimeout(timer);
133
+ }, [exit]);
134
+ return React.createElement(
135
+ Box,
136
+ { flexDirection: "column", width },
137
+ React.createElement(SummaryPanel, { state: props.state, width }),
138
+ React.createElement(
139
+ Box,
140
+ { flexDirection: stacked ? "column" : "row", width },
141
+ ...SCOPES.map((scope) => React.createElement(EnvironmentPanel, {
142
+ key: scope.id,
143
+ state: props.state,
144
+ scope: scope.id,
145
+ label: scope.label,
146
+ width: columnWidth
147
+ }))
148
+ ),
149
+ React.createElement(ServicesPanel, { state: props.state, width })
150
+ );
151
+ }
152
+ async function renderTreeseedStatusInk(state, context = {}) {
153
+ if (context.outputFormat === "json" || context.interactiveUi === false || !process.stdout.isTTY) {
154
+ return false;
155
+ }
156
+ const instance = render(React.createElement(StatusDashboard, { state }), { exitOnCtrlC: false });
157
+ await instance.waitUntilExit();
158
+ return true;
159
+ }
160
+ export {
161
+ renderTreeseedStatusInk
162
+ };
@@ -1,67 +1,116 @@
1
1
  import { guidedResult } from "./utils.js";
2
2
  import { createWorkflowSdk, renderWorkflowNextSteps, workflowErrorResult } from "./workflow.js";
3
- const handleStatus = async (_invocation, context) => {
3
+ import { renderTreeseedStatusInk } from "./status-ui.js";
4
+ const SCOPES = [
5
+ { id: "local", label: "Local" },
6
+ { id: "staging", label: "Staging" },
7
+ { id: "prod", label: "Production" }
8
+ ];
9
+ function yesNo(value) {
10
+ return value ? "yes" : "no";
11
+ }
12
+ function providerSummary(provider) {
13
+ if (!provider) return "unknown";
14
+ if (provider.applicable === false) return provider.detail ?? "not applicable";
15
+ const configured = provider.configured ? "configured" : "missing";
16
+ if (!provider.live) return configured;
17
+ if (provider.live.skipped) return `${configured}, skipped: ${provider.live.detail}`;
18
+ return `${configured}, ${provider.live.ready ? "live ok" : `live failed: ${provider.live.detail}`}`;
19
+ }
20
+ function environmentLines(state, scope) {
21
+ const env = state.environmentStatus?.[scope] ?? state.persistentEnvironments?.[scope] ?? {};
22
+ const providers = state.providerStatus?.[scope] ?? {};
23
+ const blockers = Array.isArray(env.blockers) ? env.blockers : [];
24
+ const warnings = Array.isArray(env.warnings) ? env.warnings : [];
25
+ const providerLines = [
26
+ `GitHub: ${providerSummary(providers.github)}`,
27
+ `Cloudflare: ${providerSummary(providers.cloudflare)}`,
28
+ `Railway: ${providerSummary(providers.railway)}`
29
+ ];
30
+ if (scope === "local") {
31
+ providerLines.push(`Local development: ${providerSummary(providers.localDevelopment)}`);
32
+ }
33
+ return [
34
+ `Phase: ${env.phase ?? "pending"}`,
35
+ `Ready: ${yesNo(Boolean(env.ready))}`,
36
+ `Configured: ${yesNo(Boolean(env.configured))}`,
37
+ `Initialized: ${yesNo(Boolean(env.initialized))}`,
38
+ `Provisioned: ${yesNo(Boolean(env.provisioned))}`,
39
+ `Deployable: ${yesNo(Boolean(env.deployable))}`,
40
+ ...providerLines,
41
+ `Last deploy: ${env.lastDeploymentTimestamp ?? "(none)"}`,
42
+ `URL: ${env.lastDeployedUrl ?? "(none)"}`,
43
+ ...blockers.length > 0 ? blockers.map((blocker) => `BLOCKER: ${blocker}`) : ["Blockers: none"],
44
+ ...warnings.length > 0 ? warnings.map((warning) => `WARNING: ${warning}`) : ["Warnings: none"]
45
+ ];
46
+ }
47
+ function statusFacts(state, live) {
48
+ return [
49
+ { label: "Mode", value: live ? "saved state + live provider checks" : "saved state" },
50
+ { label: "Workspace root", value: state.workspaceRoot ? "yes" : "no" },
51
+ { label: "Tenant config present", value: state.deployConfigPresent ? "yes" : "no" },
52
+ { label: "Branch", value: state.branchName ?? "(none)" },
53
+ { label: "Branch role", value: state.branchRole },
54
+ { label: "Mapped environment", value: state.environment },
55
+ { label: "Dirty worktree", value: state.dirtyWorktree ? "yes" : "no" },
56
+ { label: "Package mode", value: state.packageSync.mode },
57
+ { label: "Dependency mode", value: state.packageSync.dependencyMode ?? "(unknown)" },
58
+ { label: "Full package checkout", value: state.packageSync.completeCheckout ? "yes" : "no" },
59
+ { label: "Package branch aligned", value: state.packageSync.aligned ? "yes" : "no" },
60
+ { label: "Dirty package repos", value: state.packageSync.dirty ? "yes" : "no" },
61
+ { label: "Package blockers", value: state.packageSync.blockers.length > 0 ? state.packageSync.blockers.join(" | ") : "(none)" },
62
+ { label: "Preview enabled", value: state.preview.enabled ? "yes" : "no" },
63
+ { label: "Preview URL", value: state.preview.url ?? "(none)" },
64
+ { label: "Remote API auth", value: state.auth.remoteApi ? "ready" : "not ready" },
65
+ { label: "Wrapped machine key", value: state.secrets.wrappedKeyPresent ? "present" : "missing" },
66
+ { label: "Key migration", value: state.secrets.migrationRequired ? "required" : "not needed" },
67
+ { label: "Key agent", value: state.secrets.keyAgentRunning ? state.secrets.keyAgentUnlocked ? "running/unlocked" : "running/locked" : "stopped" },
68
+ { label: "Startup passphrase env", value: state.secrets.startupPassphraseConfigured ? "configured" : "unset" },
69
+ { label: "Market project", value: state.marketConnection.projectSlug ?? state.marketConnection.projectId ?? "(not paired)" },
70
+ { label: "Market team", value: state.marketConnection.teamSlug ?? state.marketConnection.teamId ?? "(not paired)" },
71
+ { label: "Market mode", value: state.marketConnection.connectionMode ?? "(not paired)" },
72
+ { label: "Hub mode", value: state.marketConnection.hubMode ?? "(unknown)" },
73
+ { label: "Runtime mode", value: state.marketConnection.runtimeMode ?? "(unknown)" },
74
+ { label: "Runtime registration", value: state.marketConnection.runtimeRegistration ?? "(none)" },
75
+ { label: "Runtime ready", value: state.marketConnection.runtimeReady ? "yes" : "no" },
76
+ { label: "Current workstream", value: state.marketConnection.currentWorkstreamId ?? "(none)" },
77
+ { label: "Approval blockers", value: state.marketConnection.approvalBlockers.length > 0 ? state.marketConnection.approvalBlockers.join(" | ") : "(none)" }
78
+ ];
79
+ }
80
+ const handleStatus = async (invocation, context) => {
4
81
  try {
5
- const result = await createWorkflowSdk(context).status();
82
+ const live = invocation.args.live === true;
83
+ const result = await createWorkflowSdk(context).status({ live });
6
84
  const state = result.payload;
85
+ const nextSteps = renderWorkflowNextSteps(result);
86
+ const report = {
87
+ ...result,
88
+ state,
89
+ live
90
+ };
91
+ if (await renderTreeseedStatusInk(state, context)) {
92
+ return {
93
+ exitCode: 0,
94
+ stdout: [],
95
+ report
96
+ };
97
+ }
7
98
  return guidedResult({
8
99
  command: "status",
9
100
  summary: "Treeseed workflow status",
10
- facts: [
11
- { label: "Workspace root", value: state.workspaceRoot ? "yes" : "no" },
12
- { label: "Tenant config present", value: state.deployConfigPresent ? "yes" : "no" },
13
- { label: "Branch", value: state.branchName ?? "(none)" },
14
- { label: "Branch role", value: state.branchRole },
15
- { label: "Mapped environment", value: state.environment },
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 },
25
- { label: "Local initialized", value: state.persistentEnvironments.local.initialized ? "yes" : "no" },
26
- { label: "Staging initialized", value: state.persistentEnvironments.staging.initialized ? "yes" : "no" },
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)" },
30
- { label: "Preview enabled", value: state.preview.enabled ? "yes" : "no" },
31
- { label: "Preview URL", value: state.preview.url ?? "(none)" },
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" },
35
- { label: "Remote API auth", value: state.auth.remoteApi ? "ready" : "not ready" },
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" }
101
+ facts: statusFacts(state, live),
102
+ sections: [
103
+ ...SCOPES.map((scope) => ({
104
+ title: scope.label,
105
+ lines: environmentLines(state, scope.id)
106
+ })),
107
+ {
108
+ title: "Managed services",
109
+ lines: Object.entries(state.managedServices ?? {}).map(([name, service]) => `${name}: ${service.enabled ? service.initialized ? "deployed" : "not deployed" : "disabled"}${service.lastDeployedUrl ? ` (${service.lastDeployedUrl})` : ""}`)
110
+ }
59
111
  ],
60
- nextSteps: renderWorkflowNextSteps(result),
61
- report: {
62
- ...result,
63
- state
64
- }
112
+ nextSteps,
113
+ report
65
114
  });
66
115
  } catch (error) {
67
116
  return workflowErrorResult(error);
@@ -6,6 +6,7 @@ const handleSwitch = async (invocation, context) => {
6
6
  const result = await createWorkflowSdk(context).switchTask({
7
7
  branch,
8
8
  preview: invocation.args.preview === true,
9
+ workspaceLinks: typeof invocation.args.workspaceLinks === "string" ? invocation.args.workspaceLinks : void 0,
9
10
  plan: invocation.args.plan === true || invocation.args.dryRun === true,
10
11
  dryRun: invocation.args.dryRun === true
11
12
  });
@@ -8,6 +8,10 @@ type GuidedResultOptions = {
8
8
  label: string;
9
9
  value: string | number | boolean | null | undefined;
10
10
  }>;
11
+ sections?: Array<{
12
+ title: string;
13
+ lines: string[];
14
+ }>;
11
15
  nextSteps?: string[];
12
16
  report?: Record<string, unknown> | null;
13
17
  exitCode?: number;
@@ -13,6 +13,11 @@ function guidedResult(options) {
13
13
  lines.push(`${fact.label}: ${fact.value}`);
14
14
  }
15
15
  }
16
+ for (const section of options.sections ?? []) {
17
+ const sectionLines = section.lines.filter((line) => line.length > 0);
18
+ if (sectionLines.length === 0) continue;
19
+ lines.push("", `${section.title}:`, ...sectionLines);
20
+ }
16
21
  if ((options.nextSteps ?? []).length > 0) {
17
22
  lines.push("", "Next steps:");
18
23
  for (const step of options.nextSteps ?? []) {
@@ -25,12 +30,14 @@ function guidedResult(options) {
25
30
  ok: (options.exitCode ?? 0) === 0,
26
31
  summary: options.summary,
27
32
  facts: facts.map((fact) => ({ label: fact.label, value: fact.value })),
33
+ sections: options.sections ?? [],
28
34
  nextSteps: options.nextSteps ?? []
29
35
  } : {
30
36
  command: options.command,
31
37
  ok: (options.exitCode ?? 0) === 0,
32
38
  summary: options.summary,
33
39
  facts: facts.map((fact) => ({ label: fact.label, value: fact.value })),
40
+ sections: options.sections ?? [],
34
41
  nextSteps: options.nextSteps ?? []
35
42
  };
36
43
  return {
@@ -104,8 +104,10 @@ function renderWorkflowNextStep(step) {
104
104
  switch (step.operation) {
105
105
  case "switch":
106
106
  return `treeseed switch ${String(input.branch ?? "feature/my-change")}${input.preview ? " --preview" : ""}`;
107
- case "save":
108
- return `treeseed save "${String(input.message ?? "describe your change")}"${input.hotfix ? " --hotfix" : ""}`;
107
+ case "save": {
108
+ const message = String(input.message ?? "").trim();
109
+ return `treeseed save${message ? ` "${message}"` : ""}${input.hotfix ? " --hotfix" : ""}`;
110
+ }
109
111
  case "close":
110
112
  return `treeseed close "${String(input.message ?? "reason")}"`;
111
113
  case "stage":
@@ -0,0 +1,2 @@
1
+ import type { TreeseedCommandHandler } from '../types.js';
2
+ export declare const handleWorkspace: TreeseedCommandHandler;
@@ -0,0 +1,51 @@
1
+ import {
2
+ ensureLocalWorkspaceLinks,
3
+ findNearestTreeseedWorkspaceRoot,
4
+ inspectWorkspaceDependencyMode,
5
+ unlinkLocalWorkspaceLinks
6
+ } from "@treeseed/sdk/workflow-support";
7
+ import { guidedResult } from "./utils.js";
8
+ import { workflowErrorResult } from "./workflow.js";
9
+ const workspaceCommand = (name) => `workspace${":"}${name}`;
10
+ function workspaceRootOrThrow(cwd) {
11
+ const root = findNearestTreeseedWorkspaceRoot(cwd);
12
+ if (!root) {
13
+ throw new Error("Treeseed workspace commands must run inside a workspace with checked-out packages.");
14
+ }
15
+ return root;
16
+ }
17
+ function facts(report) {
18
+ return [
19
+ { label: "Dependency mode", value: report.mode },
20
+ { label: "Workspace links enabled", value: report.enabled ? "yes" : "no" },
21
+ { label: "Links", value: String(report.links.length) },
22
+ { label: "Linked", value: String(report.links.filter((link) => link.linked && link.targetMatches).length) },
23
+ { label: "Created", value: String(report.created.length) },
24
+ { label: "Removed", value: String(report.removed.length) },
25
+ { label: "Issues", value: report.issues.length > 0 ? report.issues.join(" | ") : "(none)" }
26
+ ];
27
+ }
28
+ const handleWorkspace = async (invocation, context) => {
29
+ try {
30
+ const root = workspaceRootOrThrow(context.cwd);
31
+ const linkCommand = workspaceCommand("link");
32
+ const unlinkCommand = workspaceCommand("unlink");
33
+ const statusCommand = workspaceCommand("status");
34
+ const report = invocation.commandName === linkCommand ? ensureLocalWorkspaceLinks(root, { env: context.env }) : invocation.commandName === unlinkCommand ? unlinkLocalWorkspaceLinks(root, { env: context.env }) : inspectWorkspaceDependencyMode(root, { env: context.env });
35
+ return guidedResult({
36
+ command: invocation.commandName || statusCommand,
37
+ summary: invocation.commandName === linkCommand ? "Treeseed local workspace links are ready." : invocation.commandName === unlinkCommand ? "Treeseed local workspace links were removed." : "Treeseed workspace dependency mode",
38
+ facts: facts(report),
39
+ report: {
40
+ ok: true,
41
+ command: invocation.commandName || statusCommand,
42
+ payload: report
43
+ }
44
+ });
45
+ } catch (error) {
46
+ return workflowErrorResult(error);
47
+ }
48
+ };
49
+ export {
50
+ handleWorkspace
51
+ };
@@ -5,6 +5,7 @@ import {
5
5
  function command(overlay) {
6
6
  return overlay;
7
7
  }
8
+ const workspaceCommand = (name) => `workspace${":"}${name}`;
8
9
  function example(commandLine, title, description, extras = {}) {
9
10
  return {
10
11
  command: commandLine,
@@ -133,8 +134,11 @@ function mergeHelpSpec(metadata, overlay, spec) {
133
134
  const PASS_THROUGH_ARGS = (invocation) => ({ args: invocation.rawArgs });
134
135
  const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
135
136
  ["status", command({
136
- options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
137
- examples: ["treeseed status", "treeseed status --json"],
137
+ options: [
138
+ { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" },
139
+ { name: "live", flags: "--live", description: "Run read-only provider connectivity checks and include the results in status.", kind: "boolean" }
140
+ ],
141
+ examples: ["treeseed status", "treeseed status --json", "treeseed status --live"],
138
142
  help: {
139
143
  workflowPosition: "inspect",
140
144
  longSummary: [
@@ -147,6 +151,7 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
147
151
  ],
148
152
  beforeYouRun: [
149
153
  "Run from the workspace you want to inspect.",
154
+ "Use `--live` only when you want read-only provider connectivity checks in addition to saved state.",
150
155
  "Choose `--json` when another tool or agent needs to read the status programmatically."
151
156
  ],
152
157
  outcomes: [
@@ -156,6 +161,7 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
156
161
  examples: [
157
162
  example("treeseed status", "Check the current task state", "Show the current branch role and project health in human-readable form."),
158
163
  example("treeseed status --json", "Feed an agent or script", "Emit structured status data for automation and external tooling."),
164
+ example("treeseed status --live", "Check provider connectivity", "Include read-only GitHub, Cloudflare, and Railway identity checks in the status report."),
159
165
  example("trsd status", "Use the short alias", "Run the same status inspection path through the shorter CLI entrypoint.")
160
166
  ],
161
167
  automationNotes: [
@@ -202,6 +208,7 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
202
208
  arguments: [{ name: "branch-name", description: "Task branch to create or resume.", required: true }],
203
209
  options: [
204
210
  { name: "preview", flags: "--preview", description: "Provision or refresh a branch-scoped Cloudflare preview environment.", kind: "boolean" },
211
+ { name: "workspaceLinks", flags: "--workspace-links <mode>", description: "Control local workspace package links.", kind: "enum", values: ["auto", "off"] },
205
212
  { name: "plan", flags: "--plan", description: "Compute the recursive branch switch plan without mutating any repo.", kind: "boolean" },
206
213
  { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
207
214
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
@@ -250,15 +257,16 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
250
257
  handlerName: "switch"
251
258
  })],
252
259
  ["save", command({
253
- arguments: [{ name: "message", description: "Git commit message for the save operation.", required: true, kind: "message_tail" }],
260
+ arguments: [{ name: "message", description: "Optional hint for generated save commit messages.", required: false, kind: "message_tail" }],
254
261
  options: [
255
262
  { name: "hotfix", flags: "--hotfix", description: "Allow save on main for an explicit hotfix.", kind: "boolean" },
256
263
  { name: "preview", flags: "--preview", description: "Create or refresh the branch preview during save.", kind: "boolean" },
264
+ { name: "workspaceLinks", flags: "--workspace-links <mode>", description: "Control local workspace package links.", kind: "enum", values: ["auto", "off"] },
257
265
  { name: "plan", flags: "--plan", description: "Compute the recursive save plan without mutating any repo.", kind: "boolean" },
258
266
  { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
259
267
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
260
268
  ],
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"'],
269
+ examples: ["treeseed save", 'treeseed save "add search filters"', "treeseed save --preview", "treeseed save --plan", 'treeseed save --hotfix "fix production form submit"'],
262
270
  help: {
263
271
  workflowPosition: "checkpoint work",
264
272
  longSummary: [
@@ -272,17 +280,18 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
272
280
  ],
273
281
  beforeYouRun: [
274
282
  "Run from a task branch unless you intentionally mean to use the hotfix path.",
275
- "Provide a commit message that captures the checkpoint clearly because Treeseed will use it for the save commit."
283
+ "Optionally provide a short hint; Treeseed generates the final commit message from the diff and hint."
276
284
  ],
277
285
  outcomes: [
278
- "Verifies and commits current work using the provided message.",
286
+ "Verifies and commits current work using a generated commit message.",
279
287
  "Syncs and pushes branch state.",
280
288
  "Optionally refreshes preview infrastructure if requested."
281
289
  ],
282
290
  examples: [
283
- example('treeseed save "feat: add search filters"', "Standard task checkpoint", "Verify, commit, and push the current task branch with a descriptive message."),
284
- example('treeseed save --preview "feat: add search filters"', "Checkpoint plus preview refresh", "Include preview refresh when the save should update the branch environment."),
285
- example('treeseed save --hotfix "fix: unblock production form submit"', "Explicit hotfix save", "Allow a save from main when the work is a deliberate hotfix path.", { why: "Use sparingly and only when the workflow intentionally bypasses the usual task-branch rule." })
291
+ example("treeseed save", "Generated checkpoint", "Verify, commit, and push the current task branch with a generated message."),
292
+ example('treeseed save "add search filters"', "Checkpoint with a hint", "Feed a short hint into commit-message generation without replacing the generated message."),
293
+ example("treeseed save --preview", "Checkpoint plus preview refresh", "Include preview refresh when the save should update the branch environment."),
294
+ example('treeseed save --hotfix "fix production form submit"', "Explicit hotfix save", "Allow a save from main when the work is a deliberate hotfix path.", { why: "Use sparingly and only when the workflow intentionally bypasses the usual task-branch rule." })
286
295
  ],
287
296
  warnings: [
288
297
  "`--hotfix` deliberately loosens the normal task-branch safety model. Keep it exceptional."
@@ -300,6 +309,7 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
300
309
  arguments: [{ name: "message", description: "Reason for closing the task without staging it.", required: true, kind: "message_tail" }],
301
310
  options: [
302
311
  { name: "plan", flags: "--plan", description: "Compute the recursive close plan without mutating any repo.", kind: "boolean" },
312
+ { name: "workspaceLinks", flags: "--workspace-links <mode>", description: "Control local workspace package links.", kind: "enum", values: ["auto", "off"] },
303
313
  { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
304
314
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
305
315
  ],
@@ -338,6 +348,7 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
338
348
  arguments: [{ name: "message", description: "Resolution message for the staged task.", required: true, kind: "message_tail" }],
339
349
  options: [
340
350
  { name: "plan", flags: "--plan", description: "Compute the recursive staging plan without mutating any repo.", kind: "boolean" },
351
+ { name: "workspaceLinks", flags: "--workspace-links <mode>", description: "Control local workspace package links.", kind: "enum", values: ["auto", "off"] },
341
352
  { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
342
353
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
343
354
  ],
@@ -420,6 +431,24 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
420
431
  executionMode: "handler",
421
432
  handlerName: "recover"
422
433
  })],
434
+ [workspaceCommand("status"), command({
435
+ options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
436
+ examples: ["treeseed workspace:status"],
437
+ executionMode: "handler",
438
+ handlerName: workspaceCommand("status")
439
+ })],
440
+ [workspaceCommand("link"), command({
441
+ options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
442
+ examples: ["treeseed workspace:link"],
443
+ executionMode: "handler",
444
+ handlerName: workspaceCommand("link")
445
+ })],
446
+ [workspaceCommand("unlink"), command({
447
+ options: [{ name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }],
448
+ examples: ["treeseed workspace:unlink"],
449
+ executionMode: "handler",
450
+ handlerName: workspaceCommand("unlink")
451
+ })],
423
452
  ["rollback", command({
424
453
  arguments: [{ name: "environment", description: "The persistent environment to roll back.", required: true }],
425
454
  options: [
@@ -492,6 +521,40 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
492
521
  executionMode: "handler",
493
522
  handlerName: "doctor"
494
523
  })],
524
+ ["install", command({
525
+ options: [
526
+ { name: "force", flags: "--force", description: "Repair or reinstall managed tools even when they are already present.", kind: "boolean" },
527
+ { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
528
+ ],
529
+ examples: ["treeseed install", "trsd install --json", "treeseed install --force"],
530
+ help: {
531
+ workflowPosition: "setup",
532
+ longSummary: [
533
+ "Install prepares the local Treeseed toolchain by installing or verifying Treeseed-managed dependencies. It is safe to rerun and uses the same dependency initializer that config runs during bootstrap."
534
+ ],
535
+ whenToUse: [
536
+ "Use this on a new machine before running config, dev, or deployment workflows.",
537
+ "Use `--force` when a managed tool cache looks stale or corrupted."
538
+ ],
539
+ outcomes: [
540
+ "Installs the managed GitHub CLI, verifies npm-backed Treeseed tool dependencies, and installs gh-act when Docker is available.",
541
+ "Reports any missing host prerequisites such as Git without modifying the operating system."
542
+ ],
543
+ examples: [
544
+ example("treeseed install", "Install managed dependencies", "Prepare the local Treeseed dependency toolchain."),
545
+ example("trsd install --json", "Inspect setup from automation", "Emit a structured dependency report for scripts or agents."),
546
+ example("treeseed install --force", "Repair the managed cache", "Reinstall managed downloaded tools and extensions.")
547
+ ],
548
+ relatedDetails: [
549
+ related("config", "Use `config` after install to configure project and provider values."),
550
+ related("doctor", "Use `doctor` when install succeeds but workflow readiness still looks wrong.")
551
+ ]
552
+ },
553
+ executionMode: "adapter",
554
+ buildAdapterInput: (invocation) => ({
555
+ force: invocation.args.force === true
556
+ })
557
+ })],
495
558
  ["auth:login", command({
496
559
  options: [
497
560
  { name: "host", flags: "--host <id>", description: "Override the configured remote host id for this login.", kind: "string" },
@@ -842,6 +905,7 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
842
905
  { name: "major", flags: "--major", description: "Bump to the next major version.", kind: "boolean" },
843
906
  { name: "minor", flags: "--minor", description: "Bump to the next minor version.", kind: "boolean" },
844
907
  { name: "patch", flags: "--patch", description: "Bump to the next patch version.", kind: "boolean" },
908
+ { name: "workspaceLinks", flags: "--workspace-links <mode>", description: "Control local workspace package links.", kind: "enum", values: ["auto", "off"] },
845
909
  { name: "plan", flags: "--plan", description: "Compute the recursive release plan without mutating any repo.", kind: "boolean" },
846
910
  { name: "dryRun", flags: "--dry-run", description: "Alias for --plan.", kind: "boolean" },
847
911
  { name: "json", flags: "--json", description: "Emit machine-readable JSON instead of human-readable text.", kind: "boolean" }
@@ -931,6 +995,9 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
931
995
  handlerName: "destroy"
932
996
  })],
933
997
  ["dev", command({
998
+ options: [
999
+ { name: "workspaceLinks", flags: "--workspace-links <mode>", description: "Control local workspace package links.", kind: "enum", values: ["auto", "off"] }
1000
+ ],
934
1001
  examples: ["treeseed dev"],
935
1002
  help: {
936
1003
  longSummary: ["Dev starts the unified local Treeseed runtime so you can work against the integrated web, API, and supporting local surfaces."],
@@ -944,6 +1011,9 @@ const CLI_COMMAND_OVERLAYS = /* @__PURE__ */ new Map([
944
1011
  handlerName: "dev"
945
1012
  })],
946
1013
  ["dev:watch", command({
1014
+ options: [
1015
+ { name: "workspaceLinks", flags: "--workspace-links <mode>", description: "Control local workspace package links.", kind: "enum", values: ["auto", "off"] }
1016
+ ],
947
1017
  examples: ["treeseed dev:watch"],
948
1018
  help: {
949
1019
  longSummary: ["Dev:watch starts local development with rebuild and watch semantics so code changes are reflected continuously during active development."],
@@ -1,5 +1,6 @@
1
1
  import type { TreeseedCommandSpec } from './operations-types.js';
2
2
  export declare const COMMAND_HANDLERS: {
3
+ readonly [x: string]: import("./operations-types.js").TreeseedCommandHandler;
3
4
  readonly init: import("./operations-types.js").TreeseedCommandHandler;
4
5
  readonly config: import("./operations-types.js").TreeseedCommandHandler;
5
6
  readonly close: import("./operations-types.js").TreeseedCommandHandler;
@@ -32,6 +32,8 @@ import { handleStage } from "./handlers/stage.js";
32
32
  import { handleExport } from "./handlers/export.js";
33
33
  import { handleResume } from "./handlers/resume.js";
34
34
  import { handleRecover } from "./handlers/recover.js";
35
+ import { handleWorkspace } from "./handlers/workspace.js";
36
+ const workspaceCommand = (name) => `workspace${":"}${name}`;
35
37
  const COMMAND_HANDLERS = {
36
38
  init: handleInit,
37
39
  config: handleConfig,
@@ -51,6 +53,9 @@ const COMMAND_HANDLERS = {
51
53
  stage: handleStage,
52
54
  resume: handleResume,
53
55
  recover: handleRecover,
56
+ [workspaceCommand("status")]: handleWorkspace,
57
+ [workspaceCommand("link")]: handleWorkspace,
58
+ [workspaceCommand("unlink")]: handleWorkspace,
54
59
  export: handleExport,
55
60
  "auth:login": handleAuthLogin,
56
61
  "auth:logout": handleAuthLogout,
@@ -54,8 +54,13 @@ function colorizeTreeseedCliOutput(output, colorEnabled = true) {
54
54
  if (!colorEnabled) {
55
55
  return output;
56
56
  }
57
- return output.replace(/^((?:\[[^\]]+\]){3,4})(\s|$)/u, (match, prefix, suffix) => {
57
+ return output.replace(/^((?:\[[^\]]+\]){2,4})(\s|$)/u, (match, prefix, suffix) => {
58
58
  const segments = [...prefix.matchAll(/\[([^\]]+)\]/gu)].map((entry) => entry[1]);
59
+ if (segments.length === 2) {
60
+ const stage2 = segments[1] ?? "";
61
+ const code2 = /fail|error/iu.test(stage2) ? "31;1" : /skip/iu.test(stage2) ? "90;1" : "32;1";
62
+ return `\x1B[${code2}m${prefix}\x1B[0m${suffix}`;
63
+ }
59
64
  const system = segments[1] ?? "";
60
65
  const stage = segments[segments.length - 1] ?? "";
61
66
  const code = /fail|error/iu.test(stage) ? "31;1" : /skip/iu.test(stage) ? "90;1" : colorCodeForBootstrapSystem(system);
@@ -226,20 +231,24 @@ class TreeseedOperationsSdk {
226
231
  rawArgs: argv
227
232
  };
228
233
  const input = spec.buildAdapterInput?.(invocation, context) ?? {};
234
+ const adapterContext = {
235
+ ...context,
236
+ outputFormat: invocation.args.json === true ? "json" : context.outputFormat ?? "human"
237
+ };
229
238
  return sdkOperationsRuntime.execute(
230
239
  { operationName: spec.name, input },
231
240
  {
232
- cwd: context.cwd,
233
- env: context.env,
234
- write: context.write,
235
- spawn: context.spawn,
236
- outputFormat: context.outputFormat,
241
+ cwd: adapterContext.cwd,
242
+ env: adapterContext.env,
243
+ write: adapterContext.write,
244
+ spawn: adapterContext.spawn,
245
+ outputFormat: adapterContext.outputFormat,
237
246
  transport: "cli"
238
247
  }
239
- ).then((result) => writeTreeseedResult(result, context)).catch((error) => writeTreeseedResult({
248
+ ).then((result) => writeTreeseedResult(result, adapterContext)).catch((error) => writeTreeseedResult({
240
249
  exitCode: 1,
241
250
  stderr: [error instanceof Error ? error.message : String(error)]
242
- }, context));
251
+ }, adapterContext));
243
252
  }
244
253
  async executeAgents(argv, context) {
245
254
  try {
@@ -321,7 +330,7 @@ function formatProjectError(spec) {
321
330
  ].join("\n");
322
331
  }
323
332
  function commandNeedsProjectRoot(spec) {
324
- return spec.name !== "init" && spec.name !== "export";
333
+ return spec.name !== "init" && spec.name !== "export" && spec.name !== "install";
325
334
  }
326
335
  function resolveTreeseedCommandCwd(spec, cwd) {
327
336
  if (!commandNeedsProjectRoot(spec)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treeseed/cli",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Operator-facing Treeseed CLI package.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -33,6 +33,7 @@
33
33
  "lint": "npm run build:dist",
34
34
  "test": "npm run build:dist && node --test --test-concurrency=1 ./scripts/treeseed-help.test.mjs ./scripts/wrapper-package.test.mjs",
35
35
  "build:dist": "node ./scripts/run-ts.mjs ./scripts/build-dist.ts",
36
+ "prepare": "node ./scripts/prepare.mjs",
36
37
  "prepack": "npm run build:dist",
37
38
  "verify:direct": "npm run release:verify",
38
39
  "verify:local": "node --input-type=module -e \"process.env.TREESEED_VERIFY_DRIVER='direct'; await import('./scripts/verify-driver.mjs')\"",
@@ -44,7 +45,7 @@
44
45
  "release:publish": "node ./scripts/run-ts.mjs ./scripts/publish-package.ts"
45
46
  },
46
47
  "dependencies": {
47
- "@treeseed/sdk": "^0.6.7",
48
+ "@treeseed/sdk": "0.6.9",
48
49
  "ink": "^7.0.0",
49
50
  "react": "^19.2.5"
50
51
  },