@treeseed/cli 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +115 -0
  2. package/dist/cli/handlers/close.js +60 -0
  3. package/dist/cli/handlers/config.js +76 -0
  4. package/dist/cli/handlers/continue.js +23 -0
  5. package/dist/cli/handlers/deploy.js +139 -0
  6. package/dist/cli/handlers/destroy.js +94 -0
  7. package/dist/cli/handlers/doctor.js +48 -0
  8. package/dist/cli/handlers/init.js +30 -0
  9. package/dist/cli/handlers/next.js +27 -0
  10. package/dist/cli/handlers/prepare.js +8 -0
  11. package/dist/cli/handlers/promote.js +8 -0
  12. package/dist/cli/handlers/publish.js +8 -0
  13. package/dist/cli/handlers/release.js +60 -0
  14. package/dist/cli/handlers/rollback.js +163 -0
  15. package/dist/cli/handlers/save.js +87 -0
  16. package/dist/cli/handlers/setup.js +48 -0
  17. package/dist/cli/handlers/ship.js +49 -0
  18. package/dist/cli/handlers/start.js +97 -0
  19. package/dist/cli/handlers/status.js +31 -0
  20. package/dist/cli/handlers/teardown.js +50 -0
  21. package/dist/cli/handlers/utils.js +59 -0
  22. package/dist/cli/handlers/work.js +85 -0
  23. package/dist/cli/help.js +143 -0
  24. package/dist/cli/main.js +27 -0
  25. package/dist/cli/parser.js +96 -0
  26. package/dist/cli/registry.js +445 -0
  27. package/dist/cli/repair.js +89 -0
  28. package/dist/cli/runtime.js +165 -0
  29. package/dist/cli/types.js +0 -0
  30. package/dist/cli/workflow-state.js +171 -0
  31. package/dist/index.js +1 -0
  32. package/dist/scripts/aggregate-book.d.ts +1 -0
  33. package/dist/scripts/aggregate-book.js +121 -0
  34. package/dist/scripts/assert-release-tag-version.d.ts +1 -0
  35. package/dist/scripts/assert-release-tag-version.js +21 -0
  36. package/dist/scripts/build-dist.d.ts +1 -0
  37. package/dist/scripts/build-dist.js +108 -0
  38. package/dist/scripts/build-tenant-worker.d.ts +1 -0
  39. package/dist/scripts/build-tenant-worker.js +36 -0
  40. package/dist/scripts/cleanup-markdown.d.ts +2 -0
  41. package/dist/scripts/cleanup-markdown.js +373 -0
  42. package/dist/scripts/config-runtime-lib.d.ts +122 -0
  43. package/dist/scripts/config-runtime-lib.js +505 -0
  44. package/dist/scripts/config-treeseed.d.ts +2 -0
  45. package/dist/scripts/config-treeseed.js +81 -0
  46. package/dist/scripts/d1-migration-lib.d.ts +6 -0
  47. package/dist/scripts/d1-migration-lib.js +90 -0
  48. package/dist/scripts/deploy-lib.d.ts +127 -0
  49. package/dist/scripts/deploy-lib.js +841 -0
  50. package/dist/scripts/ensure-mailpit.d.ts +1 -0
  51. package/dist/scripts/ensure-mailpit.js +29 -0
  52. package/dist/scripts/git-workflow-lib.d.ts +25 -0
  53. package/dist/scripts/git-workflow-lib.js +136 -0
  54. package/dist/scripts/github-automation-lib.d.ts +156 -0
  55. package/dist/scripts/github-automation-lib.js +242 -0
  56. package/dist/scripts/local-dev-lib.d.ts +9 -0
  57. package/dist/scripts/local-dev-lib.js +84 -0
  58. package/dist/scripts/local-dev.d.ts +1 -0
  59. package/dist/scripts/local-dev.js +129 -0
  60. package/dist/scripts/logs-mailpit.d.ts +1 -0
  61. package/dist/scripts/logs-mailpit.js +2 -0
  62. package/dist/scripts/mailpit-runtime.d.ts +4 -0
  63. package/dist/scripts/mailpit-runtime.js +57 -0
  64. package/dist/scripts/package-tools.d.ts +22 -0
  65. package/dist/scripts/package-tools.js +255 -0
  66. package/dist/scripts/patch-starlight-content-path.d.ts +1 -0
  67. package/dist/scripts/patch-starlight-content-path.js +172 -0
  68. package/dist/scripts/paths.d.ts +17 -0
  69. package/dist/scripts/paths.js +26 -0
  70. package/dist/scripts/publish-package.d.ts +1 -0
  71. package/dist/scripts/publish-package.js +19 -0
  72. package/dist/scripts/release-verify.d.ts +1 -0
  73. package/dist/scripts/release-verify.js +136 -0
  74. package/dist/scripts/run-fixture-astro-command.d.ts +1 -0
  75. package/dist/scripts/run-fixture-astro-command.js +18 -0
  76. package/dist/scripts/save-deploy-preflight-lib.d.ts +34 -0
  77. package/dist/scripts/save-deploy-preflight-lib.js +69 -0
  78. package/dist/scripts/scaffold-site.d.ts +2 -0
  79. package/dist/scripts/scaffold-site.js +92 -0
  80. package/dist/scripts/stop-mailpit.d.ts +1 -0
  81. package/dist/scripts/stop-mailpit.js +5 -0
  82. package/dist/scripts/sync-dev-vars.d.ts +1 -0
  83. package/dist/scripts/sync-dev-vars.js +6 -0
  84. package/dist/scripts/template-registry-lib.d.ts +47 -0
  85. package/dist/scripts/template-registry-lib.js +137 -0
  86. package/dist/scripts/tenant-astro-command.d.ts +1 -0
  87. package/dist/scripts/tenant-astro-command.js +3 -0
  88. package/dist/scripts/tenant-build.d.ts +1 -0
  89. package/dist/scripts/tenant-build.js +16 -0
  90. package/dist/scripts/tenant-check.d.ts +1 -0
  91. package/dist/scripts/tenant-check.js +7 -0
  92. package/dist/scripts/tenant-d1-migrate-local.d.ts +1 -0
  93. package/dist/scripts/tenant-d1-migrate-local.js +11 -0
  94. package/dist/scripts/tenant-deploy.d.ts +2 -0
  95. package/dist/scripts/tenant-deploy.js +180 -0
  96. package/dist/scripts/tenant-destroy.d.ts +2 -0
  97. package/dist/scripts/tenant-destroy.js +104 -0
  98. package/dist/scripts/tenant-dev.d.ts +1 -0
  99. package/dist/scripts/tenant-dev.js +171 -0
  100. package/dist/scripts/tenant-lint.d.ts +1 -0
  101. package/dist/scripts/tenant-lint.js +4 -0
  102. package/dist/scripts/tenant-test.d.ts +1 -0
  103. package/dist/scripts/tenant-test.js +4 -0
  104. package/dist/scripts/test-cloudflare-local.d.ts +1 -0
  105. package/dist/scripts/test-cloudflare-local.js +212 -0
  106. package/dist/scripts/test-scaffold.d.ts +2 -0
  107. package/dist/scripts/test-scaffold.js +297 -0
  108. package/dist/scripts/treeseed.d.ts +2 -0
  109. package/dist/scripts/treeseed.js +4 -0
  110. package/dist/scripts/validate-templates.d.ts +2 -0
  111. package/dist/scripts/validate-templates.js +4 -0
  112. package/dist/scripts/watch-dev-lib.d.ts +21 -0
  113. package/dist/scripts/watch-dev-lib.js +277 -0
  114. package/dist/scripts/workspace-close.d.ts +2 -0
  115. package/dist/scripts/workspace-close.js +24 -0
  116. package/dist/scripts/workspace-command-e2e.d.ts +2 -0
  117. package/dist/scripts/workspace-command-e2e.js +718 -0
  118. package/dist/scripts/workspace-lint.d.ts +1 -0
  119. package/dist/scripts/workspace-lint.js +9 -0
  120. package/dist/scripts/workspace-preflight-lib.d.ts +36 -0
  121. package/dist/scripts/workspace-preflight-lib.js +179 -0
  122. package/dist/scripts/workspace-preflight.d.ts +2 -0
  123. package/dist/scripts/workspace-preflight.js +22 -0
  124. package/dist/scripts/workspace-publish-changed-packages.d.ts +1 -0
  125. package/dist/scripts/workspace-publish-changed-packages.js +16 -0
  126. package/dist/scripts/workspace-release-verify.d.ts +1 -0
  127. package/dist/scripts/workspace-release-verify.js +81 -0
  128. package/dist/scripts/workspace-release.d.ts +2 -0
  129. package/dist/scripts/workspace-release.js +42 -0
  130. package/dist/scripts/workspace-save-lib.d.ts +42 -0
  131. package/dist/scripts/workspace-save-lib.js +220 -0
  132. package/dist/scripts/workspace-save.d.ts +2 -0
  133. package/dist/scripts/workspace-save.js +124 -0
  134. package/dist/scripts/workspace-start-warning.d.ts +0 -0
  135. package/dist/scripts/workspace-start-warning.js +3 -0
  136. package/dist/scripts/workspace-start.d.ts +2 -0
  137. package/dist/scripts/workspace-start.js +71 -0
  138. package/dist/scripts/workspace-test-unit.d.ts +1 -0
  139. package/dist/scripts/workspace-test-unit.js +4 -0
  140. package/dist/scripts/workspace-test.d.ts +1 -0
  141. package/dist/scripts/workspace-test.js +11 -0
  142. package/dist/scripts/workspace-tools.d.ts +13 -0
  143. package/dist/scripts/workspace-tools.js +226 -0
  144. package/dist/src/cli/handlers/close.d.ts +2 -0
  145. package/dist/src/cli/handlers/config.d.ts +2 -0
  146. package/dist/src/cli/handlers/continue.d.ts +2 -0
  147. package/dist/src/cli/handlers/deploy.d.ts +2 -0
  148. package/dist/src/cli/handlers/destroy.d.ts +2 -0
  149. package/dist/src/cli/handlers/doctor.d.ts +2 -0
  150. package/dist/src/cli/handlers/init.d.ts +2 -0
  151. package/dist/src/cli/handlers/next.d.ts +2 -0
  152. package/dist/src/cli/handlers/prepare.d.ts +2 -0
  153. package/dist/src/cli/handlers/promote.d.ts +2 -0
  154. package/dist/src/cli/handlers/publish.d.ts +2 -0
  155. package/dist/src/cli/handlers/release.d.ts +2 -0
  156. package/dist/src/cli/handlers/rollback.d.ts +2 -0
  157. package/dist/src/cli/handlers/save.d.ts +2 -0
  158. package/dist/src/cli/handlers/setup.d.ts +2 -0
  159. package/dist/src/cli/handlers/ship.d.ts +2 -0
  160. package/dist/src/cli/handlers/start.d.ts +3 -0
  161. package/dist/src/cli/handlers/status.d.ts +2 -0
  162. package/dist/src/cli/handlers/teardown.d.ts +2 -0
  163. package/dist/src/cli/handlers/utils.d.ts +18 -0
  164. package/dist/src/cli/handlers/work.d.ts +2 -0
  165. package/dist/src/cli/help.d.ts +4 -0
  166. package/dist/src/cli/main.d.ts +6 -0
  167. package/dist/src/cli/parser.d.ts +3 -0
  168. package/dist/src/cli/registry.d.ts +27 -0
  169. package/dist/src/cli/repair.d.ts +6 -0
  170. package/dist/src/cli/runtime.d.ts +4 -0
  171. package/dist/src/cli/types.d.ts +71 -0
  172. package/dist/src/cli/workflow-state.d.ts +49 -0
  173. package/dist/src/index.d.ts +1 -0
  174. package/package.json +57 -0
