@treeseed/sdk 0.4.11 → 0.4.13

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.
@@ -454,6 +454,8 @@ class DefaultTreeseedOperationsProvider {
454
454
  new WorkflowOperation("save"),
455
455
  new WorkflowOperation("close"),
456
456
  new WorkflowOperation("stage"),
457
+ new WorkflowOperation("resume"),
458
+ new WorkflowOperation("recover"),
457
459
  new WorkflowOperation("config"),
458
460
  new WorkflowOperation("export"),
459
461
  new WorkflowOperation("release"),
@@ -1,24 +1,55 @@
1
1
  export declare const STAGING_BRANCH = "staging";
2
2
  export declare const PRODUCTION_BRANCH = "main";
3
+ export declare function headCommit(repoDir: any, ref?: string): string;
3
4
  export declare function gitWorkflowRoot(cwd?: any): string;
4
5
  export declare function assertCleanWorktree(cwd?: any): string;
6
+ export declare function assertCleanWorktrees(repoDirs: any): any;
5
7
  export declare function branchExists(repoDir: any, branchName: any): boolean;
6
8
  export declare function remoteBranchExists(repoDir: any, branchName: any): boolean;
7
9
  export declare function fetchOrigin(repoDir: any): void;
8
10
  export declare function ensureLocalBranchTracking(repoDir: any, branchName: any): void;
9
11
  export declare function checkoutBranch(repoDir: any, branchName: any): void;
12
+ export declare function checkoutTaskBranchFromStaging(cwd: any, branchName: any, { createIfMissing, pushIfCreated }?: {
13
+ createIfMissing?: boolean | undefined;
14
+ pushIfCreated?: boolean | undefined;
15
+ }): {
16
+ repoDir: string;
17
+ branchName: any;
18
+ baseBranch: string;
19
+ created: boolean;
20
+ resumed: boolean;
21
+ remoteBranch: boolean;
22
+ };
10
23
  export declare function syncBranchWithOrigin(repoDir: any, branchName: any): void;
11
24
  export declare function createFeatureBranchFromStaging(cwd: any, branchName: any): {
12
25
  repoDir: string;
13
- baseBranch: string;
14
26
  branchName: any;
27
+ baseBranch: string;
28
+ created: boolean;
29
+ resumed: boolean;
30
+ remoteBranch: boolean;
15
31
  };
16
32
  export declare function pushBranch(repoDir: any, branchName: any, { setUpstream }?: {
17
33
  setUpstream?: boolean | undefined;
18
34
  }): void;
19
35
  export declare function deleteLocalBranch(repoDir: any, branchName: any): void;
20
36
  export declare function deleteRemoteBranch(repoDir: any, branchName: any): boolean;
21
- export declare function mergeCurrentBranchIntoStaging(cwd: any, featureBranch: any): string;
37
+ export declare function mergeCurrentBranchIntoStaging(cwd: any, featureBranch: any): {
38
+ repoDir: string;
39
+ targetBranch: string;
40
+ committed: boolean;
41
+ commitSha: string;
42
+ pushed: boolean;
43
+ };
44
+ export declare function squashMergeBranchIntoStaging(cwd: any, featureBranch: any, message: any, { pushTarget }?: {
45
+ pushTarget?: boolean | undefined;
46
+ }): {
47
+ repoDir: string;
48
+ targetBranch: string;
49
+ committed: boolean;
50
+ commitSha: string;
51
+ pushed: boolean;
52
+ };
22
53
  export declare function currentManagedBranch(cwd?: any): string;
23
54
  export declare function isTaskBranch(branchName: any): boolean;
24
55
  export declare function assertFeatureBranch(cwd?: any): string;
@@ -46,4 +77,17 @@ export declare function waitForStagingAutomation(repoDir: any): {
46
77
  reason?: undefined;
47
78
  };
48
79
  export declare function prepareReleaseBranches(cwd?: any): string;
49
- export declare function mergeStagingIntoMain(cwd?: any): string;
80
+ export declare function mergeStagingIntoMain(cwd?: any): {
81
+ repoDir: string;
82
+ targetBranch: any;
83
+ commitSha: string;
84
+ pushed: boolean;
85
+ };
86
+ export declare function mergeBranchIntoTarget(cwd?: any, { sourceBranch, targetBranch, message, pushTarget }?: {
87
+ pushTarget?: boolean | undefined;
88
+ }): {
89
+ repoDir: string;
90
+ targetBranch: any;
91
+ commitSha: string;
92
+ pushed: boolean;
93
+ };
@@ -6,6 +6,17 @@ const RESERVED_BRANCHES = /* @__PURE__ */ new Set([STAGING_BRANCH, PRODUCTION_BR
6
6
  function runGit(args, { cwd, capture = false } = {}) {
7
7
  return run("git", args, { cwd, capture });
8
8
  }
9
+ function repoHasStagedChanges(repoDir) {
10
+ try {
11
+ runGit(["diff", "--cached", "--quiet"], { cwd: repoDir });
12
+ return false;
13
+ } catch {
14
+ return true;
15
+ }
16
+ }
17
+ function headCommit(repoDir, ref = "HEAD") {
18
+ return runGit(["rev-parse", ref], { cwd: repoDir, capture: true }).trim();
19
+ }
9
20
  function gitWorkflowRoot(cwd = workspaceRoot()) {
10
21
  return repoRoot(cwd);
11
22
  }
@@ -16,6 +27,12 @@ function assertCleanWorktree(cwd = workspaceRoot()) {
16
27
  }
17
28
  return root;
18
29
  }
30
+ function assertCleanWorktrees(repoDirs) {
31
+ for (const repoDir of repoDirs) {
32
+ assertCleanWorktree(repoDir);
33
+ }
34
+ return repoDirs;
35
+ }
19
36
  function branchExists(repoDir, branchName) {
20
37
  try {
21
38
  runGit(["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoDir });
@@ -48,6 +65,63 @@ function ensureLocalBranchTracking(repoDir, branchName) {
48
65
  function checkoutBranch(repoDir, branchName) {
49
66
  runGit(["checkout", branchName], { cwd: repoDir });
50
67
  }
68
+ function checkoutTaskBranchFromStaging(cwd, branchName, { createIfMissing = true, pushIfCreated = false } = {}) {
69
+ const repoDir = assertCleanWorktree(cwd);
70
+ fetchOrigin(repoDir);
71
+ syncBranchWithOrigin(repoDir, STAGING_BRANCH);
72
+ if (currentBranch(repoDir) === branchName) {
73
+ return {
74
+ repoDir,
75
+ branchName,
76
+ baseBranch: STAGING_BRANCH,
77
+ created: false,
78
+ resumed: true,
79
+ remoteBranch: remoteBranchExists(repoDir, branchName)
80
+ };
81
+ }
82
+ if (branchExists(repoDir, branchName)) {
83
+ checkoutBranch(repoDir, branchName);
84
+ if (remoteBranchExists(repoDir, branchName)) {
85
+ runGit(["pull", "--rebase", "origin", branchName], { cwd: repoDir });
86
+ }
87
+ return {
88
+ repoDir,
89
+ branchName,
90
+ baseBranch: STAGING_BRANCH,
91
+ created: false,
92
+ resumed: true,
93
+ remoteBranch: remoteBranchExists(repoDir, branchName)
94
+ };
95
+ }
96
+ if (remoteBranchExists(repoDir, branchName)) {
97
+ runGit(["checkout", "-b", branchName, `origin/${branchName}`], { cwd: repoDir });
98
+ runGit(["pull", "--rebase", "origin", branchName], { cwd: repoDir });
99
+ return {
100
+ repoDir,
101
+ branchName,
102
+ baseBranch: STAGING_BRANCH,
103
+ created: false,
104
+ resumed: true,
105
+ remoteBranch: true
106
+ };
107
+ }
108
+ if (!createIfMissing) {
109
+ throw new Error(`Branch "${branchName}" does not exist locally or on origin.`);
110
+ }
111
+ checkoutBranch(repoDir, STAGING_BRANCH);
112
+ runGit(["checkout", "-b", branchName], { cwd: repoDir });
113
+ if (pushIfCreated) {
114
+ pushBranch(repoDir, branchName, { setUpstream: true });
115
+ }
116
+ return {
117
+ repoDir,
118
+ branchName,
119
+ baseBranch: STAGING_BRANCH,
120
+ created: true,
121
+ resumed: false,
122
+ remoteBranch: pushIfCreated
123
+ };
124
+ }
51
125
  function syncBranchWithOrigin(repoDir, branchName) {
52
126
  fetchOrigin(repoDir);
53
127
  if (!branchExists(repoDir, branchName) && remoteBranchExists(repoDir, branchName)) {
@@ -60,18 +134,14 @@ function syncBranchWithOrigin(repoDir, branchName) {
60
134
  }
61
135
  }
62
136
  function createFeatureBranchFromStaging(cwd, branchName) {
63
- const repoDir = assertCleanWorktree(cwd);
64
- fetchOrigin(repoDir);
65
- if (branchExists(repoDir, branchName) || remoteBranchExists(repoDir, branchName)) {
137
+ const result = checkoutTaskBranchFromStaging(cwd, branchName, {
138
+ createIfMissing: true,
139
+ pushIfCreated: false
140
+ });
141
+ if (!result.created) {
66
142
  throw new Error(`Branch "${branchName}" already exists locally or on origin.`);
67
143
  }
68
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
69
- runGit(["checkout", "-b", branchName], { cwd: repoDir });
70
- return {
71
- repoDir,
72
- baseBranch: STAGING_BRANCH,
73
- branchName
74
- };
144
+ return result;
75
145
  }
76
146
  function pushBranch(repoDir, branchName, { setUpstream = false } = {}) {
77
147
  const args = setUpstream ? ["push", "-u", "origin", branchName] : ["push", "origin", branchName];
@@ -91,12 +161,28 @@ function deleteRemoteBranch(repoDir, branchName) {
91
161
  return true;
92
162
  }
93
163
  function mergeCurrentBranchIntoStaging(cwd, featureBranch) {
164
+ return squashMergeBranchIntoStaging(cwd, featureBranch, `stage: ${featureBranch}`);
165
+ }
166
+ function squashMergeBranchIntoStaging(cwd, featureBranch, message, { pushTarget = true } = {}) {
94
167
  const repoDir = assertCleanWorktree(cwd);
95
168
  fetchOrigin(repoDir);
96
169
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
97
- runGit(["merge", "--no-ff", featureBranch, "-m", `merge: ${featureBranch} -> ${STAGING_BRANCH}`], { cwd: repoDir });
98
- pushBranch(repoDir, STAGING_BRANCH);
99
- return repoDir;
170
+ runGit(["merge", "--squash", featureBranch], { cwd: repoDir });
171
+ let committed = false;
172
+ if (repoHasStagedChanges(repoDir)) {
173
+ runGit(["commit", "-m", message], { cwd: repoDir });
174
+ committed = true;
175
+ }
176
+ if (pushTarget) {
177
+ pushBranch(repoDir, STAGING_BRANCH);
178
+ }
179
+ return {
180
+ repoDir,
181
+ targetBranch: STAGING_BRANCH,
182
+ committed,
183
+ commitSha: headCommit(repoDir),
184
+ pushed: pushTarget
185
+ };
100
186
  }
101
187
  function currentManagedBranch(cwd = workspaceRoot()) {
102
188
  return currentBranch(gitWorkflowRoot(cwd));
@@ -180,23 +266,40 @@ function prepareReleaseBranches(cwd = workspaceRoot()) {
180
266
  return repoDir;
181
267
  }
182
268
  function mergeStagingIntoMain(cwd = workspaceRoot()) {
269
+ return mergeBranchIntoTarget(cwd, {
270
+ sourceBranch: STAGING_BRANCH,
271
+ targetBranch: PRODUCTION_BRANCH,
272
+ message: `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`,
273
+ pushTarget: true
274
+ });
275
+ }
276
+ function mergeBranchIntoTarget(cwd = workspaceRoot(), { sourceBranch, targetBranch, message, pushTarget = true } = {}) {
183
277
  const repoDir = prepareReleaseBranches(cwd);
184
- checkoutBranch(repoDir, PRODUCTION_BRANCH);
185
- if (remoteBranchExists(repoDir, PRODUCTION_BRANCH)) {
186
- runGit(["pull", "--rebase", "origin", PRODUCTION_BRANCH], { cwd: repoDir });
278
+ checkoutBranch(repoDir, targetBranch);
279
+ if (remoteBranchExists(repoDir, targetBranch)) {
280
+ runGit(["pull", "--rebase", "origin", targetBranch], { cwd: repoDir });
187
281
  }
188
- runGit(["merge", "--no-ff", STAGING_BRANCH, "-m", `release: ${STAGING_BRANCH} -> ${PRODUCTION_BRANCH}`], { cwd: repoDir });
282
+ runGit(["merge", "--no-ff", sourceBranch, "-m", message], { cwd: repoDir });
189
283
  pushBranch(repoDir, STAGING_BRANCH);
190
- pushBranch(repoDir, PRODUCTION_BRANCH);
191
- return repoDir;
284
+ if (pushTarget) {
285
+ pushBranch(repoDir, targetBranch);
286
+ }
287
+ return {
288
+ repoDir,
289
+ targetBranch,
290
+ commitSha: headCommit(repoDir),
291
+ pushed: pushTarget
292
+ };
192
293
  }
193
294
  export {
194
295
  PRODUCTION_BRANCH,
195
296
  STAGING_BRANCH,
196
297
  assertCleanWorktree,
298
+ assertCleanWorktrees,
197
299
  assertFeatureBranch,
198
300
  branchExists,
199
301
  checkoutBranch,
302
+ checkoutTaskBranchFromStaging,
200
303
  createDeprecatedTaskTag,
201
304
  createFeatureBranchFromStaging,
202
305
  currentManagedBranch,
@@ -205,13 +308,16 @@ export {
205
308
  ensureLocalBranchTracking,
206
309
  fetchOrigin,
207
310
  gitWorkflowRoot,
311
+ headCommit,
208
312
  isTaskBranch,
209
313
  listTaskBranches,
314
+ mergeBranchIntoTarget,
210
315
  mergeCurrentBranchIntoStaging,
211
316
  mergeStagingIntoMain,
212
317
  prepareReleaseBranches,
213
318
  pushBranch,
214
319
  remoteBranchExists,
320
+ squashMergeBranchIntoStaging,
215
321
  syncBranchWithOrigin,
216
322
  taskTagSlug,
217
323
  waitForStagingAutomation
@@ -190,3 +190,28 @@ export declare function ensureGitHubDeployAutomation(tenantRoot: any, { dryRun }
190
190
  mode?: undefined;
191
191
  };
192
192
  };
193
+ export declare function waitForGitHubWorkflowCompletion(tenantRoot: any, { repository, workflow, headSha, branch, timeoutSeconds, pollSeconds, }?: {
194
+ workflow?: string | undefined;
195
+ timeoutSeconds?: number | undefined;
196
+ pollSeconds?: number | undefined;
197
+ }): {
198
+ status: string;
199
+ reason: string;
200
+ repository: any;
201
+ workflow: string;
202
+ headSha: any;
203
+ branch: any;
204
+ runId?: undefined;
205
+ conclusion?: undefined;
206
+ url?: undefined;
207
+ } | {
208
+ status: string;
209
+ repository: any;
210
+ workflow: string;
211
+ runId: any;
212
+ headSha: any;
213
+ conclusion: any;
214
+ url: any;
215
+ reason?: undefined;
216
+ branch?: undefined;
217
+ };
@@ -53,6 +53,14 @@ function runGh(args, { cwd, allowFailure = false, capture = true, input } = {})
53
53
  }
54
54
  return result;
55
55
  }
56
+ function sleepSeconds(seconds) {
57
+ if (!Number.isFinite(seconds) || seconds <= 0) {
58
+ return;
59
+ }
60
+ spawnSync("sleep", [String(seconds)], {
61
+ stdio: "ignore"
62
+ });
63
+ }
56
64
  function resolveGitHubRepositorySlug(tenantRoot) {
57
65
  const remoteResult = runGit(["remote", "get-url", "origin"], { cwd: tenantRoot });
58
66
  const remoteUrl = remoteResult.stdout?.trim() ?? "";
@@ -291,6 +299,78 @@ function ensureGitHubDeployAutomation(tenantRoot, { dryRun = false } = {}) {
291
299
  environment
292
300
  };
293
301
  }
302
+ function waitForGitHubWorkflowCompletion(tenantRoot, {
303
+ repository,
304
+ workflow = "publish.yml",
305
+ headSha,
306
+ branch,
307
+ timeoutSeconds = 600,
308
+ pollSeconds = 5
309
+ } = {}) {
310
+ if (isGitHubAutomationStubbed()) {
311
+ return {
312
+ status: "skipped",
313
+ reason: "stubbed",
314
+ repository: repository ?? maybeResolveGitHubRepositorySlug(tenantRoot),
315
+ workflow,
316
+ headSha: headSha ?? null,
317
+ branch: branch ?? null
318
+ };
319
+ }
320
+ const repo = repository ?? resolveGitHubRepositorySlug(tenantRoot);
321
+ const startedAt = Date.now();
322
+ while (Date.now() - startedAt < timeoutSeconds * 1e3) {
323
+ const result = runGh([
324
+ "run",
325
+ "list",
326
+ "--repo",
327
+ repo,
328
+ "--workflow",
329
+ workflow,
330
+ "--limit",
331
+ "20",
332
+ "--json",
333
+ "databaseId,headSha,headBranch,status,conclusion,event,displayTitle,url"
334
+ ], { cwd: tenantRoot });
335
+ const runs = JSON.parse(result.stdout || "[]");
336
+ const match = runs.find((run) => {
337
+ if (headSha && run?.headSha !== headSha) {
338
+ return false;
339
+ }
340
+ if (branch && run?.headBranch !== branch) {
341
+ return false;
342
+ }
343
+ return true;
344
+ });
345
+ if (match?.databaseId) {
346
+ runGh(["run", "watch", String(match.databaseId), "--repo", repo, "--exit-status"], {
347
+ cwd: tenantRoot,
348
+ capture: false
349
+ });
350
+ const finalResult = runGh([
351
+ "run",
352
+ "view",
353
+ String(match.databaseId),
354
+ "--repo",
355
+ repo,
356
+ "--json",
357
+ "status,conclusion,url,workflowName,headSha"
358
+ ], { cwd: tenantRoot });
359
+ const finalRun = JSON.parse(finalResult.stdout || "{}");
360
+ return {
361
+ status: "completed",
362
+ repository: repo,
363
+ workflow,
364
+ runId: match.databaseId,
365
+ headSha: finalRun.headSha ?? match.headSha ?? null,
366
+ conclusion: finalRun.conclusion ?? match.conclusion ?? null,
367
+ url: finalRun.url ?? match.url ?? null
368
+ };
369
+ }
370
+ sleepSeconds(pollSeconds);
371
+ }
372
+ throw new Error(`Timed out waiting for GitHub workflow ${workflow} in ${repo}.`);
373
+ }
294
374
  export {
295
375
  ensureDeployWorkflow,
296
376
  ensureGitHubDeployAutomation,
@@ -309,5 +389,6 @@ export {
309
389
  requiredGitHubEnvironment,
310
390
  requiredGitHubSecrets,
311
391
  resolveGitHubRepositorySlug,
312
- resolveGitRepositoryRoot
392
+ resolveGitRepositoryRoot,
393
+ waitForGitHubWorkflowCompletion
313
394
  };
@@ -8,12 +8,21 @@ export declare function planWorkspaceVersionChanges(root?: any): {
8
8
  touched: Set<unknown>;
9
9
  };
10
10
  export declare function applyWorkspaceVersionChanges(plan: any): any;
11
- export declare function planWorkspaceReleaseBump(level?: string, root?: any): {
11
+ export declare function planWorkspaceReleaseBump(level?: string, root?: any, options?: {}): {
12
12
  packages: any[];
13
13
  touched: Set<unknown>;
14
14
  versions: Map<any, any>;
15
15
  level: string;
16
+ selected: Set<any>;
16
17
  };
18
+ export declare function collectWorkspaceVersionConsistencyIssues(root?: any): {
19
+ packageName: any;
20
+ dependencyName: string;
21
+ field: string;
22
+ currentSpec: unknown;
23
+ expectedSpec: string;
24
+ }[];
25
+ export declare function assertWorkspaceVersionConsistency(root?: any): void;
17
26
  export declare function repoRoot(cwd?: any): string;
18
27
  export declare function currentBranch(repoDir: any): string;
19
28
  export declare function originRemoteUrl(repoDir: any): string;
@@ -113,17 +113,20 @@ function applyWorkspaceVersionChanges(plan) {
113
113
  }
114
114
  return plan;
115
115
  }
116
- function planWorkspaceReleaseBump(level = "patch", root = workspaceRoot()) {
116
+ function planWorkspaceReleaseBump(level = "patch", root = workspaceRoot(), options = {}) {
117
117
  const packages = workspacePackages(root).map((pkg) => ({
118
118
  ...pkg,
119
119
  packageJsonPath: resolve(pkg.dir, "package.json"),
120
120
  packageJson: readPackageJson(resolve(pkg.dir, "package.json"))
121
121
  }));
122
122
  const publishable = new Set(publishableWorkspacePackages(root).map((pkg) => pkg.name));
123
+ const selected = options.selectedPackageNames ? new Set(
124
+ [...options.selectedPackageNames].map((name) => String(name)).filter((name) => publishable.has(name))
125
+ ) : new Set(publishable);
123
126
  const touched = /* @__PURE__ */ new Set();
124
127
  const versions = /* @__PURE__ */ new Map();
125
128
  for (const pkg of packages) {
126
- if (!publishable.has(pkg.name)) {
129
+ if (!publishable.has(pkg.name) || !selected.has(pkg.name)) {
127
130
  continue;
128
131
  }
129
132
  const nextVersion = incrementVersion(pkg.packageJson.version, level);
@@ -132,6 +135,9 @@ function planWorkspaceReleaseBump(level = "patch", root = workspaceRoot()) {
132
135
  touched.add(pkg.name);
133
136
  }
134
137
  for (const pkg of packages) {
138
+ if (!selected.has(pkg.name)) {
139
+ continue;
140
+ }
135
141
  for (const field of internalDependencyFields(pkg.packageJson)) {
136
142
  for (const depName of Object.keys(pkg.packageJson[field] ?? {})) {
137
143
  if (!versions.has(depName)) {
@@ -146,9 +152,52 @@ function planWorkspaceReleaseBump(level = "patch", root = workspaceRoot()) {
146
152
  packages,
147
153
  touched,
148
154
  versions,
149
- level
155
+ level,
156
+ selected
150
157
  };
151
158
  }
159
+ function collectWorkspaceVersionConsistencyIssues(root = workspaceRoot()) {
160
+ const packages = workspacePackages(root).map((pkg) => ({
161
+ ...pkg,
162
+ packageJson: readPackageJson(resolve(pkg.dir, "package.json"))
163
+ }));
164
+ const versions = new Map(packages.map((pkg) => [pkg.name, pkg.packageJson.version]));
165
+ const issues = [];
166
+ for (const pkg of packages) {
167
+ for (const field of internalDependencyFields(pkg.packageJson)) {
168
+ for (const [depName, currentSpec] of Object.entries(pkg.packageJson[field] ?? {})) {
169
+ if (!versions.has(depName)) {
170
+ continue;
171
+ }
172
+ const expectedSpec = `^${versions.get(depName)}`;
173
+ if (currentSpec !== expectedSpec) {
174
+ issues.push({
175
+ packageName: pkg.name,
176
+ dependencyName: depName,
177
+ field,
178
+ currentSpec,
179
+ expectedSpec
180
+ });
181
+ }
182
+ }
183
+ }
184
+ }
185
+ return issues;
186
+ }
187
+ function assertWorkspaceVersionConsistency(root = workspaceRoot()) {
188
+ const issues = collectWorkspaceVersionConsistencyIssues(root);
189
+ if (issues.length === 0) {
190
+ return;
191
+ }
192
+ const rendered = issues.map((issue) => `${issue.packageName} ${issue.field}.${issue.dependencyName}=${issue.currentSpec} expected ${issue.expectedSpec}`).join("\n");
193
+ throw new Error(
194
+ [
195
+ "Treeseed save found inconsistent checked-out package dependency versions.",
196
+ "Resolve the package manifest drift before saving.",
197
+ rendered
198
+ ].join("\n")
199
+ );
200
+ }
152
201
  function repoRoot(cwd = workspaceRoot()) {
153
202
  return run("git", ["rev-parse", "--show-toplevel"], { cwd, capture: true }).trim();
154
203
  }
@@ -220,7 +269,9 @@ function formatMergeConflictReport(report, repoDir, targetBranch = "main") {
220
269
  export {
221
270
  MERGE_CONFLICT_EXIT_CODE,
222
271
  applyWorkspaceVersionChanges,
272
+ assertWorkspaceVersionConsistency,
223
273
  collectMergeConflictReport,
274
+ collectWorkspaceVersionConsistencyIssues,
224
275
  countConflictMarkers,
225
276
  currentBranch,
226
277
  formatMergeConflictReport,
@@ -1,3 +1,4 @@
1
+ export declare const TREESEED_WORKSPACE_PACKAGE_DIRS: string[];
1
2
  export declare function workspaceRoot(startCwd?: string): any;
2
3
  export declare function findNearestTreeseedRoot(startCwd?: string): string | null;
3
4
  export declare function isWorkspaceRoot(root?: string): any;
@@ -1,7 +1,21 @@
1
1
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { join, relative, resolve } from "node:path";
4
- const TREESEED_WORKSPACE_PACKAGE_DIRS = ["sdk", "core", "cli", "agent", "api"];
4
+ const TREESEED_WORKSPACE_PACKAGE_DIRS = ["sdk", "core", "cli"];
5
+ function packageSortWeight(pkg) {
6
+ const relativeDir = String(pkg.relativeDir ?? "");
7
+ const dirName = relativeDir.split("/").pop() ?? "";
8
+ const index = TREESEED_WORKSPACE_PACKAGE_DIRS.indexOf(dirName);
9
+ return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
10
+ }
11
+ function compareWorkspacePackages(left, right) {
12
+ const leftWeight = packageSortWeight(left);
13
+ const rightWeight = packageSortWeight(right);
14
+ if (leftWeight !== rightWeight) {
15
+ return leftWeight - rightWeight;
16
+ }
17
+ return left.name.localeCompare(right.name);
18
+ }
5
19
  function escapeRegex(source) {
6
20
  return source.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
7
21
  }
@@ -103,7 +117,7 @@ function workspacePackages(root = workspaceRoot()) {
103
117
  });
104
118
  }
105
119
  }
106
- return [...discovered.values()].sort((left, right) => left.name.localeCompare(right.name));
120
+ return [...discovered.values()].sort(compareWorkspacePackages);
107
121
  }
108
122
  function internalDependenciesFor(pkg, packageNames) {
109
123
  const internalDeps = /* @__PURE__ */ new Set();
@@ -127,7 +141,7 @@ function sortWorkspacePackages(packages) {
127
141
  indegree.set(pkg.name, (indegree.get(pkg.name) ?? 0) + 1);
128
142
  }
129
143
  }
130
- const ready = [...packages].filter((pkg) => (indegree.get(pkg.name) ?? 0) === 0).sort((left, right) => left.name.localeCompare(right.name));
144
+ const ready = [...packages].filter((pkg) => (indegree.get(pkg.name) ?? 0) === 0).sort(compareWorkspacePackages);
131
145
  const ordered = [];
132
146
  while (ready.length > 0) {
133
147
  const next = ready.shift();
@@ -142,13 +156,13 @@ function sortWorkspacePackages(packages) {
142
156
  const dependent = packageMap.get(dependentName);
143
157
  if (dependent) {
144
158
  ready.push(dependent);
145
- ready.sort((left, right) => left.name.localeCompare(right.name));
159
+ ready.sort(compareWorkspacePackages);
146
160
  }
147
161
  }
148
162
  }
149
163
  }
150
164
  if (ordered.length !== packages.length) {
151
- return [...packages].sort((left, right) => left.name.localeCompare(right.name));
165
+ return [...packages].sort(compareWorkspacePackages);
152
166
  }
153
167
  return ordered;
154
168
  }
@@ -251,6 +265,7 @@ function cleanupDir(dirPath) {
251
265
  }
252
266
  }
253
267
  export {
268
+ TREESEED_WORKSPACE_PACKAGE_DIRS,
254
269
  changedWorkspacePackages,
255
270
  cleanupDir,
256
271
  createTempDir,
@@ -3,11 +3,13 @@ function operation(spec) {
3
3
  }
4
4
  const TRESEED_OPERATION_SPECS = [
5
5
  operation({ id: "workspace.status", name: "status", aliases: [], group: "Workflow", summary: "Show Treeseed project health and the current task state.", description: "Report branch/task state, runtime readiness, preview/deploy state, auth readiness, and recommended next operations.", provider: "default", related: ["tasks", "switch", "config"] }),
6
- operation({ id: "branch.tasks", name: "tasks", aliases: [], group: "Workflow", summary: "List task branches and preview metadata.", description: "List task-based preview Git branches from local and origin, excluding protected and deprecated refs.", provider: "default", related: ["status", "switch", "close"] }),
7
- operation({ id: "branch.switch", name: "switch", aliases: [], group: "Workflow", summary: "Create or resume a task branch.", description: "Create a new task branch from staging or resume an existing local or remote task branch.", provider: "default", related: ["tasks", "dev", "save"] }),
8
- operation({ id: "branch.save", name: "save", aliases: [], group: "Workflow", summary: "Verify, commit, sync, push, and refresh preview for the current task.", description: "Run lint, test, and build verification, sync the current branch with origin, push it, and refresh the branch preview when enabled.", provider: "default", related: ["switch", "stage", "status"] }),
9
- operation({ id: "branch.close", name: "close", aliases: [], group: "Workflow", summary: "Archive a task branch without merging it.", description: "Destroy branch preview resources, create a deprecated resurrection tag, push the tag, and delete the local and remote branch.", provider: "default", related: ["tasks", "switch", "stage"] }),
10
- operation({ id: "branch.stage", name: "stage", aliases: [], group: "Workflow", summary: "Merge the current task into staging and clean it up.", description: "Verify the current task branch, merge it into staging, wait for staging automation, clean preview resources, create a deprecated resurrection tag, and delete the task branch.", provider: "default", related: ["save", "release", "close"] }),
6
+ operation({ id: "branch.tasks", name: "tasks", aliases: [], group: "Workflow", summary: "List task branches plus package branch alignment.", description: "List task branches from market and checked-out package repos, including preview metadata and branch/pointer alignment state.", provider: "default", related: ["status", "switch", "close"] }),
7
+ operation({ id: "branch.switch", name: "switch", aliases: [], group: "Workflow", summary: "Create or resume a recursive task branch.", description: "Create or resume the task branch in market and any checked-out package repos, with optional preview provisioning.", provider: "default", related: ["tasks", "dev", "save"] }),
8
+ operation({ id: "branch.save", name: "save", aliases: [], group: "Workflow", summary: "Recursively verify, commit, and push the current task checkpoint.", description: "Save dirty package repos in dependency order, then verify, commit, push, and optionally refresh preview for market.", provider: "default", related: ["switch", "stage", "status"] }),
9
+ operation({ id: "branch.close", name: "close", aliases: [], group: "Workflow", summary: "Recursively archive and delete a task branch.", description: "Auto-save if needed, clean preview resources, create deprecated tags, and remove the task branch across market and checked-out package repos.", provider: "default", related: ["tasks", "switch", "stage"] }),
10
+ operation({ id: "branch.stage", name: "stage", aliases: [], group: "Workflow", summary: "Squash a task branch into staging across market and packages.", description: "Auto-save if needed, squash-merge package task branches into staging first, update market submodule pointers, then squash-merge market into staging and clean up the task branch.", provider: "default", related: ["save", "release", "close"] }),
11
+ operation({ id: "workspace.resume", name: "resume", aliases: [], group: "Workflow", summary: "Resume an interrupted workflow run.", description: "Continue a failed journaled workflow run from its next incomplete step after validating current workspace preconditions.", provider: "default", related: ["recover", "status"] }),
12
+ operation({ id: "workspace.recover", name: "recover", aliases: [], group: "Workflow", summary: "Inspect active workflow locks and interrupted runs.", description: "List active workflow locks, resumable interrupted runs, and the exact commands needed to continue or recover the workspace state.", provider: "default", related: ["resume", "status"] }),
11
13
  operation({ id: "deploy.rollback", name: "rollback", aliases: [], group: "Workflow", summary: "Roll back staging or production to a recorded deployment.", description: "Redeploy a previously recorded staging or production commit using a temporary checkout of that revision.", provider: "default", related: ["status", "release"] }),
12
14
  operation({ id: "workspace.doctor", name: "doctor", aliases: [], group: "Validation", summary: "Diagnose Treeseed tooling, auth, and workflow readiness.", description: "Collect doctor-style diagnostics for workspace readiness and optional safe repairs.", provider: "default", related: ["status", "config"] }),
13
15
  operation({ id: "auth.login", name: "auth:login", aliases: [], group: "Validation", summary: "Authenticate against the configured Treeseed API.", description: "Start the device login flow against the active Treeseed API host and persist the returned session locally.", provider: "default", related: ["auth:check", "auth:whoami", "auth:logout"] }),
@@ -18,7 +20,7 @@ const TRESEED_OPERATION_SPECS = [
18
20
  operation({ id: "project.init", name: "init", aliases: [], group: "Workflow", summary: "Scaffold a new Treeseed tenant project.", description: "Create a new Treeseed tenant directory from a remote-catalog template backed by the packaged scaffold artifact.", provider: "default", related: ["config", "switch", "dev"] }),
19
21
  operation({ id: "project.config", name: "config", aliases: [], group: "Workflow", summary: "Configure and test the runtime foundation.", description: "Apply safe repairs, collect environment values, write local machine config, generate local env files, initialize environments, sync providers, and run doctor-style checks.", provider: "default", related: ["status", "switch", "dev"] }),
20
22
  operation({ id: "project.export", name: "export", aliases: [], group: "Utilities", summary: "Export a Markdown snapshot of the current codebase.", description: "Generate a Markdown codebase snapshot for the selected directory using the SDK-owned repomix integration and store it under .treeseed/exports.", provider: "default", related: ["status", "config"] }),
21
- operation({ id: "deploy.release", name: "release", aliases: [], group: "Workflow", summary: "Promote staging to production with a version bump.", description: "Validate staging, apply one version bump, tag the release, merge staging into main, push, and rely on production deploy automation.", provider: "default", related: ["stage", "status", "rollback"] }),
23
+ operation({ id: "deploy.release", name: "release", aliases: [], group: "Workflow", summary: "Release changed packages and market from staging to production.", description: "Select changed packages plus dependents, validate publish workflows, release packages first, then promote market from staging to main with aligned package pointers.", provider: "default", related: ["stage", "status", "rollback"] }),
22
24
  operation({ id: "deploy.destroy", name: "destroy", aliases: [], group: "Workflow", summary: "Destroy a persistent environment and its local state.", description: "Delete the selected persistent environment resources and remove the local deploy state after confirmation.", provider: "default", related: ["config", "status"] }),
23
25
  operation({ id: "local.dev", name: "dev", aliases: [], group: "Local Development", summary: "Start the unified local Treeseed development environment.", description: "Start the unified local Treeseed development environment.", provider: "default" }),
24
26
  operation({ id: "local.devWatch", name: "dev:watch", aliases: [], group: "Local Development", summary: "Start local development with rebuild and watch mode.", description: "Start local development with rebuild and watch mode.", provider: "default" }),