@treeseed/sdk 0.6.6 → 0.6.8

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 (48) hide show
  1. package/dist/copilot.d.ts +15 -0
  2. package/dist/copilot.js +75 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +18 -0
  5. package/dist/managed-dependencies.d.ts +56 -0
  6. package/dist/managed-dependencies.js +668 -0
  7. package/dist/operations/providers/default.js +30 -1
  8. package/dist/operations/services/commit-message-provider.d.ts +33 -0
  9. package/dist/operations/services/commit-message-provider.js +319 -0
  10. package/dist/operations/services/config-runtime.js +41 -20
  11. package/dist/operations/services/git-remote-policy.d.ts +9 -0
  12. package/dist/operations/services/git-remote-policy.js +55 -0
  13. package/dist/operations/services/git-workflow.js +22 -3
  14. package/dist/operations/services/github-api.js +9 -4
  15. package/dist/operations/services/knowledge-coop-launch.js +4 -0
  16. package/dist/operations/services/local-dev.js +7 -2
  17. package/dist/operations/services/package-reference-policy.d.ts +70 -0
  18. package/dist/operations/services/package-reference-policy.js +314 -0
  19. package/dist/operations/services/project-platform.d.ts +4 -0
  20. package/dist/operations/services/project-platform.js +30 -4
  21. package/dist/operations/services/railway-deploy.d.ts +4 -1
  22. package/dist/operations/services/railway-deploy.js +76 -38
  23. package/dist/operations/services/repository-save-orchestrator.d.ts +172 -0
  24. package/dist/operations/services/repository-save-orchestrator.js +1462 -0
  25. package/dist/operations/services/workspace-dependency-mode.d.ts +70 -0
  26. package/dist/operations/services/workspace-dependency-mode.js +404 -0
  27. package/dist/operations/services/workspace-preflight.js +5 -0
  28. package/dist/operations/services/workspace-save.js +10 -6
  29. package/dist/operations-registry.js +5 -0
  30. package/dist/operations-types.d.ts +1 -0
  31. package/dist/platform/books-data.js +4 -1
  32. package/dist/platform/env.yaml +6 -3
  33. package/dist/reconcile/builtin-adapters.js +37 -7
  34. package/dist/scripts/cleanup-markdown.js +4 -0
  35. package/dist/scripts/publish-package.js +5 -0
  36. package/dist/scripts/tenant-workflow-action.js +11 -2
  37. package/dist/verification.js +24 -12
  38. package/dist/workflow/operations.d.ts +381 -55
  39. package/dist/workflow/operations.js +718 -258
  40. package/dist/workflow-state.d.ts +40 -1
  41. package/dist/workflow-state.js +220 -17
  42. package/dist/workflow-support.d.ts +3 -0
  43. package/dist/workflow-support.js +34 -0
  44. package/dist/workflow.d.ts +19 -3
  45. package/dist/workflow.js +3 -3
  46. package/dist/wrangler-d1.js +6 -1
  47. package/package.json +17 -1
  48. package/templates/github/deploy.workflow.yml +28 -13