@@ -0,0 +1,60 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { applyTreeseedEnvironmentToProcess } from "../../scripts/config-runtime-lib.js";
4
+ import { PRODUCTION_BRANCH, STAGING_BRANCH, mergeStagingIntoMain, prepareReleaseBranches, pushBranch } from "../../scripts/git-workflow-lib.js";
5
+ import { applyWorkspaceVersionChanges, incrementVersion, planWorkspaceReleaseBump, repoRoot } from "../../scripts/workspace-save-lib.js";
6
+ import { run, workspaceRoot } from "../../scripts/workspace-tools.js";
7
+ import { runWorkspaceSavePreflight } from "../../scripts/save-deploy-preflight-lib.js";
8
+ import { guidedResult } from "./utils.js";
9
+ function bumpRootPackageJson(root, level) {
10
+ const packageJsonPath = resolve(root, "package.json");
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
12
+ packageJson.version = incrementVersion(packageJson.version, level);
13
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
14
+ `, "utf8");
15
+ return packageJson.version;
16
+ }
17
+ const handleRelease = (invocation, context) => {
18
+ const commandName = invocation.commandName || "release";
19
+ const level = ["major", "minor", "patch"].find((candidate) => invocation.args[candidate] === true);
20
+ const root = workspaceRoot();
21
+ const gitRoot = repoRoot(root);
22
+ prepareReleaseBranches(root);
23
+ applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging" });
24
+ runWorkspaceSavePreflight({ cwd: root });
25
+ const plan = planWorkspaceReleaseBump(level, root);
26
+ applyWorkspaceVersionChanges(plan);
27
+ const rootVersion = bumpRootPackageJson(root, level);
28
+ run("git", ["checkout", STAGING_BRANCH], { cwd: gitRoot });
29
+ run("git", ["add", "-A"], { cwd: gitRoot });
30
+ run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
31
+ pushBranch(gitRoot, STAGING_BRANCH);
32
+ mergeStagingIntoMain(root);
33
+ return {
34
+ ...guidedResult({
35
+ command: commandName,
36
+ summary: `Treeseed ${commandName} completed successfully.`,
37
+ facts: [
38
+ { label: "Staging branch", value: STAGING_BRANCH },
39
+ { label: "Production branch", value: PRODUCTION_BRANCH },
40
+ { label: "Release level", value: level ?? "(unknown)" },
41
+ { label: "Root version", value: rootVersion },
42
+ { label: "Updated packages", value: plan.touched.size }
43
+ ],
44
+ nextSteps: [
45
+ "Monitor CI for the production deployment triggered from main.",
46
+ "treeseed status"
47
+ ],
48
+ report: {
49
+ level,
50
+ rootVersion,
51
+ stagingBranch: STAGING_BRANCH,
52
+ productionBranch: PRODUCTION_BRANCH,
53
+ touchedPackages: [...plan.touched]
54
+ }
55
+ })
56
+ };
57
+ };
58
+ export {
59
+ handleRelease
60
+ };
@@ -0,0 +1,163 @@
1
+ import { existsSync, mkdtempSync, symlinkSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join, relative, resolve } from "node:path";
4
+ import { applyTreeseedEnvironmentToProcess } from "../../scripts/config-runtime-lib.js";
5
+ import {
6
+ createPersistentDeployTarget,
7
+ deployTargetLabel,
8
+ ensureGeneratedWranglerConfig,
9
+ finalizeDeploymentState,
10
+ loadDeployState
11
+ } from "../../scripts/deploy-lib.js";
12
+ import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin } from "../../scripts/package-tools.js";
13
+ import { run } from "../../scripts/workspace-tools.js";
14
+ import { repoRoot } from "../../scripts/workspace-save-lib.js";
15
+ import { guidedResult } from "./utils.js";
16
+ import { copyTreeseedOperationalState } from "../repair.js";
17
+ function selectRollbackCommit(state, requested) {
18
+ const history = Array.isArray(state.deploymentHistory) ? state.deploymentHistory : [];
19
+ if (requested) return requested;
20
+ const latestCommit = typeof state.lastDeployedCommit === "string" ? state.lastDeployedCommit : null;
21
+ const previous = [...history].reverse().find((entry) => typeof entry.commit === "string" && entry.commit !== latestCommit);
22
+ return typeof previous?.commit === "string" ? previous.commit : latestCommit;
23
+ }
24
+ function selectRollbackEntry(state, requested) {
25
+ const history = Array.isArray(state.deploymentHistory) ? state.deploymentHistory : [];
26
+ if (requested) {
27
+ return history.find((entry) => entry.commit === requested) ?? null;
28
+ }
29
+ const latestCommit = typeof state.lastDeployedCommit === "string" ? state.lastDeployedCommit : null;
30
+ return [...history].reverse().find((entry) => typeof entry.commit === "string" && entry.commit !== latestCommit) ?? null;
31
+ }
32
+ function isRollbackCompatible(state, candidate) {
33
+ if (!candidate) {
34
+ return { ok: true, reason: null };
35
+ }
36
+ const current = state.runtimeCompatibility;
37
+ const currentGeneration = typeof current?.envelopeSchemaGeneration === "string" ? current.envelopeSchemaGeneration : null;
38
+ const currentWave = typeof current?.migrationWaveId === "string" ? current.migrationWaveId : null;
39
+ const candidateGeneration = typeof candidate.envelopeSchemaGeneration === "string" ? candidate.envelopeSchemaGeneration : null;
40
+ const candidateWave = typeof candidate.migrationWaveId === "string" ? candidate.migrationWaveId : null;
41
+ if (currentGeneration && candidateGeneration && currentGeneration !== candidateGeneration) {
42
+ return { ok: false, reason: `Rollback target uses envelope generation ${candidateGeneration} but the current environment is on ${currentGeneration}.` };
43
+ }
44
+ if (currentWave && candidateWave && currentWave !== candidateWave) {
45
+ return { ok: false, reason: `Rollback target was deployed before migration wave ${currentWave} and cannot be safely reused without a forward-compatible data bridge.` };
46
+ }
47
+ return { ok: true, reason: null };
48
+ }
49
+ function runGitWorktree(repoDir, args) {
50
+ return run("git", ["worktree", ...args], { cwd: repoDir, capture: true });
51
+ }
52
+ const handleRollback = (invocation, context) => {
53
+ const scope = invocation.positionals[0];
54
+ if (scope !== "staging" && scope !== "prod") {
55
+ return guidedResult({
56
+ command: "rollback",
57
+ summary: "Treeseed rollback requires an explicit persistent target.",
58
+ facts: [{ label: "Provided target", value: scope ?? "(none)" }],
59
+ nextSteps: ["treeseed rollback staging", "treeseed rollback prod"],
60
+ report: { scope: scope ?? null },
61
+ exitCode: 1
62
+ });
63
+ }
64
+ const requestedCommit = typeof invocation.args.to === "string" ? invocation.args.to : null;
65
+ const tenantRoot = context.cwd;
66
+ const deployConfig = loadCliDeployConfig(tenantRoot);
67
+ const target = createPersistentDeployTarget(scope);
68
+ const state = loadDeployState(tenantRoot, deployConfig, { target });
69
+ const rollbackEntry = selectRollbackEntry(state, requestedCommit);
70
+ const rollbackCommit = selectRollbackCommit(state, requestedCommit);
71
+ if (!rollbackCommit) {
72
+ return guidedResult({
73
+ command: "rollback",
74
+ summary: `No rollback candidate is recorded for ${scope}.`,
75
+ facts: [{ label: "Target", value: scope }],
76
+ nextSteps: ["treeseed status"],
77
+ report: { scope, rollbackCommit: null, deploymentHistory: state.deploymentHistory ?? [] },
78
+ exitCode: 1
79
+ });
80
+ }
81
+ const compatibility = isRollbackCompatible(state, rollbackEntry);
82
+ if (!compatibility.ok) {
83
+ return guidedResult({
84
+ command: "rollback",
85
+ summary: `Treeseed rollback refused for ${scope}.`,
86
+ facts: [
87
+ { label: "Target", value: scope },
88
+ { label: "Requested commit", value: rollbackCommit },
89
+ { label: "Reason", value: compatibility.reason ?? "Unknown compatibility boundary" }
90
+ ],
91
+ nextSteps: ["treeseed status", `treeseed publish --environment ${scope}`],
92
+ report: { scope, rollbackCommit, compatibility, deploymentHistory: state.deploymentHistory ?? [] },
93
+ exitCode: 1
94
+ });
95
+ }
96
+ const gitRoot = repoRoot(tenantRoot);
97
+ const tenantRelativePath = relative(gitRoot, tenantRoot);
98
+ const tempRoot = mkdtempSync(join(tmpdir(), "treeseed-rollback-"));
99
+ const tempTenantRoot = resolve(tempRoot, tenantRelativePath);
100
+ const currentNodeModules = resolve(tenantRoot, "node_modules");
101
+ let finalizedState = null;
102
+ try {
103
+ runGitWorktree(gitRoot, ["add", "--detach", tempRoot, rollbackCommit]);
104
+ copyTreeseedOperationalState(tenantRoot, tempTenantRoot);
105
+ if (existsSync(currentNodeModules) && !existsSync(resolve(tempTenantRoot, "node_modules"))) {
106
+ symlinkSync(currentNodeModules, resolve(tempTenantRoot, "node_modules"), "dir");
107
+ }
108
+ applyTreeseedEnvironmentToProcess({ tenantRoot, scope });
109
+ const { wranglerPath } = ensureGeneratedWranglerConfig(tempTenantRoot, { target });
110
+ const buildResult = context.spawn(process.execPath, [packageScriptPath("tenant-build")], {
111
+ cwd: tempTenantRoot,
112
+ env: { ...context.env },
113
+ stdio: "inherit"
114
+ });
115
+ if ((buildResult.status ?? 1) !== 0) {
116
+ return { exitCode: buildResult.status ?? 1 };
117
+ }
118
+ const publishResult = context.spawn(process.execPath, [resolveWranglerBin(), "deploy", "--config", wranglerPath], {
119
+ cwd: tempTenantRoot,
120
+ env: { ...context.env },
121
+ stdio: "inherit"
122
+ });
123
+ if ((publishResult.status ?? 1) !== 0) {
124
+ return { exitCode: publishResult.status ?? 1 };
125
+ }
126
+ const previousCommit = process.env.TREESEED_DEPLOY_COMMIT;
127
+ process.env.TREESEED_DEPLOY_COMMIT = rollbackCommit;
128
+ try {
129
+ finalizedState = finalizeDeploymentState(tenantRoot, { target });
130
+ } finally {
131
+ if (previousCommit) {
132
+ process.env.TREESEED_DEPLOY_COMMIT = previousCommit;
133
+ } else {
134
+ delete process.env.TREESEED_DEPLOY_COMMIT;
135
+ }
136
+ }
137
+ } finally {
138
+ try {
139
+ runGitWorktree(gitRoot, ["remove", "--force", tempRoot]);
140
+ } catch {
141
+ }
142
+ }
143
+ return guidedResult({
144
+ command: "rollback",
145
+ summary: `Treeseed rollback completed for ${scope}.`,
146
+ facts: [
147
+ { label: "Target", value: deployTargetLabel(target) },
148
+ { label: "Rolled back commit", value: rollbackCommit },
149
+ { label: "Deployed URL", value: finalizedState?.lastDeployedUrl }
150
+ ],
151
+ nextSteps: ["treeseed status", `treeseed publish --environment ${scope}`],
152
+ report: {
153
+ scope,
154
+ rollbackCommit,
155
+ rollbackEntry,
156
+ target: deployTargetLabel(target),
157
+ finalizedState
158
+ }
159
+ });
160
+ };
161
+ export {
162
+ handleRollback
163
+ };
@@ -0,0 +1,87 @@
1
+ import { applyTreeseedEnvironmentToProcess } from "../../scripts/config-runtime-lib.js";
2
+ import {
3
+ collectMergeConflictReport,
4
+ currentBranch,
5
+ formatMergeConflictReport,
6
+ hasMeaningfulChanges,
7
+ originRemoteUrl,
8
+ repoRoot
9
+ } from "../../scripts/workspace-save-lib.js";
10
+ import { PRODUCTION_BRANCH, STAGING_BRANCH, remoteBranchExists } from "../../scripts/git-workflow-lib.js";
11
+ import { run, workspaceRoot } from "../../scripts/workspace-tools.js";
12
+ import { runWorkspaceSavePreflight } from "../../scripts/save-deploy-preflight-lib.js";
13
+ import { guidedResult } from "./utils.js";
14
+ const handleSave = (invocation, context) => {
15
+ const commandName = invocation.commandName || "save";
16
+ const optionsHotfix = invocation.args.hotfix === true;
17
+ const message = invocation.positionals.join(" ").trim();
18
+ const root = workspaceRoot();
19
+ const gitRoot = repoRoot(root);
20
+ const branch = currentBranch(gitRoot);
21
+ const scope = branch === STAGING_BRANCH ? "staging" : branch === PRODUCTION_BRANCH ? "prod" : "local";
22
+ applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope });
23
+ if (!message) {
24
+ return { exitCode: 1, stderr: [`Treeseed ${commandName} requires a commit message. Usage: treeseed ${commandName} <message>`] };
25
+ }
26
+ if (!branch) {
27
+ return { exitCode: 1, stderr: ["Treeseed save requires an active git branch."] };
28
+ }
29
+ if (branch === PRODUCTION_BRANCH && !optionsHotfix) {
30
+ return {
31
+ exitCode: 1,
32
+ stderr: [`Treeseed ${commandName} is blocked on main. Use \`treeseed promote\` for normal production promotion or \`treeseed ${commandName} --hotfix\` for an explicit hotfix.`]
33
+ };
34
+ }
35
+ try {
36
+ originRemoteUrl(gitRoot);
37
+ } catch {
38
+ return { exitCode: 1, stderr: [`Treeseed ${commandName} requires an origin remote.`] };
39
+ }
40
+ try {
41
+ runWorkspaceSavePreflight({ cwd: root });
42
+ } catch (error) {
43
+ return { exitCode: error?.exitCode ?? 1, stderr: [error instanceof Error ? error.message : String(error)] };
44
+ }
45
+ if (!hasMeaningfulChanges(gitRoot)) {
46
+ return { exitCode: 1, stderr: [`Treeseed ${commandName} found no meaningful repository changes to commit.`] };
47
+ }
48
+ run("git", ["add", "-A"], { cwd: gitRoot });
49
+ run("git", ["commit", "-m", message], { cwd: gitRoot });
50
+ try {
51
+ if (remoteBranchExists(gitRoot, branch)) {
52
+ run("git", ["pull", "--rebase", "origin", branch], { cwd: gitRoot });
53
+ run("git", ["push", "origin", branch], { cwd: gitRoot });
54
+ } else {
55
+ run("git", ["push", "-u", "origin", branch], { cwd: gitRoot });
56
+ }
57
+ } catch {
58
+ const report = collectMergeConflictReport(gitRoot);
59
+ return {
60
+ exitCode: 12,
61
+ stderr: [formatMergeConflictReport(report, gitRoot, branch)]
62
+ };
63
+ }
64
+ return {
65
+ ...guidedResult({
66
+ command: commandName,
67
+ summary: `Treeseed ${commandName} completed successfully.`,
68
+ facts: [
69
+ { label: "Branch", value: branch },
70
+ { label: "Environment scope", value: scope },
71
+ { label: "Hotfix", value: optionsHotfix ? "yes" : "no" }
72
+ ],
73
+ nextSteps: [
74
+ branch === STAGING_BRANCH ? "treeseed deploy --environment staging" : branch === PRODUCTION_BRANCH ? "Monitor CI or run `treeseed deploy --environment prod` only if you intentionally need a manual production publish." : "treeseed close"
75
+ ],
76
+ report: {
77
+ branch,
78
+ scope,
79
+ hotfix: optionsHotfix,
80
+ message
81
+ }
82
+ })
83
+ };
84
+ };
85
+ export {
86
+ handleSave
87
+ };
@@ -0,0 +1,48 @@
1
+ import { handleConfig } from "./config.js";
2
+ import { handleDoctor } from "./doctor.js";
3
+ import { guidedResult } from "./utils.js";
4
+ import { applyTreeseedSafeRepairs } from "../repair.js";
5
+ import { resolveTreeseedWorkflowState } from "../workflow-state.js";
6
+ const handleSetup = async (invocation, context) => {
7
+ const tenantRoot = context.cwd;
8
+ const scopes = Array.isArray(invocation.args.environment) ? invocation.args.environment : invocation.args.environment ? [invocation.args.environment] : ["local"];
9
+ const repairs = resolveTreeseedWorkflowState(tenantRoot).deployConfigPresent ? applyTreeseedSafeRepairs(tenantRoot) : [];
10
+ const configResult = await handleConfig({
11
+ ...invocation,
12
+ commandName: "config",
13
+ args: {
14
+ ...invocation.args,
15
+ environment: scopes,
16
+ sync: invocation.args.sync ?? "none"
17
+ },
18
+ positionals: [],
19
+ rawArgs: [
20
+ ...scopes.flatMap((scope) => ["--environment", scope]),
21
+ ...invocation.args.json === true ? ["--json"] : []
22
+ ]
23
+ }, context);
24
+ if ((configResult.exitCode ?? 0) !== 0) {
25
+ return configResult;
26
+ }
27
+ const doctorResult = handleDoctor({ ...invocation, args: { ...invocation.args } }, context);
28
+ const doctorPayload = doctorResult.report ?? {};
29
+ return guidedResult({
30
+ command: "setup",
31
+ summary: "Treeseed setup completed.",
32
+ facts: [
33
+ { label: "Initialized environments", value: scopes.join(", ") },
34
+ { label: "Safe repairs", value: repairs.length },
35
+ { label: "Blocking issues remaining", value: Array.isArray(doctorPayload.mustFixNow) ? doctorPayload.mustFixNow?.length ?? 0 : 0 }
36
+ ],
37
+ nextSteps: ["treeseed dev", "treeseed work feature/my-change"],
38
+ report: {
39
+ repairs,
40
+ scopes,
41
+ config: configResult.report,
42
+ doctor: doctorPayload
43
+ }
44
+ });
45
+ };
46
+ export {
47
+ handleSetup
48
+ };
@@ -0,0 +1,49 @@
1
+ import { guidedResult } from "./utils.js";
2
+ import { handleSave } from "./save.js";
3
+ import { handleDeploy } from "./deploy.js";
4
+ import { resolveTreeseedWorkflowState } from "../workflow-state.js";
5
+ const handleShip = async (invocation, context) => {
6
+ const beforeState = resolveTreeseedWorkflowState(context.cwd);
7
+ const saveResult = await handleSave({
8
+ ...invocation,
9
+ commandName: "save"
10
+ }, context);
11
+ if ((saveResult.exitCode ?? 0) !== 0) {
12
+ return saveResult;
13
+ }
14
+ let previewRefresh = null;
15
+ if (beforeState.branchRole === "feature" && beforeState.preview.enabled && beforeState.branchName) {
16
+ const publishResult = await handleDeploy({
17
+ commandName: "deploy",
18
+ args: {
19
+ targetBranch: beforeState.branchName,
20
+ json: invocation.args.json
21
+ },
22
+ positionals: [],
23
+ rawArgs: ["--target-branch", beforeState.branchName, ...invocation.args.json === true ? ["--json"] : []]
24
+ }, context);
25
+ if ((publishResult.exitCode ?? 0) !== 0) {
26
+ return publishResult;
27
+ }
28
+ previewRefresh = publishResult.report ?? null;
29
+ }
30
+ const afterState = resolveTreeseedWorkflowState(context.cwd);
31
+ return guidedResult({
32
+ command: "ship",
33
+ summary: "Treeseed ship completed successfully.",
34
+ facts: [
35
+ { label: "Branch", value: afterState.branchName ?? beforeState.branchName ?? "(none)" },
36
+ { label: "Branch role", value: afterState.branchRole },
37
+ { label: "Preview refreshed", value: previewRefresh ? "yes" : "no" }
38
+ ],
39
+ nextSteps: afterState.recommendations.map((item) => `${item.command} # ${item.reason}`),
40
+ report: {
41
+ state: afterState,
42
+ save: saveResult.report,
43
+ previewRefresh
44
+ }
45
+ });
46
+ };
47
+ export {
48
+ handleShip
49
+ };
@@ -0,0 +1,97 @@
1
+ import {
2
+ applyTreeseedEnvironmentToProcess,
3
+ assertTreeseedCommandEnvironment
4
+ } from "../../scripts/config-runtime-lib.js";
5
+ import {
6
+ createBranchPreviewDeployTarget,
7
+ deployTargetLabel,
8
+ ensureGeneratedWranglerConfig,
9
+ finalizeDeploymentState,
10
+ printDeploySummary,
11
+ provisionCloudflareResources,
12
+ runRemoteD1Migrations,
13
+ syncCloudflareSecrets,
14
+ validateDeployPrerequisites
15
+ } from "../../scripts/deploy-lib.js";
16
+ import { createFeatureBranchFromStaging, pushBranch } from "../../scripts/git-workflow-lib.js";
17
+ import { packageScriptPath, resolveWranglerBin } from "../../scripts/package-tools.js";
18
+ import { guidedResult } from "./utils.js";
19
+ function provisionBranchPreview(branchName, context, commandName = "start") {
20
+ const tenantRoot = context.cwd;
21
+ applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "staging" });
22
+ assertTreeseedCommandEnvironment({ tenantRoot, scope: "staging", purpose: "deploy" });
23
+ validateDeployPrerequisites(tenantRoot, { requireRemote: true });
24
+ const target = createBranchPreviewDeployTarget(branchName);
25
+ const summary = provisionCloudflareResources(tenantRoot, { target });
26
+ printDeploySummary(summary);
27
+ const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot, { target });
28
+ syncCloudflareSecrets(tenantRoot, { target });
29
+ runRemoteD1Migrations(tenantRoot, { target });
30
+ const buildResult = context.spawn(process.execPath, [packageScriptPath("tenant-build")], {
31
+ cwd: tenantRoot,
32
+ env: { ...context.env },
33
+ stdio: "inherit"
34
+ });
35
+ if ((buildResult.status ?? 1) !== 0) {
36
+ return { exitCode: buildResult.status ?? 1 };
37
+ }
38
+ const deployResult = context.spawn(process.execPath, [resolveWranglerBin(), "deploy", "--config", wranglerPath], {
39
+ cwd: tenantRoot,
40
+ env: { ...context.env },
41
+ stdio: "inherit"
42
+ });
43
+ if ((deployResult.status ?? 1) !== 0) {
44
+ return { exitCode: deployResult.status ?? 1 };
45
+ }
46
+ const state = finalizeDeploymentState(tenantRoot, { target });
47
+ return guidedResult({
48
+ command: commandName,
49
+ summary: `Treeseed ${commandName} preview completed for ${branchName}.`,
50
+ facts: [
51
+ { label: "Target", value: deployTargetLabel(target) },
52
+ { label: "Preview URL", value: state.lastDeployedUrl }
53
+ ],
54
+ nextSteps: [
55
+ 'treeseed save "describe your change"',
56
+ `treeseed deploy --target-branch ${branchName}`
57
+ ],
58
+ report: {
59
+ branchName,
60
+ preview: true,
61
+ target: deployTargetLabel(target),
62
+ previewUrl: state.lastDeployedUrl
63
+ }
64
+ });
65
+ }
66
+ const handleStart = (invocation, context) => {
67
+ const commandName = invocation.commandName || "start";
68
+ const branchName = invocation.positionals[0];
69
+ const preview = invocation.args.preview === true;
70
+ const tenantRoot = context.cwd;
71
+ const result = createFeatureBranchFromStaging(tenantRoot, branchName);
72
+ pushBranch(result.repoDir, branchName, { setUpstream: true });
73
+ if (!preview) {
74
+ return guidedResult({
75
+ command: commandName,
76
+ summary: `Created feature branch ${branchName} from staging.`,
77
+ facts: [
78
+ { label: "Branch", value: branchName },
79
+ { label: "Preview", value: "disabled" }
80
+ ],
81
+ nextSteps: [
82
+ "treeseed dev",
83
+ 'treeseed save "describe your change"'
84
+ ],
85
+ report: {
86
+ branchName,
87
+ preview: false,
88
+ baseBranch: result.baseBranch
89
+ }
90
+ });
91
+ }
92
+ return provisionBranchPreview(branchName, context, commandName);
93
+ };
94
+ export {
95
+ handleStart,
96
+ provisionBranchPreview
97
+ };
@@ -0,0 +1,31 @@
1
+ import { guidedResult } from "./utils.js";
2
+ import { resolveTreeseedWorkflowState } from "../workflow-state.js";
3
+ const handleStatus = (_invocation, context) => {
4
+ const state = resolveTreeseedWorkflowState(context.cwd);
5
+ return guidedResult({
6
+ command: "status",
7
+ summary: "Treeseed workflow status",
8
+ facts: [
9
+ { label: "Workspace root", value: state.workspaceRoot ? "yes" : "no" },
10
+ { label: "Tenant config present", value: state.deployConfigPresent ? "yes" : "no" },
11
+ { label: "Branch", value: state.branchName ?? "(none)" },
12
+ { label: "Branch role", value: state.branchRole },
13
+ { label: "Mapped environment", value: state.environment },
14
+ { label: "Dirty worktree", value: state.dirtyWorktree ? "yes" : "no" },
15
+ { label: "Local initialized", value: state.persistentEnvironments.local.initialized ? "yes" : "no" },
16
+ { label: "Staging initialized", value: state.persistentEnvironments.staging.initialized ? "yes" : "no" },
17
+ { label: "Prod initialized", value: state.persistentEnvironments.prod.initialized ? "yes" : "no" },
18
+ { label: "Preview enabled", value: state.preview.enabled ? "yes" : "no" },
19
+ { label: "Preview URL", value: state.preview.url ?? "(none)" },
20
+ { label: "GitHub auth", value: state.auth.gh ? "ready" : "not ready" },
21
+ { label: "Wrangler auth", value: state.auth.wrangler ? "ready" : "not ready" }
22
+ ],
23
+ nextSteps: state.recommendations.map((item) => `${item.command} # ${item.reason}`),
24
+ report: {
25
+ state
26
+ }
27
+ });
28
+ };
29
+ export {
30
+ handleStatus
31
+ };
@@ -0,0 +1,50 @@
1
+ import { currentManagedBranch, PRODUCTION_BRANCH, STAGING_BRANCH } from "../../scripts/git-workflow-lib.js";
2
+ import { guidedResult } from "./utils.js";
3
+ import { handleClose } from "./close.js";
4
+ import { handleDestroy } from "./destroy.js";
5
+ const handleTeardown = async (invocation, context) => {
6
+ const commandName = invocation.commandName || "teardown";
7
+ const environment = typeof invocation.args.environment === "string" ? invocation.args.environment : null;
8
+ if (environment) {
9
+ return handleDestroy({
10
+ ...invocation,
11
+ commandName
12
+ }, context);
13
+ }
14
+ const branch = currentManagedBranch(context.cwd);
15
+ if (!branch || branch === STAGING_BRANCH || branch === PRODUCTION_BRANCH) {
16
+ return guidedResult({
17
+ command: commandName,
18
+ summary: "Treeseed teardown needs an explicit persistent environment on staging or main.",
19
+ facts: [{ label: "Current branch", value: branch ?? "(none)" }],
20
+ nextSteps: [
21
+ "treeseed teardown --environment staging",
22
+ "treeseed teardown --environment prod"
23
+ ],
24
+ report: {
25
+ branch
26
+ },
27
+ exitCode: 1
28
+ });
29
+ }
30
+ if (invocation.args.dryRun === true) {
31
+ return guidedResult({
32
+ command: commandName,
33
+ summary: `Dry run: Treeseed would merge ${branch} into staging and remove any branch preview artifacts.`,
34
+ facts: [{ label: "Branch", value: branch }],
35
+ nextSteps: ["treeseed teardown"],
36
+ report: {
37
+ branch,
38
+ dryRun: true,
39
+ mode: "feature-close"
40
+ }
41
+ });
42
+ }
43
+ return handleClose({
44
+ ...invocation,
45
+ commandName
46
+ }, context);
47
+ };
48
+ export {
49
+ handleTeardown
50
+ };
@@ -0,0 +1,59 @@
1
+ function ok(lines = []) {
2
+ return { exitCode: 0, stdout: lines };
3
+ }
4
+ function fail(message, exitCode = 1) {
5
+ return { exitCode, stderr: [message] };
6
+ }
7
+ function guidedResult(options) {
8
+ const lines = [options.summary];
9
+ const facts = (options.facts ?? []).filter((fact) => fact.value !== void 0 && fact.value !== null && `${fact.value}`.length > 0);
10
+ if (facts.length > 0) {
11
+ lines.push("");
12
+ for (const fact of facts) {
13
+ lines.push(`${fact.label}: ${fact.value}`);
14
+ }
15
+ }
16
+ if ((options.nextSteps ?? []).length > 0) {
17
+ lines.push("", "Next steps:");
18
+ for (const step of options.nextSteps ?? []) {
19
+ lines.push(`- ${step}`);
20
+ }
21
+ }
22
+ return {
23
+ exitCode: options.exitCode ?? 0,
24
+ stdout: lines,
25
+ 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
+ }
34
+ };
35
+ }
36
+ function writeResult(result, context) {
37
+ if (context.outputFormat === "json") {
38
+ const payload = result.report ?? {
39
+ ok: (result.exitCode ?? 0) === 0,
40
+ stdout: result.stdout ?? [],
41
+ stderr: result.stderr ?? []
42
+ };
43
+ context.write(JSON.stringify(payload, null, 2), (result.exitCode ?? 0) === 0 ? "stdout" : "stderr");
44
+ return result.exitCode ?? 0;
45
+ }
46
+ for (const line of result.stdout ?? []) {
47
+ context.write(line, "stdout");
48
+ }
49
+ for (const line of result.stderr ?? []) {
50
+ context.write(line, "stderr");
51
+ }
52
+ return result.exitCode ?? 0;
53
+ }
54
+ export {
55
+ fail,
56
+ guidedResult,
57
+ ok,
58
+ writeResult
59
+ };