@@ -0,0 +1,1462 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { basename, resolve, relative } from "node:path";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import { getGitHubAutomationMode } from "./github-automation.js";
6
+ import {
7
+ ensureSshPushUrlForOrigin,
8
+ remoteWriteUrl,
9
+ sshPushUrlForRemote
10
+ } from "./git-remote-policy.js";
11
+ import {
12
+ generateRepositoryCommitMessage
13
+ } from "./commit-message-provider.js";
14
+ import {
15
+ createDevTagMessage,
16
+ createPackageDependencyReference,
17
+ updateInternalDependencySpecs
18
+ } from "./package-reference-policy.js";
19
+ import {
20
+ PRODUCTION_BRANCH,
21
+ branchExists,
22
+ headCommit,
23
+ remoteBranchExists,
24
+ STAGING_BRANCH
25
+ } from "./git-workflow.js";
26
+ import {
27
+ collectMergeConflictReport,
28
+ currentBranch,
29
+ formatMergeConflictReport,
30
+ hasMeaningfulChanges,
31
+ incrementVersion,
32
+ originRemoteUrl,
33
+ repoRoot
34
+ } from "./workspace-save.js";
35
+ import {
36
+ hasCompleteTreeseedPackageCheckout,
37
+ run,
38
+ sortWorkspacePackages,
39
+ workspacePackages
40
+ } from "./workspace-tools.js";
41
+ import { collectDeploymentLockfileWorkspaceIssues } from "./workspace-dependency-mode.js";
42
+ class RepositorySaveError extends Error {
43
+ exitCode;
44
+ details;
45
+ constructor(message, options = {}) {
46
+ super(message);
47
+ this.name = "RepositorySaveError";
48
+ this.exitCode = options.exitCode;
49
+ this.details = options.details;
50
+ }
51
+ }
52
+ function readJson(filePath) {
53
+ return JSON.parse(readFileSync(filePath, "utf8"));
54
+ }
55
+ function writeJson(filePath, value) {
56
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
57
+ `, "utf8");
58
+ }
59
+ function progressPrefix(node, phase) {
60
+ return `[${node.name}][${phase}]`;
61
+ }
62
+ function emitProgress(options, node, phase, message, stream = "stdout") {
63
+ const lines = String(message ?? "").split(/\r?\n/u).map((line) => line.trimEnd()).filter(Boolean);
64
+ for (const line of lines) {
65
+ options.onProgress?.(`${progressPrefix(node, phase)} ${line}`, stream);
66
+ }
67
+ }
68
+ function prefixedOutput(node, phase, output) {
69
+ return String(output ?? "").split(/\r?\n/u).map((line) => line.trimEnd()).filter(Boolean).map((line) => `${progressPrefix(node, phase)} ${line}`).join("\n");
70
+ }
71
+ function runCapturedCommand(node, options, phase, command, args, commandOptions = {}) {
72
+ emitProgress(options, node, phase, `$ ${command} ${args.join(" ")}`);
73
+ const result = spawnSync(command, args, {
74
+ cwd: commandOptions.cwd ?? node.path,
75
+ env: { ...process.env, ...commandOptions.env ?? {} },
76
+ stdio: "pipe",
77
+ encoding: "utf8",
78
+ timeout: commandOptions.timeoutMs
79
+ });
80
+ const stdout = result.stdout?.trim() ?? "";
81
+ const stderr = result.stderr?.trim() ?? "";
82
+ if (stdout) emitProgress(options, node, phase, stdout);
83
+ if (stderr) emitProgress(options, node, phase, stderr, "stderr");
84
+ if (result.status !== 0) {
85
+ const message = (result.error?.message ? `${result.error.message}
86
+ ` : "") + (prefixedOutput(node, phase, stderr) || prefixedOutput(node, phase, stdout) || `${progressPrefix(node, phase)} ${command} ${args.join(" ")} failed`);
87
+ throw new RepositorySaveError(message, {
88
+ details: {
89
+ failingRepo: node.name,
90
+ phase,
91
+ command: `${command} ${args.join(" ")}`
92
+ }
93
+ });
94
+ }
95
+ return stdout;
96
+ }
97
+ function isNoOpGitCommitError(error) {
98
+ if (!(error instanceof RepositorySaveError)) return false;
99
+ const command = typeof error.details?.command === "string" ? error.details.command : "";
100
+ if (!command.startsWith("git commit ")) return false;
101
+ return /nothing to commit|no changes added to commit/u.test(error.message);
102
+ }
103
+ function runQuietCommand(node, phase, command, args, commandOptions = {}) {
104
+ const result = spawnSync(command, args, {
105
+ cwd: commandOptions.cwd ?? node.path,
106
+ env: { ...process.env, ...commandOptions.env ?? {} },
107
+ stdio: "pipe",
108
+ encoding: "utf8",
109
+ timeout: commandOptions.timeoutMs
110
+ });
111
+ const stdout = result.stdout?.trim() ?? "";
112
+ const stderr = result.stderr?.trim() ?? "";
113
+ if (result.status !== 0) {
114
+ throw new RepositorySaveError(
115
+ [
116
+ `${progressPrefix(node, phase)} ${command} ${args.join(" ")} failed`,
117
+ stderr || stdout
118
+ ].filter(Boolean).join("\n"),
119
+ {
120
+ details: {
121
+ failingRepo: node.name,
122
+ phase,
123
+ command: `${command} ${args.join(" ")}`
124
+ }
125
+ }
126
+ );
127
+ }
128
+ return stdout;
129
+ }
130
+ async function runStreamingCommand(node, options, phase, command, args, commandOptions = {}) {
131
+ emitProgress(options, node, phase, `$ ${command} ${args.join(" ")}`);
132
+ return await new Promise((resolvePromise, reject) => {
133
+ const child = spawn(command, args, {
134
+ cwd: commandOptions.cwd ?? node.path,
135
+ env: { ...process.env, ...commandOptions.env ?? {} },
136
+ stdio: ["ignore", "pipe", "pipe"]
137
+ });
138
+ let stdout = "";
139
+ let stderr = "";
140
+ let stdoutRemainder = "";
141
+ let stderrRemainder = "";
142
+ let settled = false;
143
+ const flush = (chunk, stream) => {
144
+ const combined = stream === "stdout" ? stdoutRemainder + chunk : stderrRemainder + chunk;
145
+ const parts = combined.split(/\r?\n/u);
146
+ const complete = parts.slice(0, -1);
147
+ if (stream === "stdout") stdoutRemainder = parts.at(-1) ?? "";
148
+ else stderrRemainder = parts.at(-1) ?? "";
149
+ for (const line of complete) {
150
+ emitProgress(options, node, phase, line, stream);
151
+ }
152
+ };
153
+ const timeout = commandOptions.timeoutMs ? setTimeout(() => {
154
+ if (settled) return;
155
+ settled = true;
156
+ child.kill("SIGTERM");
157
+ reject(new Error(`${progressPrefix(node, phase)} ${command} ${args.join(" ")} timed out after ${commandOptions.timeoutMs}ms`));
158
+ }, commandOptions.timeoutMs) : null;
159
+ child.stdout?.on("data", (chunk) => {
160
+ const text = chunk.toString();
161
+ stdout += text;
162
+ flush(text, "stdout");
163
+ });
164
+ child.stderr?.on("data", (chunk) => {
165
+ const text = chunk.toString();
166
+ stderr += text;
167
+ flush(text, "stderr");
168
+ });
169
+ child.on("error", (error) => {
170
+ if (settled) return;
171
+ settled = true;
172
+ if (timeout) clearTimeout(timeout);
173
+ reject(error);
174
+ });
175
+ child.on("close", (code) => {
176
+ if (settled) return;
177
+ settled = true;
178
+ if (timeout) clearTimeout(timeout);
179
+ if (stdoutRemainder) emitProgress(options, node, phase, stdoutRemainder);
180
+ if (stderrRemainder) emitProgress(options, node, phase, stderrRemainder, "stderr");
181
+ if (code === 0) {
182
+ resolvePromise({ stdout, stderr });
183
+ return;
184
+ }
185
+ reject(new RepositorySaveError(
186
+ prefixedOutput(node, phase, stderr) || prefixedOutput(node, phase, stdout) || `${progressPrefix(node, phase)} ${command} ${args.join(" ")} failed with exit code ${code ?? "unknown"}`,
187
+ {
188
+ details: {
189
+ failingRepo: node.name,
190
+ phase,
191
+ command: `${command} ${args.join(" ")}`
192
+ }
193
+ }
194
+ ));
195
+ });
196
+ });
197
+ }
198
+ function packageScripts(packageJson) {
199
+ const scripts = packageJson?.scripts;
200
+ return scripts && typeof scripts === "object" && !Array.isArray(scripts) ? Object.fromEntries(Object.entries(scripts).map(([key, value]) => [key, String(value)])) : {};
201
+ }
202
+ function classifyRepoKind(packageJson) {
203
+ if (typeof packageJson?.name !== "string" || typeof packageJson?.version !== "string") {
204
+ return "project";
205
+ }
206
+ if (packageJson.private === true) {
207
+ return "project";
208
+ }
209
+ const scripts = packageScripts(packageJson);
210
+ const publishConfig = packageJson.publishConfig;
211
+ return typeof scripts["release:publish"] === "string" || publishConfig !== null && typeof publishConfig === "object" && !Array.isArray(publishConfig) ? "package" : "project";
212
+ }
213
+ function dependencyFields(packageJson) {
214
+ if (!packageJson) return [];
215
+ return ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies"].filter((field) => packageJson[field] && typeof packageJson[field] === "object" && !Array.isArray(packageJson[field]));
216
+ }
217
+ function repoIdForPath(root, repoDir) {
218
+ return relative(root, repoDir).replaceAll("\\", "/") || ".";
219
+ }
220
+ function isGitRepo(repoDir) {
221
+ try {
222
+ run("git", ["rev-parse", "--is-inside-work-tree"], { cwd: repoDir, capture: true });
223
+ return true;
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+ function originRemoteUrlSafe(repoDir) {
229
+ try {
230
+ return originRemoteUrl(repoDir);
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ function ensureWritableRemote(node, options) {
236
+ if (!node.remoteUrl || (options.gitRemoteWriteMode ?? "ssh-pushurl") === "off") return;
237
+ const result = ensureSshPushUrlForOrigin(node.path, node.remoteUrl, options.gitRemoteWriteMode ?? "ssh-pushurl");
238
+ if (result.changed && result.pushUrl) {
239
+ emitProgress(options, node, "remote", `Configured origin push URL ${result.pushUrl}; keeping ${node.remoteUrl} for reads.`);
240
+ }
241
+ }
242
+ function repoDisplayName(repoDir, packageJson) {
243
+ return typeof packageJson?.name === "string" && packageJson.name.length > 0 ? packageJson.name : basename(repoDir);
244
+ }
245
+ function parseGitmodules(root) {
246
+ const gitmodulesPath = resolve(root, ".gitmodules");
247
+ if (!existsSync(gitmodulesPath)) {
248
+ return [];
249
+ }
250
+ const source = readFileSync(gitmodulesPath, "utf8");
251
+ const paths = [];
252
+ for (const match of source.matchAll(/^\s*path\s*=\s*(.+?)\s*$/gmu)) {
253
+ paths.push(match[1].replaceAll("\\", "/"));
254
+ }
255
+ return paths;
256
+ }
257
+ function slugBranch(branch) {
258
+ return branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "dev";
259
+ }
260
+ function timestampLabel(date = /* @__PURE__ */ new Date()) {
261
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/u, "Z");
262
+ }
263
+ function nextDevVersion(version, branch, date = /* @__PURE__ */ new Date()) {
264
+ return `${incrementVersion(version, "patch")}-dev.${slugBranch(branch)}.${timestampLabel(date)}`;
265
+ }
266
+ function escapeRegExp(value) {
267
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
268
+ }
269
+ function isStableSemverVersion(version) {
270
+ return /^\d+\.\d+\.\d+$/u.test(version);
271
+ }
272
+ function isDevVersionForBranch(version, branch) {
273
+ const branchSlug = escapeRegExp(slugBranch(branch));
274
+ return new RegExp(`^\\d+\\.\\d+\\.\\d+-dev\\.${branchSlug}\\.\\d{8}T\\d{6}Z$`, "u").test(version);
275
+ }
276
+ function packageVersionAtHead(node) {
277
+ if (!node.packageJsonPath) return null;
278
+ try {
279
+ const source = run("git", ["show", "HEAD:package.json"], { cwd: node.path, capture: true });
280
+ const packageJson = JSON.parse(source);
281
+ return typeof packageJson.version === "string" ? packageJson.version : null;
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+ function packageVersionEligibleForBranch(node, version, options) {
287
+ return node.branchMode === "package-release-main" ? isStableSemverVersion(version) : isDevVersionForBranch(version, node.branch || options.branch);
288
+ }
289
+ function selectPackageVersion(node, options) {
290
+ const current = String(node.packageJson?.version ?? "0.0.0");
291
+ if (node.branchMode === "package-dev-save" && isDevVersionForBranch(current, node.branch || options.branch) && !tagExists(node.path, current)) {
292
+ return { version: current, reused: true };
293
+ }
294
+ if (node.branchMode === "package-release-main") {
295
+ const headVersion = packageVersionAtHead(node);
296
+ if (headVersion && current === incrementVersion(headVersion, options.bump ?? "patch") && !tagExists(node.path, current)) {
297
+ return { version: current, reused: true };
298
+ }
299
+ }
300
+ return { version: planPackageVersion(node, options), reused: false };
301
+ }
302
+ function createReport(node) {
303
+ return {
304
+ name: node.name,
305
+ path: node.path,
306
+ branch: node.branch,
307
+ dirty: hasMeaningfulChanges(node.path),
308
+ created: false,
309
+ resumed: false,
310
+ merged: false,
311
+ verified: false,
312
+ committed: false,
313
+ pushed: false,
314
+ deletedLocal: false,
315
+ deletedRemote: false,
316
+ tagName: null,
317
+ commitSha: node.branch ? headCommit(node.path) : null,
318
+ skippedReason: null,
319
+ publishWait: null,
320
+ version: typeof node.packageJson?.version === "string" ? node.packageJson.version : null,
321
+ dependencySpec: node.plannedDependencySpec,
322
+ devTagMetadata: null,
323
+ replacedDevTags: [],
324
+ branchMode: node.branchMode,
325
+ verification: null,
326
+ install: null,
327
+ lockfileValidation: null,
328
+ commitMessage: null,
329
+ commitMessageProvider: null,
330
+ commitMessageFallbackUsed: false,
331
+ commitMessageError: null
332
+ };
333
+ }
334
+ function discoverRepositorySaveNodes(root, gitRoot = repoRoot(root), branch = currentBranch(gitRoot), options = {}) {
335
+ const repoDirs = /* @__PURE__ */ new Map();
336
+ repoDirs.set(".", gitRoot);
337
+ if (hasCompleteTreeseedPackageCheckout(root)) {
338
+ for (const pkg of workspacePackages(root)) {
339
+ if (isGitRepo(pkg.dir)) {
340
+ repoDirs.set(pkg.relativeDir, pkg.dir);
341
+ }
342
+ }
343
+ }
344
+ for (const submodulePath of parseGitmodules(root)) {
345
+ const dir = resolve(root, submodulePath);
346
+ if (existsSync(dir) && isGitRepo(dir)) {
347
+ repoDirs.set(submodulePath, dir);
348
+ }
349
+ }
350
+ const nodes = [...repoDirs.entries()].map(([relativePath, repoDir]) => {
351
+ const packageJsonPath = resolve(repoDir, "package.json");
352
+ const packageJson = existsSync(packageJsonPath) ? readJson(packageJsonPath) : null;
353
+ const kind = classifyRepoKind(packageJson);
354
+ const repoBranch = relativePath === "." ? currentBranch(repoDir) || branch || null : branch || currentBranch(repoDir) || null;
355
+ const branchMode = kind === "project" ? "project-save" : options.stablePackageRelease === true && repoBranch === PRODUCTION_BRANCH ? "package-release-main" : "package-dev-save";
356
+ return {
357
+ id: relativePath,
358
+ name: repoDisplayName(repoDir, packageJson),
359
+ path: repoDir,
360
+ relativePath,
361
+ kind,
362
+ branch: repoBranch,
363
+ branchMode,
364
+ packageJsonPath: packageJson ? packageJsonPath : null,
365
+ packageJson,
366
+ scripts: packageScripts(packageJson),
367
+ remoteUrl: originRemoteUrlSafe(repoDir),
368
+ dependencies: [],
369
+ dependents: [],
370
+ submoduleDependencies: [],
371
+ plannedVersion: null,
372
+ plannedTag: null,
373
+ plannedDependencySpec: null
374
+ };
375
+ });
376
+ return deriveRepositoryGraph(root, nodes);
377
+ }
378
+ function deriveRepositoryGraph(root, nodes) {
379
+ const byPackageName = new Map(nodes.filter((node) => node.kind === "package").map((node) => [String(node.packageJson?.name), node]));
380
+ const byId = new Map(nodes.map((node) => [node.id, node]));
381
+ const dependencies = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
382
+ const dependents = new Map(nodes.map((node) => [node.id, /* @__PURE__ */ new Set()]));
383
+ for (const node of nodes) {
384
+ for (const field of dependencyFields(node.packageJson)) {
385
+ const values = node.packageJson?.[field];
386
+ for (const depName of Object.keys(values)) {
387
+ const dependency = byPackageName.get(depName);
388
+ if (!dependency || dependency.id === node.id) continue;
389
+ dependencies.get(node.id)?.add(dependency.id);
390
+ dependents.get(dependency.id)?.add(node.id);
391
+ }
392
+ }
393
+ for (const submodulePath of parseGitmodules(node.path)) {
394
+ const absolute = resolve(node.path, submodulePath);
395
+ const relativeToRoot = repoIdForPath(root, absolute);
396
+ const dependency = byId.get(relativeToRoot);
397
+ if (!dependency || dependency.id === node.id) continue;
398
+ dependencies.get(node.id)?.add(dependency.id);
399
+ dependents.get(dependency.id)?.add(node.id);
400
+ }
401
+ }
402
+ return nodes.map((node) => ({
403
+ ...node,
404
+ dependencies: [...dependencies.get(node.id) ?? []].sort(),
405
+ dependents: [...dependents.get(node.id) ?? []].sort(),
406
+ submoduleDependencies: [...dependencies.get(node.id) ?? []].filter((id) => node.id === "." || id.startsWith(`${node.id}/`)).sort()
407
+ }));
408
+ }
409
+ function repositorySaveWaves(nodes) {
410
+ const nodeIds = new Set(nodes.map((node) => node.id));
411
+ const dependencies = new Map(nodes.map((node) => [node.id, new Set(node.dependencies.filter((id) => nodeIds.has(id)))]));
412
+ const dependents = new Map(nodes.map((node) => [node.id, new Set(node.dependents.filter((id) => nodeIds.has(id)))]));
413
+ const ready = [...nodes].filter((node) => (dependencies.get(node.id)?.size ?? 0) === 0).sort(compareNodes);
414
+ const waves = [];
415
+ const processed = /* @__PURE__ */ new Set();
416
+ while (ready.length > 0) {
417
+ const wave = ready.splice(0).filter((node) => !processed.has(node.id));
418
+ if (wave.length === 0) continue;
419
+ waves.push(wave);
420
+ for (const node of wave) {
421
+ processed.add(node.id);
422
+ for (const dependentId of dependents.get(node.id) ?? []) {
423
+ const remaining = dependencies.get(dependentId);
424
+ remaining?.delete(node.id);
425
+ if (remaining && remaining.size === 0 && !processed.has(dependentId)) {
426
+ const dependent = nodes.find((candidate) => candidate.id === dependentId);
427
+ if (dependent) ready.push(dependent);
428
+ }
429
+ }
430
+ }
431
+ ready.sort(compareNodes);
432
+ }
433
+ if (processed.size !== nodes.length) {
434
+ const unresolved = nodes.filter((node) => !processed.has(node.id)).map((node) => `${node.name} depends on ${(dependencies.get(node.id) ? [...dependencies.get(node.id)] : []).join(", ")}`);
435
+ throw new RepositorySaveError(`Repository dependency cycle detected:
436
+ ${unresolved.join("\n")}`, {
437
+ details: { unresolved }
438
+ });
439
+ }
440
+ return waves;
441
+ }
442
+ function compareNodes(left, right) {
443
+ if (left.id === ".") return 1;
444
+ if (right.id === ".") return -1;
445
+ const sorted = sortWorkspacePackages([
446
+ { name: left.name, relativeDir: left.relativePath, dir: left.path, packageJson: left.packageJson ?? {} },
447
+ { name: right.name, relativeDir: right.relativePath, dir: right.path, packageJson: right.packageJson ?? {} }
448
+ ]);
449
+ return sorted[0]?.name === left.name ? -1 : 1;
450
+ }
451
+ function runLimited(items, limit, action) {
452
+ let index = 0;
453
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
454
+ while (index < items.length) {
455
+ const current = items[index++];
456
+ await action(current);
457
+ }
458
+ });
459
+ return Promise.all(workers);
460
+ }
461
+ function remoteBranchExistsSafe(repoDir, branch) {
462
+ try {
463
+ run("git", ["rev-parse", "--verify", `refs/remotes/origin/${branch}`], { cwd: repoDir, capture: true });
464
+ return true;
465
+ } catch {
466
+ }
467
+ try {
468
+ return remoteBranchExists(repoDir, branch);
469
+ } catch {
470
+ return false;
471
+ }
472
+ }
473
+ function checkoutCommandFor(repoDir, branch) {
474
+ if (currentBranch(repoDir) === branch) return `git checkout ${branch} # already current`;
475
+ if (branchExists(repoDir, branch)) return `git checkout ${branch}`;
476
+ if (remoteBranchExistsSafe(repoDir, branch)) return `git checkout -b ${branch} origin/${branch}`;
477
+ return `git checkout -b ${branch}`;
478
+ }
479
+ function checkoutOrCreateBranch(node, options, branch) {
480
+ if (currentBranch(node.path) === branch) {
481
+ emitProgress(options, node, "branch", `Already on ${branch}.`);
482
+ return;
483
+ }
484
+ if (branchExists(node.path, branch)) {
485
+ runCapturedCommand(node, options, "branch", "git", ["checkout", branch]);
486
+ return;
487
+ }
488
+ if (remoteBranchExistsSafe(node.path, branch)) {
489
+ runCapturedCommand(node, options, "branch", "git", ["checkout", "-b", branch, `origin/${branch}`]);
490
+ return;
491
+ }
492
+ runCapturedCommand(node, options, "branch", "git", ["checkout", "-b", branch]);
493
+ }
494
+ async function commitMessageFor(node, options, context) {
495
+ return generateRepositoryCommitMessage({
496
+ repoName: node.name,
497
+ repoPath: node.path,
498
+ branch: node.branch || options.branch,
499
+ kind: node.kind,
500
+ branchMode: node.branchMode,
501
+ userMessage: options.message?.trim() || void 0,
502
+ ...context
503
+ }, {
504
+ mode: options.commitMessageMode ?? "auto",
505
+ provider: options.commitMessageProvider
506
+ });
507
+ }
508
+ function gitDiffSummary(repoDir) {
509
+ const changedFiles = run("git", ["status", "--porcelain"], { cwd: repoDir, capture: true });
510
+ const diff = run("git", ["diff", "--cached"], { cwd: repoDir, capture: true });
511
+ return { changedFiles, diff };
512
+ }
513
+ function hasStagedChanges(repoDir) {
514
+ try {
515
+ return run("git", ["diff", "--cached", "--name-only"], { cwd: repoDir, capture: true }).trim().length > 0;
516
+ } catch {
517
+ return false;
518
+ }
519
+ }
520
+ function updateDependencyReferences(node, finalizedReferences) {
521
+ if (!node.packageJson || !node.packageJsonPath) return [];
522
+ const changed = updateInternalDependencySpecs(node.packageJson, finalizedReferences);
523
+ if (changed.length > 0) {
524
+ writeJson(node.packageJsonPath, node.packageJson);
525
+ }
526
+ return changed;
527
+ }
528
+ function planPackageVersion(node, options) {
529
+ if (!node.packageJson || !node.packageJsonPath) return null;
530
+ const current = String(node.packageJson.version ?? "0.0.0");
531
+ return node.branchMode === "package-release-main" ? incrementVersion(current, options.bump ?? "patch") : nextDevVersion(current, options.branch);
532
+ }
533
+ function applyPackageVersion(node, version) {
534
+ if (!node.packageJson || !node.packageJsonPath) return false;
535
+ if (node.packageJson.version === version) return false;
536
+ node.packageJson.version = version;
537
+ writeJson(node.packageJsonPath, node.packageJson);
538
+ return true;
539
+ }
540
+ function shouldSkipNetworkInstall() {
541
+ return getGitHubAutomationMode() === "stub" || process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
542
+ }
543
+ function shouldSkipGitDependencySmoke() {
544
+ return shouldSkipNetworkInstall() || process.env.TREESEED_GIT_DEPENDENCY_SMOKE === "skip";
545
+ }
546
+ function hasNpmLockfile(repoDir) {
547
+ return existsSync(resolve(repoDir, "package-lock.json")) || existsSync(resolve(repoDir, "npm-shrinkwrap.json"));
548
+ }
549
+ async function runGitDependencySmoke(node, options, reference) {
550
+ if (reference.mode !== "dev-git-tag" || shouldSkipGitDependencySmoke()) return;
551
+ const installSpec = reference.installSpec ?? reference.spec;
552
+ const tempRoot = mkdtempSync(resolve(tmpdir(), "treeseed-git-dep-smoke-"));
553
+ const npmCacheRoot = resolve(tempRoot, ".npm-cache");
554
+ try {
555
+ emitProgress(options, node, "smoke", `Installing ${installSpec} in a temporary project.`);
556
+ writeFileSync(resolve(tempRoot, "package.json"), JSON.stringify({
557
+ name: "treeseed-git-dependency-smoke",
558
+ version: "0.0.0",
559
+ private: true,
560
+ type: "module",
561
+ dependencies: {
562
+ [reference.packageName]: installSpec
563
+ }
564
+ }, null, 2), "utf8");
565
+ let lastError = null;
566
+ for (let attempt = 1; attempt <= 5; attempt += 1) {
567
+ emitProgress(options, node, "smoke", `npm install --cache ${npmCacheRoot} attempt ${attempt}/5.`);
568
+ try {
569
+ await runStreamingCommand(node, options, "smoke", "npm", ["install", "--cache", npmCacheRoot], { cwd: tempRoot });
570
+ return;
571
+ } catch (error) {
572
+ lastError = error instanceof Error ? error.message : String(error);
573
+ }
574
+ if (attempt < 5) {
575
+ emitProgress(options, node, "smoke", "npm install failed; retrying in 60 seconds.", "stderr");
576
+ spawnSync("sleep", ["60"], { stdio: "ignore" });
577
+ }
578
+ }
579
+ throw new RepositorySaveError([
580
+ `Git dependency smoke install failed for ${reference.packageName} after 5 attempts.`,
581
+ `Spec: ${installSpec}`,
582
+ lastError ?? ""
583
+ ].join("\n"));
584
+ } finally {
585
+ rmSync(tempRoot, { recursive: true, force: true });
586
+ }
587
+ }
588
+ async function runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs = []) {
589
+ if (shouldSkipNetworkInstall()) {
590
+ emitProgress(options, node, "install", "Skipped npm install because network install mode is disabled.");
591
+ return { status: "skipped", attempts: 0, reason: "stubbed" };
592
+ }
593
+ let lastError = null;
594
+ const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
595
+ const rootWorkspaceInstall = node.path === options.root && Array.isArray(packageJson?.workspaces);
596
+ const args = rootWorkspaceInstall ? gitDependencyRefreshSpecs.length > 0 ? ["install", ...gitDependencyRefreshSpecs, "--force"] : ["install"] : gitDependencyRefreshSpecs.length > 0 ? ["install", ...gitDependencyRefreshSpecs, "--force", "--workspaces=false"] : ["install", "--workspaces=false"];
597
+ for (let attempt = 1; attempt <= 5; attempt += 1) {
598
+ emitProgress(options, node, "install", `npm ${args.join(" ")} attempt ${attempt}/5.`);
599
+ try {
600
+ await runStreamingCommand(node, options, "install", "npm", args);
601
+ return { status: "completed", attempts: attempt, reason: null };
602
+ } catch (error) {
603
+ lastError = error instanceof Error ? error.message : String(error);
604
+ }
605
+ if (attempt < 5) {
606
+ emitProgress(options, node, "install", "npm install failed; retrying in 60 seconds.", "stderr");
607
+ spawnSync("sleep", ["60"], { stdio: "ignore" });
608
+ }
609
+ }
610
+ throw new RepositorySaveError(`npm install failed after 5 attempts.
611
+ ${lastError ?? ""}`);
612
+ }
613
+ function lockfileValidationCommand(node, options) {
614
+ const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
615
+ const rootWorkspaceInstall = node.path === options.root && Array.isArray(packageJson?.workspaces);
616
+ const args = rootWorkspaceInstall ? ["ci", "--ignore-scripts", "--dry-run"] : ["ci", "--ignore-scripts", "--dry-run", "--workspaces=false"];
617
+ return { command: "npm", args };
618
+ }
619
+ async function validateRepositoryLockfile(node, options) {
620
+ if (!hasNpmLockfile(node.path)) {
621
+ return { status: "skipped", command: null, issues: [], error: "no npm lockfile" };
622
+ }
623
+ const issues = collectDeploymentLockfileWorkspaceIssues(node.path).map((issue) => `${issue.filePath}: ${issue.packageName} ${issue.reason}`);
624
+ if (issues.length > 0) {
625
+ throw new RepositorySaveError([
626
+ `Lockfile validation failed for ${node.name}.`,
627
+ ...issues
628
+ ].join("\n"), {
629
+ details: {
630
+ failingRepo: node.name,
631
+ phase: "lockfile",
632
+ issues
633
+ }
634
+ });
635
+ }
636
+ const { command, args } = lockfileValidationCommand(node, options);
637
+ const commandText = `${command} ${args.join(" ")}`;
638
+ if (shouldSkipNetworkInstall()) {
639
+ emitProgress(options, node, "lockfile", `Skipped ${commandText} because network install mode is disabled.`);
640
+ return { status: "skipped", command: commandText, issues: [], error: "stubbed" };
641
+ }
642
+ try {
643
+ runCapturedCommand(node, options, "lockfile", command, args, { timeoutMs: 12e4 });
644
+ return { status: "passed", command: commandText, issues: [], error: null };
645
+ } catch (error) {
646
+ const message = error instanceof Error ? error.message : String(error);
647
+ const result = { status: "failed", command: commandText, issues: [message], error: message };
648
+ throw new RepositorySaveError([
649
+ `Lockfile validation failed for ${node.name}.`,
650
+ `Command: ${commandText}`,
651
+ message
652
+ ].join("\n"), {
653
+ details: {
654
+ failingRepo: node.name,
655
+ phase: "lockfile",
656
+ command: commandText,
657
+ issues: result.issues
658
+ }
659
+ });
660
+ }
661
+ }
662
+ function hasScript(node, scriptName) {
663
+ return typeof node.scripts[scriptName] === "string" && node.scripts[scriptName].length > 0;
664
+ }
665
+ async function runScript(node, options, scriptName) {
666
+ await runStreamingCommand(node, options, "verify", "npm", ["run", scriptName]);
667
+ }
668
+ async function runRepoVerification(node, options, verifyMode) {
669
+ if (verifyMode === "skip" || getGitHubAutomationMode() === "stub") {
670
+ emitProgress(options, node, "verify", getGitHubAutomationMode() === "stub" ? "Skipped verification in stub automation mode." : "Skipped verification by request.");
671
+ return { mode: verifyMode, status: "skipped", primary: null, fallbackUsed: false, error: getGitHubAutomationMode() === "stub" ? "stubbed" : null };
672
+ }
673
+ if (node.kind !== "package") {
674
+ emitProgress(options, node, "verify", "Skipped package verification for project repository.");
675
+ return { mode: verifyMode, status: "skipped", primary: null, fallbackUsed: false, error: null };
676
+ }
677
+ if (verifyMode === "local-only") {
678
+ if (!hasScript(node, "verify:local")) {
679
+ throw new RepositorySaveError(`Package ${node.name} is missing required verify:local script.`);
680
+ }
681
+ await runScript(node, options, "verify:local");
682
+ return { mode: verifyMode, status: "passed", primary: "verify:local", fallbackUsed: false, error: null };
683
+ }
684
+ if (!hasScript(node, "verify:action") && !hasScript(node, "verify:local")) {
685
+ throw new RepositorySaveError(`Package ${node.name} is missing required verify:action or verify:local script.`);
686
+ }
687
+ if (hasScript(node, "verify:action")) {
688
+ try {
689
+ await runScript(node, options, "verify:action");
690
+ return { mode: verifyMode, status: "passed", primary: "verify:action", fallbackUsed: false, error: null };
691
+ } catch (error) {
692
+ if (!hasScript(node, "verify:local")) {
693
+ throw error;
694
+ }
695
+ emitProgress(options, node, "verify", "verify:action failed; falling back to verify:local.", "stderr");
696
+ await runScript(node, options, "verify:local");
697
+ return {
698
+ mode: verifyMode,
699
+ status: "passed",
700
+ primary: "verify:action",
701
+ fallbackUsed: true,
702
+ error: error instanceof Error ? error.message : String(error)
703
+ };
704
+ }
705
+ }
706
+ await runScript(node, options, "verify:local");
707
+ return { mode: verifyMode, status: "passed", primary: "verify:local", fallbackUsed: true, error: null };
708
+ }
709
+ function pullRebaseFromOrigin(node, options, branch) {
710
+ if (!remoteBranchExistsSafe(node.path, branch)) {
711
+ emitProgress(options, node, "rebase", `Skipped pull --rebase because origin/${branch} does not exist.`);
712
+ return {
713
+ remoteBranchExisted: false,
714
+ pulledRebase: false
715
+ };
716
+ }
717
+ try {
718
+ runCapturedCommand(node, options, "rebase", "git", ["pull", "--rebase", "--recurse-submodules=no", "origin", branch]);
719
+ return {
720
+ remoteBranchExisted: true,
721
+ pulledRebase: true
722
+ };
723
+ } catch (error) {
724
+ const report = collectMergeConflictReport(node.path);
725
+ throw new RepositorySaveError(formatMergeConflictReport(report, node.path, branch), {
726
+ exitCode: 12,
727
+ details: { branch, report, originalError: error instanceof Error ? error.message : String(error) }
728
+ });
729
+ }
730
+ }
731
+ function pushCurrentBranch(node, options, branch, tagName) {
732
+ ensureWritableRemote(node, options);
733
+ const remoteBranchExists2 = remoteBranchExistsSafe(node.path, branch);
734
+ let pushedTag = false;
735
+ const args = remoteBranchExists2 ? ["push", "origin", branch] : ["push", "-u", "origin", branch];
736
+ if (tagName) {
737
+ const state = tagState(node.path, tagName);
738
+ assertTagStateMatchesHead(node, tagName, state, headCommit(node.path));
739
+ if (!state.remoteExists) {
740
+ args.push(tagName);
741
+ pushedTag = true;
742
+ }
743
+ }
744
+ runCapturedCommand(node, options, "push", "git", args);
745
+ return {
746
+ createdRemoteBranch: !remoteBranchExists2,
747
+ pushed: true,
748
+ pushedTag,
749
+ combinedBranchAndTagPush: pushedTag
750
+ };
751
+ }
752
+ function tagExists(repoDir, tagName) {
753
+ try {
754
+ run("git", ["rev-parse", "--verify", `refs/tags/${tagName}`], { cwd: repoDir, capture: true });
755
+ return true;
756
+ } catch {
757
+ return false;
758
+ }
759
+ }
760
+ function localTagCommit(repoDir, tagName) {
761
+ try {
762
+ return run("git", ["rev-list", "-n", "1", tagName], { cwd: repoDir, capture: true }).trim();
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+ function remoteTagCommit(repoDir, tagName) {
768
+ try {
769
+ const output = run("git", ["ls-remote", "--tags", "origin", `refs/tags/${tagName}*`], { cwd: repoDir, capture: true });
770
+ const lines = output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
771
+ const dereferenced = lines.find((line) => line.endsWith(`refs/tags/${tagName}^{}`));
772
+ const exact = lines.find((line) => line.endsWith(`refs/tags/${tagName}`));
773
+ const selected = dereferenced ?? exact;
774
+ return selected ? selected.split(/\s+/u)[0] ?? null : null;
775
+ } catch {
776
+ return null;
777
+ }
778
+ }
779
+ function localTagMessage(repoDir, tagName) {
780
+ try {
781
+ return run("git", ["tag", "-l", tagName, "--format=%(contents)"], { cwd: repoDir, capture: true }).trim();
782
+ } catch {
783
+ return null;
784
+ }
785
+ }
786
+ function tagState(repoDir, tagName) {
787
+ const localCommit = localTagCommit(repoDir, tagName);
788
+ const remoteCommit = remoteTagCommit(repoDir, tagName);
789
+ return {
790
+ tagName,
791
+ localExists: localCommit != null,
792
+ localCommit,
793
+ remoteExists: remoteCommit != null,
794
+ remoteCommit
795
+ };
796
+ }
797
+ function assertTagStateMatchesHead(node, tagName, state, head) {
798
+ if (state.localCommit && state.localCommit !== head) {
799
+ throw new RepositorySaveError(`Package ${node.name} tag ${tagName} points to ${state.localCommit.slice(0, 12)}, but ${node.name} HEAD is ${head.slice(0, 12)}. Refusing to move an existing tag.`, {
800
+ details: {
801
+ failingRepo: node.name,
802
+ phase: "tag",
803
+ currentVersion: tagName,
804
+ expectedTag: tagName,
805
+ tagState: state
806
+ }
807
+ });
808
+ }
809
+ if (state.remoteCommit && state.remoteCommit !== head) {
810
+ throw new RepositorySaveError(`Remote tag ${tagName} for ${node.name} points to ${state.remoteCommit.slice(0, 12)}, but ${node.name} HEAD is ${head.slice(0, 12)}. Refusing to move an existing tag.`, {
811
+ details: {
812
+ failingRepo: node.name,
813
+ phase: "tag",
814
+ currentVersion: tagName,
815
+ expectedTag: tagName,
816
+ tagState: state
817
+ }
818
+ });
819
+ }
820
+ }
821
+ function createPackageTagMessage(node, tagName, branch, workflowRunId) {
822
+ return tagName.includes("-dev.") ? createDevTagMessage({
823
+ packageName: node.name,
824
+ version: tagName,
825
+ branch,
826
+ commitSha: headCommit(node.path),
827
+ workflowRunId
828
+ }) : `release: ${tagName}`;
829
+ }
830
+ function ensureRemoteAccessBeforeVerification(node, options, state) {
831
+ if (shouldSkipRemoteAccessPreflight()) return;
832
+ if (state.remoteAccessChecked.has(node.path)) return;
833
+ ensureWritableRemote(node, options);
834
+ const writeUrl = remoteWriteUrl(node.path) ?? "origin";
835
+ emitProgress(options, node, "preflight", `Checking write remote access before verification (${writeUrl}).`);
836
+ try {
837
+ runQuietCommand(node, "preflight", "git", ["ls-remote", "--heads", writeUrl], { timeoutMs: 3e4 });
838
+ state.remoteAccessChecked.add(node.path);
839
+ } catch (error) {
840
+ const detail = error instanceof Error ? error.message : String(error);
841
+ throw new RepositorySaveError([
842
+ `Cannot access origin remote for ${node.name}; save would fail after verification when pushing branch or tags.`,
843
+ "Fix Git authentication, then rerun `npx trsd save` to resume.",
844
+ detail
845
+ ].join("\n"), {
846
+ exitCode: 13,
847
+ details: {
848
+ failingRepo: node.name,
849
+ phase: "preflight",
850
+ originalError: detail
851
+ }
852
+ });
853
+ }
854
+ }
855
+ function shouldSkipRemoteAccessPreflight() {
856
+ return getGitHubAutomationMode() === "stub" || process.env.TREESEED_SAVE_REMOTE_PREFLIGHT === "skip";
857
+ }
858
+ function localTreeseedTagWasCreatedByThisRun(node, tagName, workflowRunId) {
859
+ const message = localTagMessage(node.path, tagName);
860
+ if (!message?.includes("treeseed-dev-tag: true")) return false;
861
+ if (!workflowRunId) return true;
862
+ return message.includes(`workflowRunId: ${workflowRunId}`);
863
+ }
864
+ function ensurePackageTagReady(node, options, tagName, branch, workflowRunId) {
865
+ let message = null;
866
+ ensureWritableRemote(node, options);
867
+ const head = headCommit(node.path);
868
+ let state = tagState(node.path, tagName);
869
+ assertTagStateMatchesHead(node, tagName, state, head);
870
+ if (!state.localExists && state.remoteExists && state.remoteCommit === head) {
871
+ runCapturedCommand(node, options, "tag", "git", ["fetch", "origin", `refs/tags/${tagName}:refs/tags/${tagName}`]);
872
+ state = tagState(node.path, tagName);
873
+ assertTagStateMatchesHead(node, tagName, state, head);
874
+ }
875
+ if (!state.localExists) {
876
+ message = createPackageTagMessage(node, tagName, branch, workflowRunId);
877
+ runCapturedCommand(node, options, "tag", "git", ["tag", "-a", tagName, "-m", message]);
878
+ state = tagState(node.path, tagName);
879
+ assertTagStateMatchesHead(node, tagName, state, head);
880
+ }
881
+ if (state.remoteExists) {
882
+ emitProgress(options, node, "tag", `Remote tag ${tagName} already points at HEAD.`);
883
+ }
884
+ return message;
885
+ }
886
+ function refreshSubmodulePointers(node, finalizedCommits) {
887
+ let changed = false;
888
+ for (const [repoName] of finalizedCommits.entries()) {
889
+ const childRelativePath = node.id === "." ? repoName : repoName.startsWith(`${node.id}/`) ? repoName.slice(node.id.length + 1) : null;
890
+ if (!childRelativePath) continue;
891
+ const childPath = resolve(node.path, childRelativePath);
892
+ if (!existsSync(childPath) || !isGitRepo(childPath)) continue;
893
+ const status = run("git", ["status", "--porcelain", "--", childRelativePath], { cwd: node.path, capture: true });
894
+ if (status.trim().length > 0) {
895
+ changed = true;
896
+ }
897
+ }
898
+ return changed;
899
+ }
900
+ function syncBranchBeforeSave(node, options, branch) {
901
+ checkoutOrCreateBranch(node, options, branch);
902
+ }
903
+ function refreshRepositoryNodePackageMetadata(node) {
904
+ const packageJsonPath = resolve(node.path, "package.json");
905
+ const packageJson = existsSync(packageJsonPath) ? readJson(packageJsonPath) : null;
906
+ node.packageJsonPath = packageJson ? packageJsonPath : null;
907
+ node.packageJson = packageJson;
908
+ node.scripts = packageScripts(packageJson);
909
+ node.remoteUrl = originRemoteUrlSafe(node.path);
910
+ if (node.kind === "package") {
911
+ node.name = repoDisplayName(node.path, packageJson);
912
+ }
913
+ }
914
+ function finalizePackageReference(node, version, options) {
915
+ const reference = createPackageDependencyReference({
916
+ packageName: node.name,
917
+ version,
918
+ branchMode: node.branchMode === "package-release-main" ? "package-release-main" : "package-dev-save",
919
+ remoteUrl: node.remoteUrl,
920
+ devDependencyReferenceMode: options.devDependencyReferenceMode ?? "git-tag",
921
+ gitDependencyProtocol: options.gitDependencyProtocol ?? "preserve-origin"
922
+ });
923
+ node.plannedDependencySpec = reference.spec;
924
+ return reference;
925
+ }
926
+ async function finalizeCleanPackageVersion(node, options, state, report, branch) {
927
+ const version = typeof node.packageJson?.version === "string" ? node.packageJson.version : null;
928
+ if (!version || !packageVersionEligibleForBranch(node, version, options)) {
929
+ return false;
930
+ }
931
+ const head = headCommit(node.path);
932
+ const currentTagState = tagState(node.path, version);
933
+ assertTagStateMatchesHead(node, version, currentTagState, head);
934
+ const remoteBranchExists2 = remoteBranchExistsSafe(node.path, branch);
935
+ const finalizedRemotely = currentTagState.localCommit === head && currentTagState.remoteCommit === head && remoteBranchExists2;
936
+ report.version = version;
937
+ report.tagName = version;
938
+ report.commitSha = head;
939
+ report.dependencySpec = finalizePackageReference(node, version, options).spec;
940
+ state.finalizedVersions.set(node.name, version);
941
+ state.finalizedReferences.set(node.name, finalizePackageReference(node, version, options));
942
+ state.finalizedCommits.set(node.relativePath, head);
943
+ if (finalizedRemotely) {
944
+ report.pushed = true;
945
+ report.skippedReason = "already-finalized";
946
+ report.publishWait = { recoveredPartialSave: true, remoteBranchExisted: true, tagAlreadyPushed: true };
947
+ emitProgress(options, node, "finalize", `Using existing finalized package version ${version}.`);
948
+ return true;
949
+ }
950
+ emitProgress(options, node, "finalize", `Finalizing interrupted package version ${version}.`);
951
+ if (hasNpmLockfile(node.path)) {
952
+ report.install = await runNpmInstallWithRetry(node, options);
953
+ report.lockfileValidation = await validateRepositoryLockfile(node, options);
954
+ }
955
+ const rebase = pullRebaseFromOrigin(node, options, branch);
956
+ if (currentTagState.localCommit === head && localTreeseedTagWasCreatedByThisRun(node, version, options.workflowRunId)) {
957
+ emitProgress(options, node, "verify", `Reusing verification from interrupted tag ${version}.`);
958
+ report.verification = {
959
+ mode: options.verifyMode ?? "action-first",
960
+ status: "skipped",
961
+ primary: null,
962
+ fallbackUsed: false,
963
+ error: "verified-before-interruption"
964
+ };
965
+ report.verified = true;
966
+ } else {
967
+ ensureRemoteAccessBeforeVerification(node, options, state);
968
+ report.verification = await runRepoVerification(node, options, options.verifyMode ?? "action-first");
969
+ report.verified = report.verification.status === "passed";
970
+ }
971
+ const tagMessage = ensurePackageTagReady(node, options, version, branch, options.workflowRunId);
972
+ report.devTagMetadata = tagMessage?.includes("treeseed-dev-tag: true") ? tagMessage : null;
973
+ const reference = finalizePackageReference(node, version, options);
974
+ const push = pushCurrentBranch(node, options, branch, version);
975
+ await runGitDependencySmoke(node, options, reference);
976
+ report.dependencySpec = reference.spec;
977
+ report.pushed = push.pushed;
978
+ report.skippedReason = "finalized-partial-save";
979
+ report.commitSha = headCommit(node.path);
980
+ state.finalizedCommits.set(node.relativePath, report.commitSha);
981
+ report.publishWait = {
982
+ ...rebase,
983
+ ...push,
984
+ recoveredPartialSave: true
985
+ };
986
+ return true;
987
+ }
988
+ function branchModeLabel(branchMode) {
989
+ switch (branchMode) {
990
+ case "package-release-main":
991
+ return "stable package release";
992
+ case "package-dev-save":
993
+ return "dev package save";
994
+ case "project-save":
995
+ return "project save";
996
+ }
997
+ }
998
+ function repoPlanCommands(node, options, plannedVersion, plannedDependencySpec, dependencyUpdates) {
999
+ const branch = node.branch || options.branch;
1000
+ const remoteExists = remoteBranchExistsSafe(node.path, branch);
1001
+ const commands = [
1002
+ checkoutCommandFor(node.path, branch)
1003
+ ];
1004
+ const sshPushUrl = (options.gitRemoteWriteMode ?? "ssh-pushurl") === "off" ? null : sshPushUrlForRemote(node.remoteUrl);
1005
+ if (sshPushUrl) {
1006
+ commands.push(`git remote set-url --push origin ${sshPushUrl} # keep ${node.remoteUrl} for reads`);
1007
+ }
1008
+ if (dependencyUpdates.length > 0) {
1009
+ commands.push(`update package.json internal dependencies: ${dependencyUpdates.join(", ")}`);
1010
+ }
1011
+ if (node.submoduleDependencies.length > 0) {
1012
+ commands.push(`refresh submodule pointers: ${node.submoduleDependencies.join(", ")}`);
1013
+ }
1014
+ const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
1015
+ const rootWorkspaceInstall = node.path === options.root && Array.isArray(packageJson?.workspaces);
1016
+ if (node.kind === "package" && plannedVersion) {
1017
+ commands.push(`update package.json version to ${plannedVersion}`);
1018
+ commands.push("npm install --workspaces=false # explicitly refresh changed git-tag dependencies with --force; retry up to 5 times with 60s delay");
1019
+ } else if (node.kind === "project" && dependencyUpdates.length > 0 && hasNpmLockfile(node.path)) {
1020
+ commands.push(rootWorkspaceInstall ? "npm install # refresh root workspace lockfile against the real checked-in manifest" : "npm install --workspaces=false # refresh project lockfile after internal dependency updates");
1021
+ }
1022
+ if (hasNpmLockfile(node.path) && (node.kind === "project" || plannedVersion || dependencyUpdates.length > 0 || node.submoduleDependencies.length > 0)) {
1023
+ commands.push(rootWorkspaceInstall ? "npm ci --ignore-scripts --dry-run # validate root manifest, workspaces, and lockfile before commit" : "npm ci --ignore-scripts --dry-run --workspaces=false # validate deployment lockfile before commit");
1024
+ }
1025
+ commands.push("git add -A");
1026
+ commands.push("generate commit message # Cloudflare AI when configured, fallback otherwise");
1027
+ commands.push("git commit -m <generated-message>");
1028
+ commands.push(
1029
+ remoteExists ? `git pull --rebase --recurse-submodules=no origin ${branch}` : `skip pull --rebase # origin/${branch} does not exist yet`
1030
+ );
1031
+ if (node.kind === "package") {
1032
+ const verifyMode = options.verifyMode ?? "action-first";
1033
+ if (verifyMode === "skip") {
1034
+ commands.push("skip package verification");
1035
+ } else if (verifyMode === "local-only") {
1036
+ commands.push("npm run verify:local");
1037
+ } else {
1038
+ commands.push("npm run verify:action # fallback to npm run verify:local on failure");
1039
+ }
1040
+ if (plannedVersion) {
1041
+ commands.push(`git tag -a ${plannedVersion} -m <${plannedVersion.includes("-dev.") ? "dev metadata" : "release"}>`);
1042
+ commands.push(remoteExists ? `git push origin ${branch} ${plannedVersion}` : `git push -u origin ${branch} ${plannedVersion}`);
1043
+ if (plannedDependencySpec && node.branchMode === "package-dev-save") {
1044
+ commands.push(`smoke install ${plannedDependencySpec}`);
1045
+ }
1046
+ }
1047
+ } else {
1048
+ commands.push("skip package verification # project repository");
1049
+ commands.push(remoteExists ? `git push origin ${branch}` : `git push -u origin ${branch}`);
1050
+ }
1051
+ return commands;
1052
+ }
1053
+ function planRepositorySave(options) {
1054
+ const scope = options.branch === STAGING_BRANCH ? "staging" : options.branch === PRODUCTION_BRANCH ? "prod" : "local";
1055
+ const allNodes = discoverRepositorySaveNodes(options.root, options.gitRoot, options.branch, {
1056
+ stablePackageRelease: options.stablePackageRelease === true
1057
+ });
1058
+ const nodes = options.includeRoot === false ? allNodes.filter((node) => node.id !== ".") : allNodes;
1059
+ const mode = nodes.some((node) => node.id !== ".") ? "recursive-workspace" : "root-only";
1060
+ const waves = repositorySaveWaves(nodes);
1061
+ const plannedVersions = /* @__PURE__ */ new Map();
1062
+ const plannedReferences = /* @__PURE__ */ new Map();
1063
+ const plans = /* @__PURE__ */ new Map();
1064
+ for (const wave of waves) {
1065
+ for (const node of wave) {
1066
+ const dependencyUpdates = node.dependencies.map((id) => nodes.find((candidate) => candidate.id === id)).filter((candidate) => Boolean(candidate)).map((dependency) => {
1067
+ const reference = plannedReferences.get(dependency.name);
1068
+ return reference ? `${dependency.name} -> ${reference.spec}` : null;
1069
+ }).filter((value) => Boolean(value));
1070
+ const dependencyChanged = dependencyUpdates.length > 0;
1071
+ const submoduleChanged = node.submoduleDependencies.length > 0 && node.submoduleDependencies.some((id) => {
1072
+ const dependency = plans.get(id);
1073
+ return dependency?.dirty || Boolean(dependency?.plannedVersion);
1074
+ });
1075
+ const dirty = hasMeaningfulChanges(node.path);
1076
+ const packageNeedsVersion = node.kind === "package" && (dirty || dependencyChanged || submoduleChanged);
1077
+ const currentVersion = typeof node.packageJson?.version === "string" ? node.packageJson.version : null;
1078
+ const plannedVersion = packageNeedsVersion ? selectPackageVersion(node, options).version : null;
1079
+ let plannedDependencySpec = null;
1080
+ if (node.kind === "package" && plannedVersion) {
1081
+ const reference = createPackageDependencyReference({
1082
+ packageName: node.name,
1083
+ version: plannedVersion,
1084
+ branchMode: node.branchMode === "package-release-main" ? "package-release-main" : "package-dev-save",
1085
+ remoteUrl: node.remoteUrl,
1086
+ devDependencyReferenceMode: options.devDependencyReferenceMode ?? "git-tag",
1087
+ gitDependencyProtocol: options.gitDependencyProtocol ?? "preserve-origin"
1088
+ });
1089
+ plannedDependencySpec = reference.spec;
1090
+ plannedVersions.set(node.name, plannedVersion);
1091
+ plannedReferences.set(node.name, reference);
1092
+ }
1093
+ const current = currentBranch(node.path) || null;
1094
+ const branch = node.branch || options.branch;
1095
+ const notes = [
1096
+ `${branchModeLabel(node.branchMode)} on top-level ${options.branch}`,
1097
+ ...current && current !== branch ? [`current branch ${current} will be switched to ${branch}`] : [],
1098
+ ...node.kind === "package" && plannedVersion?.includes("-dev.") ? ["dev Git tag only; publish workflows reject prerelease/dev tags"] : []
1099
+ ];
1100
+ const repoPlan = {
1101
+ id: node.id,
1102
+ name: node.name,
1103
+ path: node.path,
1104
+ relativePath: node.relativePath,
1105
+ kind: node.kind,
1106
+ currentBranch: current,
1107
+ targetBranch: branch,
1108
+ branchMode: node.branchMode,
1109
+ dirty,
1110
+ dependencies: node.dependencies,
1111
+ dependents: node.dependents,
1112
+ submoduleDependencies: node.submoduleDependencies,
1113
+ currentVersion,
1114
+ plannedVersion,
1115
+ plannedTag: plannedVersion,
1116
+ plannedDependencySpec,
1117
+ remoteUrl: node.remoteUrl,
1118
+ commands: repoPlanCommands(node, options, plannedVersion, plannedDependencySpec, dependencyUpdates),
1119
+ notes
1120
+ };
1121
+ plans.set(node.id, repoPlan);
1122
+ }
1123
+ }
1124
+ const rootNode = nodes.find((node) => node.id === ".") ?? allNodes.find((node) => node.id === ".");
1125
+ const rootRepo = rootNode ? plans.get(rootNode.id) : null;
1126
+ if (!rootRepo) {
1127
+ throw new RepositorySaveError("Unable to build repository save plan for root repository.");
1128
+ }
1129
+ const repoPlans = nodes.filter((node) => node.id !== ".").sort(compareNodes).map((node) => plans.get(node.id)).filter((plan) => Boolean(plan));
1130
+ const wavePlans = waves.map((wave, index) => ({
1131
+ index: index + 1,
1132
+ parallel: wave.length > 1,
1133
+ repos: wave.map((node) => node.name),
1134
+ commands: wave.map((node) => ({
1135
+ repo: node.name,
1136
+ commands: plans.get(node.id)?.commands ?? []
1137
+ }))
1138
+ }));
1139
+ return {
1140
+ mode,
1141
+ branch: options.branch,
1142
+ scope,
1143
+ devDependencyReferenceMode: options.devDependencyReferenceMode ?? "git-tag",
1144
+ gitDependencyProtocol: options.gitDependencyProtocol ?? "preserve-origin",
1145
+ verifyMode: options.verifyMode ?? "action-first",
1146
+ commitMessageMode: options.commitMessageMode ?? "auto",
1147
+ repos: repoPlans,
1148
+ rootRepo,
1149
+ waves: wavePlans,
1150
+ plannedVersions: Object.fromEntries(plannedVersions.entries()),
1151
+ plannedSteps: wavePlans.flatMap((wave) => wave.commands.map((entry) => ({
1152
+ id: `wave-${wave.index}-${entry.repo}`,
1153
+ description: `Wave ${wave.index}${wave.parallel ? " parallel" : ""}: ${entry.repo}`
1154
+ })))
1155
+ };
1156
+ }
1157
+ async function refreshAndValidateRootWorkspaceLockfileForSave(options) {
1158
+ const repoDir = options.gitRoot ?? options.root;
1159
+ const packageJsonPath = resolve(repoDir, "package.json");
1160
+ const packageJson = existsSync(packageJsonPath) ? readJson(packageJsonPath) : null;
1161
+ const node = {
1162
+ id: ".",
1163
+ name: repoDisplayName(repoDir, packageJson),
1164
+ path: repoDir,
1165
+ relativePath: ".",
1166
+ kind: "project",
1167
+ branch: options.branch ?? currentBranch(repoDir) ?? null,
1168
+ branchMode: "project-save",
1169
+ packageJsonPath: packageJson ? packageJsonPath : null,
1170
+ packageJson,
1171
+ scripts: packageScripts(packageJson),
1172
+ remoteUrl: originRemoteUrlSafe(repoDir),
1173
+ dependencies: [],
1174
+ dependents: [],
1175
+ submoduleDependencies: [],
1176
+ plannedVersion: null,
1177
+ plannedTag: null,
1178
+ plannedDependencySpec: null
1179
+ };
1180
+ if (!hasNpmLockfile(repoDir)) {
1181
+ return {
1182
+ install: null,
1183
+ lockfileValidation: { status: "skipped", command: null, issues: [], error: "no npm lockfile" }
1184
+ };
1185
+ }
1186
+ const install = await runNpmInstallWithRetry(node, { root: options.root, onProgress: options.onProgress });
1187
+ const lockfileValidation = await validateRepositoryLockfile(node, { root: options.root, onProgress: options.onProgress });
1188
+ return { install, lockfileValidation };
1189
+ }
1190
+ async function saveOneRepository(node, options, state) {
1191
+ const report = state.reports.get(node.id) ?? createReport(node);
1192
+ state.reports.set(node.id, report);
1193
+ const branch = node.branch || options.branch;
1194
+ emitProgress(options, node, "start", `Starting ${node.branchMode} on ${branch}.`);
1195
+ syncBranchBeforeSave(node, options, branch);
1196
+ node.branch = currentBranch(node.path) || branch;
1197
+ report.branch = node.branch;
1198
+ refreshRepositoryNodePackageMetadata(node);
1199
+ ensureWritableRemote(node, options);
1200
+ const dependencyUpdates = updateDependencyReferences(node, state.finalizedReferences);
1201
+ const dependencyChanged = dependencyUpdates.length > 0;
1202
+ const gitDependencyRefreshSpecs = dependencyUpdates.map((update) => state.finalizedReferences.get(update.packageName)).filter((reference) => Boolean(reference) && reference.mode === "dev-git-tag").map((reference) => `${reference.packageName}@${reference.installSpec ?? reference.spec}`);
1203
+ const submodulesChanged = refreshSubmodulePointers(node, state.finalizedCommits);
1204
+ const packageNeedsVersion = node.kind === "package" && (hasMeaningfulChanges(node.path) || dependencyChanged || submodulesChanged);
1205
+ let plannedVersion = null;
1206
+ if (packageNeedsVersion) {
1207
+ const selection = selectPackageVersion(node, options);
1208
+ plannedVersion = selection.version;
1209
+ if (!plannedVersion) {
1210
+ throw new RepositorySaveError(`Unable to plan package version for ${node.name}.`);
1211
+ }
1212
+ if (selection.reused) {
1213
+ emitProgress(options, node, "version", `Reusing existing interrupted save version ${plannedVersion}.`);
1214
+ } else {
1215
+ applyPackageVersion(node, plannedVersion);
1216
+ }
1217
+ node.plannedVersion = plannedVersion;
1218
+ node.plannedTag = plannedVersion;
1219
+ report.version = plannedVersion;
1220
+ report.tagName = plannedVersion;
1221
+ if (!selection.reused) {
1222
+ emitProgress(options, node, "version", `Planned ${plannedVersion}.`);
1223
+ }
1224
+ const reference = finalizePackageReference(node, plannedVersion, options);
1225
+ report.dependencySpec = reference.spec;
1226
+ report.install = await runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs);
1227
+ } else if (node.kind === "package") {
1228
+ report.version = String(node.packageJson?.version ?? report.version ?? "");
1229
+ } else if (node.kind === "project" && dependencyChanged && hasNpmLockfile(node.path)) {
1230
+ report.install = await runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs);
1231
+ }
1232
+ if (hasNpmLockfile(node.path) && (node.kind === "project" || packageNeedsVersion || dependencyChanged || submodulesChanged)) {
1233
+ report.lockfileValidation = await validateRepositoryLockfile(node, options);
1234
+ }
1235
+ const dirty = hasMeaningfulChanges(node.path);
1236
+ report.dirty = dirty;
1237
+ if (!dirty) {
1238
+ report.skippedReason = "clean";
1239
+ report.commitSha = headCommit(node.path);
1240
+ emitProgress(options, node, "clean", "No meaningful changes to commit.");
1241
+ if (node.kind === "package") {
1242
+ const finalized = await finalizeCleanPackageVersion(node, options, state, report, branch);
1243
+ if (finalized) {
1244
+ return report;
1245
+ }
1246
+ }
1247
+ if (node.id === ".") {
1248
+ const rebase2 = pullRebaseFromOrigin(node, options, branch);
1249
+ const push = pushCurrentBranch(node, options, branch);
1250
+ report.pushed = push.pushed;
1251
+ report.publishWait = {
1252
+ ...rebase2,
1253
+ ...push
1254
+ };
1255
+ report.commitSha = headCommit(node.path);
1256
+ }
1257
+ state.finalizedCommits.set(node.relativePath, report.commitSha);
1258
+ return report;
1259
+ }
1260
+ runCapturedCommand(node, options, "commit", "git", ["add", "-A"]);
1261
+ if (!hasStagedChanges(node.path)) {
1262
+ report.dirty = false;
1263
+ report.skippedReason = "clean-after-add";
1264
+ report.commitSha = headCommit(node.path);
1265
+ emitProgress(options, node, "clean", "No staged changes to commit after refreshing the index.");
1266
+ if (node.kind === "package") {
1267
+ const finalized = await finalizeCleanPackageVersion(node, options, state, report, branch);
1268
+ if (finalized) {
1269
+ return report;
1270
+ }
1271
+ }
1272
+ if (node.id === ".") {
1273
+ const rebase2 = pullRebaseFromOrigin(node, options, branch);
1274
+ const push = pushCurrentBranch(node, options, branch);
1275
+ report.pushed = push.pushed;
1276
+ report.publishWait = {
1277
+ ...rebase2,
1278
+ ...push
1279
+ };
1280
+ report.commitSha = headCommit(node.path);
1281
+ }
1282
+ state.finalizedCommits.set(node.relativePath, report.commitSha);
1283
+ return report;
1284
+ }
1285
+ const { changedFiles, diff } = gitDiffSummary(node.path);
1286
+ emitProgress(options, node, "message", "Generating commit message.");
1287
+ const messageResult = await commitMessageFor(node, options, {
1288
+ changedFiles,
1289
+ diff,
1290
+ plannedVersion: plannedVersion ?? report.version,
1291
+ plannedTag: node.plannedTag ?? report.tagName
1292
+ });
1293
+ report.commitMessage = messageResult.message;
1294
+ report.commitMessageProvider = messageResult.provider;
1295
+ report.commitMessageFallbackUsed = messageResult.fallbackUsed;
1296
+ report.commitMessageError = messageResult.error;
1297
+ emitProgress(options, node, "message", `${messageResult.provider}${messageResult.fallbackUsed ? " fallback" : ""}: ${messageResult.message.split(/\r?\n/u)[0]}`);
1298
+ try {
1299
+ runCapturedCommand(node, options, "commit", "git", ["commit", "-m", messageResult.message]);
1300
+ } catch (error) {
1301
+ if (!isNoOpGitCommitError(error) || hasMeaningfulChanges(node.path) || hasStagedChanges(node.path)) {
1302
+ throw error;
1303
+ }
1304
+ report.dirty = false;
1305
+ report.skippedReason = "clean-at-commit";
1306
+ report.commitSha = headCommit(node.path);
1307
+ emitProgress(options, node, "clean", "No changes remained to commit after Git refreshed the index.");
1308
+ if (node.kind === "package") {
1309
+ const finalized = await finalizeCleanPackageVersion(node, options, state, report, branch);
1310
+ if (finalized) {
1311
+ return report;
1312
+ }
1313
+ }
1314
+ if (node.id === ".") {
1315
+ const rebase2 = pullRebaseFromOrigin(node, options, branch);
1316
+ const push = pushCurrentBranch(node, options, branch);
1317
+ report.pushed = push.pushed;
1318
+ report.publishWait = {
1319
+ ...rebase2,
1320
+ ...push
1321
+ };
1322
+ report.commitSha = headCommit(node.path);
1323
+ }
1324
+ state.finalizedCommits.set(node.relativePath, report.commitSha);
1325
+ return report;
1326
+ }
1327
+ report.committed = true;
1328
+ const rebase = pullRebaseFromOrigin(node, options, branch);
1329
+ const verifyMode = options.verifyMode ?? "action-first";
1330
+ if (node.kind === "package") {
1331
+ ensureRemoteAccessBeforeVerification(node, options, state);
1332
+ }
1333
+ report.verification = await runRepoVerification(node, options, verifyMode);
1334
+ report.verified = report.verification.status === "passed";
1335
+ if (node.kind === "package") {
1336
+ const version = plannedVersion ?? String(readJson(resolve(node.path, "package.json")).version ?? report.version ?? "");
1337
+ const tagMessage = ensurePackageTagReady(node, options, version, branch, options.workflowRunId);
1338
+ report.tagName = version;
1339
+ report.version = version;
1340
+ report.devTagMetadata = tagMessage?.includes("treeseed-dev-tag: true") ? tagMessage : null;
1341
+ const reference = finalizePackageReference(node, version, options);
1342
+ const push = pushCurrentBranch(node, options, branch, version);
1343
+ await runGitDependencySmoke(node, options, reference);
1344
+ report.dependencySpec = reference.spec;
1345
+ state.finalizedVersions.set(node.name, version);
1346
+ state.finalizedReferences.set(node.name, reference);
1347
+ report.pushed = push.pushed;
1348
+ report.publishWait = {
1349
+ ...rebase,
1350
+ ...push
1351
+ };
1352
+ } else {
1353
+ const push = pushCurrentBranch(node, options, branch);
1354
+ report.pushed = push.pushed;
1355
+ report.publishWait = {
1356
+ ...rebase,
1357
+ ...push
1358
+ };
1359
+ }
1360
+ report.commitSha = headCommit(node.path);
1361
+ report.skippedReason = null;
1362
+ state.finalizedCommits.set(node.relativePath, report.commitSha);
1363
+ emitProgress(options, node, "done", `Saved ${report.commitSha?.slice(0, 12) ?? "current HEAD"}.`);
1364
+ return report;
1365
+ }
1366
+ async function runRepositorySaveOrchestrator(options) {
1367
+ const root = options.root;
1368
+ const gitRoot = options.gitRoot;
1369
+ const branch = options.branch;
1370
+ const scope = branch === STAGING_BRANCH ? "staging" : branch === PRODUCTION_BRANCH ? "prod" : "local";
1371
+ const allNodes = discoverRepositorySaveNodes(root, gitRoot, branch, {
1372
+ stablePackageRelease: options.stablePackageRelease === true
1373
+ });
1374
+ const nodes = options.includeRoot === false ? allNodes.filter((node) => node.id !== ".") : allNodes;
1375
+ const mode = nodes.some((node) => node.id !== ".") ? "recursive-workspace" : "root-only";
1376
+ const waves = repositorySaveWaves(nodes);
1377
+ const state = {
1378
+ finalizedVersions: /* @__PURE__ */ new Map(),
1379
+ finalizedReferences: /* @__PURE__ */ new Map(),
1380
+ finalizedCommits: /* @__PURE__ */ new Map(),
1381
+ reports: new Map(nodes.map((node) => [node.id, createReport(node)])),
1382
+ remoteAccessChecked: /* @__PURE__ */ new Set()
1383
+ };
1384
+ for (const wave of waves) {
1385
+ await runLimited(wave, 3, async (node) => {
1386
+ try {
1387
+ await saveOneRepository(node, options, state);
1388
+ } catch (error) {
1389
+ const existing = repositorySaveErrorDetails(error);
1390
+ throw new RepositorySaveError(error instanceof Error ? error.message : String(error), {
1391
+ exitCode: existing.exitCode,
1392
+ details: {
1393
+ ...existing.details ?? {},
1394
+ partialFailure: {
1395
+ message: `Treeseed save stopped while saving ${node.name}.`,
1396
+ failingRepo: node.name,
1397
+ phase: typeof existing.details?.phase === "string" ? existing.details.phase : null,
1398
+ currentVersion: typeof node.packageJson?.version === "string" ? node.packageJson.version : null,
1399
+ expectedTag: node.plannedTag,
1400
+ tagState: node.plannedTag ? tagState(node.path, node.plannedTag) : null,
1401
+ nextCommand: `treeseed resume ${options.workflowRunId ?? "<run-id>"}`,
1402
+ repos: [...state.reports.entries()].filter(([id]) => id !== ".").map(([, report]) => report),
1403
+ rootRepo: state.reports.get(".") ?? null,
1404
+ error: error instanceof Error ? error.message : String(error)
1405
+ }
1406
+ }
1407
+ });
1408
+ }
1409
+ });
1410
+ }
1411
+ const rootNode = nodes.find((node) => node.id === ".") ?? allNodes.find((node) => node.id === ".");
1412
+ const rootReport = rootNode ? state.reports.get(rootNode.id) ?? createReport(rootNode) : createReport({
1413
+ id: ".",
1414
+ name: "@treeseed/market",
1415
+ path: gitRoot,
1416
+ relativePath: ".",
1417
+ kind: "project",
1418
+ branch,
1419
+ branchMode: "project-save",
1420
+ packageJsonPath: null,
1421
+ packageJson: null,
1422
+ scripts: {},
1423
+ remoteUrl: null,
1424
+ dependencies: [],
1425
+ dependents: [],
1426
+ submoduleDependencies: [],
1427
+ plannedVersion: null,
1428
+ plannedTag: null,
1429
+ plannedDependencySpec: null
1430
+ });
1431
+ const packageReports = nodes.filter((node) => node.id !== ".").sort(compareNodes).map((node) => state.reports.get(node.id) ?? createReport(node));
1432
+ return {
1433
+ mode,
1434
+ branch,
1435
+ scope,
1436
+ repos: packageReports,
1437
+ rootRepo: rootReport,
1438
+ waves: waves.map((wave) => wave.map((node) => node.name)),
1439
+ plannedVersions: Object.fromEntries(state.finalizedVersions.entries())
1440
+ };
1441
+ }
1442
+ function repositorySaveErrorDetails(error) {
1443
+ if (error instanceof RepositorySaveError) {
1444
+ return {
1445
+ exitCode: error.exitCode,
1446
+ details: error.details
1447
+ };
1448
+ }
1449
+ return {
1450
+ exitCode: void 0,
1451
+ details: void 0
1452
+ };
1453
+ }
1454
+ export {
1455
+ discoverRepositorySaveNodes,
1456
+ nextDevVersion,
1457
+ planRepositorySave,
1458
+ refreshAndValidateRootWorkspaceLockfileForSave,
1459
+ repositorySaveErrorDetails,
1460
+ repositorySaveWaves,
1461
+ runRepositorySaveOrchestrator
1462
+ };