@treeseed/sdk 0.10.27 → 0.11.0

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 (168) hide show
  1. package/README.md +207 -6
  2. package/dist/capacity-provider.d.ts +3 -1
  3. package/dist/capacity-provider.js +25 -5
  4. package/dist/control-plane.d.ts +1 -0
  5. package/dist/control-plane.js +38 -13
  6. package/dist/db/market-schema.d.ts +8860 -6172
  7. package/dist/db/market-schema.js +108 -0
  8. package/dist/db/node-sqlite.js +7 -2
  9. package/dist/hosting/apps.d.ts +12 -0
  10. package/dist/hosting/apps.js +107 -0
  11. package/dist/hosting/builtins.d.ts +25 -0
  12. package/dist/hosting/builtins.js +791 -0
  13. package/dist/hosting/contracts.d.ts +207 -0
  14. package/dist/hosting/contracts.js +0 -0
  15. package/dist/hosting/graph.d.ts +192 -0
  16. package/dist/hosting/graph.js +1106 -0
  17. package/dist/hosting/index.d.ts +4 -0
  18. package/dist/hosting/index.js +4 -0
  19. package/dist/index.d.ts +11 -4
  20. package/dist/index.js +71 -7
  21. package/dist/managed-dependencies.js +1 -2
  22. package/dist/market-client.d.ts +63 -3
  23. package/dist/market-client.js +83 -11
  24. package/dist/operations/services/bootstrap-runner.d.ts +3 -1
  25. package/dist/operations/services/bootstrap-runner.js +22 -2
  26. package/dist/operations/services/config-runtime.d.ts +10 -5
  27. package/dist/operations/services/config-runtime.js +209 -66
  28. package/dist/operations/services/deploy.d.ts +70 -7
  29. package/dist/operations/services/deploy.js +579 -64
  30. package/dist/operations/services/deployment-readiness.d.ts +30 -0
  31. package/dist/operations/services/deployment-readiness.js +175 -0
  32. package/dist/operations/services/git-workflow.d.ts +2 -1
  33. package/dist/operations/services/git-workflow.js +9 -3
  34. package/dist/operations/services/github-actions-verification.d.ts +1 -0
  35. package/dist/operations/services/github-actions-verification.js +1 -0
  36. package/dist/operations/services/github-api.js +1 -1
  37. package/dist/operations/services/github-automation.d.ts +1 -1
  38. package/dist/operations/services/github-automation.js +4 -3
  39. package/dist/operations/services/github-credentials.d.ts +13 -0
  40. package/dist/operations/services/github-credentials.js +58 -0
  41. package/dist/operations/services/hosted-service-checks.d.ts +63 -0
  42. package/dist/operations/services/hosted-service-checks.js +327 -0
  43. package/dist/operations/services/hub-provider-launch.js +3 -3
  44. package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
  45. package/dist/operations/services/live-hosted-service-checks.js +350 -0
  46. package/dist/operations/services/managed-host-security.js +1 -1
  47. package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
  48. package/dist/operations/services/operations-runner-smoke.js +180 -0
  49. package/dist/operations/services/package-adapters.d.ts +95 -0
  50. package/dist/operations/services/package-adapters.js +288 -0
  51. package/dist/operations/services/package-reference-policy.d.ts +1 -0
  52. package/dist/operations/services/package-reference-policy.js +15 -2
  53. package/dist/operations/services/project-platform.d.ts +80 -22
  54. package/dist/operations/services/project-platform.js +49 -8
  55. package/dist/operations/services/project-web-monitor.js +26 -4
  56. package/dist/operations/services/railway-api.d.ts +88 -5
  57. package/dist/operations/services/railway-api.js +626 -35
  58. package/dist/operations/services/railway-deploy.d.ts +46 -40
  59. package/dist/operations/services/railway-deploy.js +261 -293
  60. package/dist/operations/services/release-candidate.d.ts +19 -0
  61. package/dist/operations/services/release-candidate.js +375 -38
  62. package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
  63. package/dist/operations/services/repository-save-orchestrator.js +279 -66
  64. package/dist/operations/services/runtime-tools.d.ts +1 -0
  65. package/dist/operations/services/runtime-tools.js +10 -9
  66. package/dist/operations/services/template-registry.js +14 -7
  67. package/dist/operations/services/verification-cache.d.ts +25 -0
  68. package/dist/operations/services/verification-cache.js +71 -0
  69. package/dist/operations/services/workspace-dependency-mode.js +9 -1
  70. package/dist/operations/services/workspace-save.js +1 -1
  71. package/dist/operations/services/workspace-tools.js +2 -1
  72. package/dist/platform/contracts.d.ts +32 -1
  73. package/dist/platform/deploy-config.js +73 -8
  74. package/dist/platform/env.yaml +163 -35
  75. package/dist/platform/environment.d.ts +1 -0
  76. package/dist/platform/environment.js +74 -5
  77. package/dist/platform/plugin.d.ts +9 -0
  78. package/dist/platform-operation-store.js +2 -2
  79. package/dist/platform-operations.js +1 -1
  80. package/dist/reconcile/bootstrap-systems.js +2 -2
  81. package/dist/reconcile/builtin-adapters.js +372 -189
  82. package/dist/reconcile/contracts.d.ts +9 -5
  83. package/dist/reconcile/desired-state.d.ts +1 -0
  84. package/dist/reconcile/desired-state.js +5 -5
  85. package/dist/reconcile/engine.d.ts +5 -2
  86. package/dist/reconcile/engine.js +53 -32
  87. package/dist/reconcile/index.d.ts +2 -0
  88. package/dist/reconcile/index.js +2 -0
  89. package/dist/reconcile/live-acceptance.d.ts +79 -0
  90. package/dist/reconcile/live-acceptance.js +1615 -0
  91. package/dist/reconcile/platform.d.ts +104 -0
  92. package/dist/reconcile/platform.js +100 -0
  93. package/dist/reconcile/state.js +4 -4
  94. package/dist/reconcile/units.js +2 -2
  95. package/dist/scripts/deployment-readiness.js +20 -0
  96. package/dist/scripts/generate-treedx-openapi-types.js +186 -0
  97. package/dist/scripts/operations-runner-smoke.js +16 -0
  98. package/dist/scripts/release-verify.js +4 -1
  99. package/dist/scripts/template-catalog.test.js +7 -7
  100. package/dist/scripts/tenant-workflow-action.js +10 -1
  101. package/dist/sdk-types.d.ts +172 -5
  102. package/dist/sdk-types.js +28 -3
  103. package/dist/sdk.d.ts +35 -24
  104. package/dist/sdk.js +186 -17
  105. package/dist/template-launch-requirements.js +9 -0
  106. package/dist/treedx/adapters.d.ts +6 -0
  107. package/dist/treedx/adapters.js +36 -0
  108. package/dist/treedx/client.d.ts +222 -0
  109. package/dist/treedx/client.js +871 -0
  110. package/dist/treedx/errors.d.ts +13 -0
  111. package/dist/treedx/errors.js +17 -0
  112. package/dist/treedx/federated-client.d.ts +27 -0
  113. package/dist/treedx/federated-client.js +158 -0
  114. package/dist/treedx/generated/openapi-types.d.ts +3558 -0
  115. package/dist/treedx/generated/openapi-types.js +0 -0
  116. package/dist/treedx/graph-adapter.d.ts +33 -0
  117. package/dist/treedx/graph-adapter.js +156 -0
  118. package/dist/treedx/index.d.ts +14 -0
  119. package/dist/treedx/index.js +48 -0
  120. package/dist/treedx/market-integration.d.ts +27 -0
  121. package/dist/treedx/market-integration.js +131 -0
  122. package/dist/treedx/ports.d.ts +166 -0
  123. package/dist/treedx/ports.js +231 -0
  124. package/dist/treedx/query-adapter.d.ts +19 -0
  125. package/dist/treedx/query-adapter.js +62 -0
  126. package/dist/treedx/registry-client.d.ts +11 -0
  127. package/dist/treedx/registry-client.js +19 -0
  128. package/dist/treedx/repository-adapter.d.ts +45 -0
  129. package/dist/treedx/repository-adapter.js +308 -0
  130. package/dist/treedx/sdk-integration.d.ts +27 -0
  131. package/dist/treedx/sdk-integration.js +63 -0
  132. package/dist/treedx/types.d.ts +1084 -0
  133. package/dist/treedx/types.js +8 -0
  134. package/dist/treedx/workspace-adapter.d.ts +27 -0
  135. package/dist/treedx/workspace-adapter.js +65 -0
  136. package/dist/treedx-backends.d.ts +218 -0
  137. package/dist/treedx-backends.js +632 -0
  138. package/dist/treedx-client.d.ts +86 -0
  139. package/dist/treedx-client.js +175 -0
  140. package/dist/treeseed/template-catalog/catalog.fixture.json +497 -138
  141. package/dist/workflow/operations.d.ts +119 -13
  142. package/dist/workflow/operations.js +309 -53
  143. package/dist/workflow-state.d.ts +13 -0
  144. package/dist/workflow-state.js +43 -26
  145. package/dist/workflow-support.d.ts +11 -3
  146. package/dist/workflow-support.js +67 -3
  147. package/dist/workflow.d.ts +5 -0
  148. package/drizzle/market/0004_treedx_market_integration.sql +99 -0
  149. package/package.json +34 -3
  150. package/templates/github/deploy-web.workflow.yml +39 -6
  151. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.d.ts +0 -3
  152. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.ts +0 -6
  153. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +0 -35
  154. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +0 -4
  155. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +0 -65
  156. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +0 -22
  157. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/empty/.gitkeep +0 -1
  158. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/knowledge/handbook/index.mdx +0 -11
  159. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/pages/welcome.mdx +0 -11
  160. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +0 -11
  161. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +0 -17
  162. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.d.ts +0 -1
  163. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.ts +0 -3
  164. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/env.yaml +0 -1
  165. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +0 -26
  166. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +0 -74
  167. package/dist/treeseed/template-catalog/templates/starter-basic/template/tsconfig.json +0 -9
  168. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +0 -103
@@ -2,6 +2,7 @@ import { type GitRemoteWriteMode } from './git-remote-policy.ts';
2
2
  import { type CommitMessageContext, type CommitMessageProvider, type CommitMessageProviderMode } from './commit-message-provider.ts';
3
3
  import { type DevDependencyReferenceMode, type GitDependencyProtocol } from './package-reference-policy.ts';
4
4
  import { type BuildWarningPolicyOptions } from './build-warning-policy.js';
5
+ import { type TreeseedPackageCommand } from './package-adapters.ts';
5
6
  export type RepoKind = 'package' | 'project';
6
7
  export type RepoBranchMode = 'package-release-main' | 'package-dev-save' | 'project-save';
7
8
  export type SaveVerifyMode = 'action-first' | 'local-only' | 'skip';
@@ -19,6 +20,7 @@ export type RepositorySaveNode = {
19
20
  packageJsonPath: string | null;
20
21
  packageJson: Record<string, unknown> | null;
21
22
  scripts: Record<string, string>;
23
+ manifestVerifyCommands: Record<'fast' | 'local' | 'release', TreeseedPackageCommand | null>;
22
24
  remoteUrl: string | null;
23
25
  dependencies: string[];
24
26
  dependents: string[];
@@ -60,7 +62,7 @@ export type RepositorySaveReport = {
60
62
  export type RepositoryVerificationResult = {
61
63
  mode: SaveVerifyMode;
62
64
  status: 'passed' | 'failed' | 'skipped';
63
- primary: 'verify:action' | 'verify:local' | null;
65
+ primary: 'verify:action' | 'verify:local' | 'manifest:fast' | 'manifest:local' | 'manifest:release' | null;
64
66
  fallbackUsed: boolean;
65
67
  error: string | null;
66
68
  };
@@ -1,5 +1,4 @@
1
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
2
  import { basename, resolve, relative } from "node:path";
4
3
  import { spawn, spawnSync } from "node:child_process";
5
4
  import {
@@ -37,11 +36,18 @@ import {
37
36
  sortWorkspacePackages,
38
37
  workspacePackages
39
38
  } from "./workspace-tools.js";
40
- import { collectDeploymentLockfileWorkspaceIssues } from "./workspace-dependency-mode.js";
39
+ import { collectDeploymentLockfileWorkspaceIssues, ensureLocalWorkspaceLinks } from "./workspace-dependency-mode.js";
41
40
  import {
42
41
  createBuildWarningSummary,
43
42
  formatAllowedBuildWarnings
44
43
  } from "./build-warning-policy.js";
44
+ import {
45
+ readTreeseedVerificationCache,
46
+ writeTreeseedVerificationCache
47
+ } from "./verification-cache.js";
48
+ import {
49
+ discoverTreeseedPackageAdapters
50
+ } from "./package-adapters.js";
45
51
  class RepositorySaveError extends Error {
46
52
  exitCode;
47
53
  details;
@@ -261,6 +267,13 @@ function isGitRepo(repoDir) {
261
267
  return false;
262
268
  }
263
269
  }
270
+ function isIndependentGitRepo(repoDir) {
271
+ try {
272
+ return resolve(repoRoot(repoDir)) === resolve(repoDir);
273
+ } catch {
274
+ return false;
275
+ }
276
+ }
264
277
  function originRemoteUrlSafe(repoDir) {
265
278
  try {
266
279
  return originRemoteUrl(repoDir);
@@ -278,6 +291,9 @@ function ensureWritableRemote(node, options) {
278
291
  function repoDisplayName(repoDir, packageJson) {
279
292
  return typeof packageJson?.name === "string" && packageJson.name.length > 0 ? packageJson.name : basename(repoDir);
280
293
  }
294
+ function emptyManifestVerifyCommands() {
295
+ return { fast: null, local: null, release: null };
296
+ }
281
297
  function parseGitmodules(root) {
282
298
  const gitmodulesPath = resolve(root, ".gitmodules");
283
299
  if (!existsSync(gitmodulesPath)) {
@@ -319,6 +335,9 @@ function packageVersionAtHead(node) {
319
335
  return null;
320
336
  }
321
337
  }
338
+ function canManagePackageJsonVersion(node) {
339
+ return node.kind === "package" && Boolean(node.packageJsonPath) && typeof node.packageJson?.version === "string";
340
+ }
322
341
  function packageVersionEligibleForBranch(node, version, options) {
323
342
  return node.branchMode === "package-release-main" ? isStableSemverVersion(version) : isDevVersionForBranch(version, node.branch || options.branch);
324
343
  }
@@ -378,28 +397,35 @@ function createReport(node) {
378
397
  function discoverRepositorySaveNodes(root, gitRoot = repoRoot(root), branch = currentBranch(gitRoot), options = {}) {
379
398
  const repoDirs = /* @__PURE__ */ new Map();
380
399
  repoDirs.set(".", gitRoot);
400
+ const packageAdaptersByDir = new Map(discoverTreeseedPackageAdapters(root).map((adapter) => [resolve(adapter.dir), adapter]));
381
401
  if (hasCompleteTreeseedPackageCheckout(root)) {
382
402
  for (const pkg of workspacePackages(root)) {
383
- if (isGitRepo(pkg.dir)) {
403
+ if (isIndependentGitRepo(pkg.dir)) {
384
404
  repoDirs.set(pkg.relativeDir, pkg.dir);
385
405
  }
386
406
  }
387
407
  }
408
+ for (const adapter of packageAdaptersByDir.values()) {
409
+ if (isIndependentGitRepo(adapter.dir)) {
410
+ repoDirs.set(adapter.relativeDir, adapter.dir);
411
+ }
412
+ }
388
413
  for (const submodulePath of parseGitmodules(root)) {
389
414
  const dir = resolve(root, submodulePath);
390
- if (existsSync(dir) && isGitRepo(dir)) {
415
+ if (existsSync(dir) && isIndependentGitRepo(dir)) {
391
416
  repoDirs.set(submodulePath, dir);
392
417
  }
393
418
  }
394
419
  const nodes = [...repoDirs.entries()].map(([relativePath, repoDir]) => {
395
420
  const packageJsonPath = resolve(repoDir, "package.json");
396
421
  const packageJson = existsSync(packageJsonPath) ? readJson(packageJsonPath) : null;
397
- const kind = classifyRepoKind(packageJson);
422
+ const adapter = packageAdaptersByDir.get(resolve(repoDir)) ?? null;
423
+ const kind = adapter && !packageJson ? "package" : classifyRepoKind(packageJson);
398
424
  const repoBranch = relativePath === "." ? currentBranch(repoDir) || branch || null : branch || currentBranch(repoDir) || null;
399
425
  const branchMode = kind === "project" ? "project-save" : options.stablePackageRelease === true && repoBranch === PRODUCTION_BRANCH ? "package-release-main" : "package-dev-save";
400
426
  return {
401
427
  id: relativePath,
402
- name: repoDisplayName(repoDir, packageJson),
428
+ name: adapter?.id ?? repoDisplayName(repoDir, packageJson),
403
429
  path: repoDir,
404
430
  relativePath,
405
431
  kind,
@@ -408,6 +434,7 @@ function discoverRepositorySaveNodes(root, gitRoot = repoRoot(root), branch = cu
408
434
  packageJsonPath: packageJson ? packageJsonPath : null,
409
435
  packageJson,
410
436
  scripts: packageScripts(packageJson),
437
+ manifestVerifyCommands: adapter?.verifyCommands ?? emptyManifestVerifyCommands(),
411
438
  remoteUrl: originRemoteUrlSafe(repoDir),
412
439
  dependencies: [],
413
440
  dependents: [],
@@ -554,7 +581,12 @@ function commitSubject(message) {
554
581
  }
555
582
  function gitDiffSummary(repoDir) {
556
583
  const changedFiles = run("git", ["status", "--porcelain"], { cwd: repoDir, capture: true });
557
- const diff = run("git", ["diff", "--cached"], { cwd: repoDir, capture: true });
584
+ const rawDiff = run("git", ["diff", "--cached"], { cwd: repoDir, capture: true, maxBuffer: 1024 * 1024 * 32 });
585
+ const maxDiffChars = 12e4;
586
+ const diff = rawDiff.length > maxDiffChars ? `${rawDiff.slice(0, maxDiffChars)}
587
+
588
+ [treeseed-save: diff truncated from ${rawDiff.length} characters for commit-message generation]
589
+ ` : rawDiff;
558
590
  return { changedFiles, diff };
559
591
  }
560
592
  function hasStagedChanges(repoDir) {
@@ -593,43 +625,100 @@ function shouldSkipGitDependencySmoke(options) {
593
625
  function hasNpmLockfile(repoDir) {
594
626
  return existsSync(resolve(repoDir, "package-lock.json")) || existsSync(resolve(repoDir, "npm-shrinkwrap.json"));
595
627
  }
596
- async function runGitDependencySmoke(node, options, reference) {
597
- if (reference.mode !== "dev-git-tag" || shouldSkipGitDependencySmoke(options)) return;
598
- const installSpec = reference.installSpec ?? reference.spec;
599
- const tempRoot = mkdtempSync(resolve(tmpdir(), "treeseed-git-dep-smoke-"));
600
- const npmCacheRoot = resolve(tempRoot, ".npm-cache");
601
- try {
602
- emitProgress(options, node, "smoke", `Installing ${installSpec} in a temporary project.`);
603
- writeFileSync(resolve(tempRoot, "package.json"), JSON.stringify({
604
- name: "treeseed-git-dependency-smoke",
605
- version: "0.0.0",
606
- private: true,
607
- type: "module",
608
- dependencies: {
609
- [reference.packageName]: installSpec
628
+ function syncRootWorkspaceLockfileMetadata(node, options) {
629
+ if (node.path !== options.root || !Array.isArray(node.packageJson?.workspaces)) return false;
630
+ const lockfilePath = resolve(node.path, "package-lock.json");
631
+ if (!existsSync(lockfilePath)) return false;
632
+ const lockfile = readJson(lockfilePath);
633
+ const packages = lockfile.packages;
634
+ if (!packages || typeof packages !== "object" || Array.isArray(packages)) return false;
635
+ let changed = false;
636
+ const packageEntries = packages;
637
+ const rootEntry = packageEntries[""] ?? {};
638
+ if (JSON.stringify(rootEntry.workspaces ?? []) !== JSON.stringify(node.packageJson.workspaces)) {
639
+ rootEntry.workspaces = node.packageJson.workspaces;
640
+ packageEntries[""] = rootEntry;
641
+ changed = true;
642
+ }
643
+ for (const field of dependencyFields(node.packageJson)) {
644
+ const nextValue = node.packageJson[field];
645
+ if (nextValue && typeof nextValue === "object" && !Array.isArray(nextValue)) {
646
+ const currentValue = rootEntry[field];
647
+ const currentDeps = currentValue && typeof currentValue === "object" && !Array.isArray(currentValue) ? currentValue : {};
648
+ const mergedDeps = { ...currentDeps };
649
+ let fieldChanged = false;
650
+ for (const [dependencyName, dependencySpec] of Object.entries(nextValue)) {
651
+ if (mergedDeps[dependencyName] != null) continue;
652
+ mergedDeps[dependencyName] = dependencySpec;
653
+ fieldChanged = true;
610
654
  }
611
- }, null, 2), "utf8");
612
- let lastError = null;
613
- for (let attempt = 1; attempt <= 5; attempt += 1) {
614
- emitProgress(options, node, "smoke", `npm install --cache ${npmCacheRoot} attempt ${attempt}/5.`);
615
- try {
616
- await runStreamingCommand(node, options, "smoke", "npm", ["install", "--cache", npmCacheRoot], { cwd: tempRoot });
617
- return;
618
- } catch (error) {
619
- lastError = error instanceof Error ? error.message : String(error);
655
+ if (fieldChanged) {
656
+ rootEntry[field] = mergedDeps;
657
+ packageEntries[""] = rootEntry;
658
+ changed = true;
620
659
  }
621
- if (attempt < 5) {
622
- emitProgress(options, node, "smoke", "npm install failed; retrying in 60 seconds.", "stderr");
623
- spawnSync("sleep", ["60"], { stdio: "ignore" });
660
+ }
661
+ }
662
+ for (const workspacePackage of workspacePackages(node.path)) {
663
+ const relativeDir = workspacePackage.relativeDir.replace(/\\/gu, "/");
664
+ const packageJson = workspacePackage.packageJson;
665
+ const packageName = String(packageJson.name ?? "");
666
+ if (!packageName) continue;
667
+ const packageEntry = packageEntries[relativeDir] ?? {};
668
+ if (packageEntry.name !== packageName) {
669
+ packageEntry.name = packageName;
670
+ changed = true;
671
+ }
672
+ if (typeof packageJson.version === "string" && packageEntry.version !== packageJson.version) {
673
+ packageEntry.version = packageJson.version;
674
+ changed = true;
675
+ }
676
+ for (const field of dependencyFields(packageJson)) {
677
+ const nextValue = packageJson[field];
678
+ if (nextValue && typeof nextValue === "object" && !Array.isArray(nextValue)) {
679
+ if (JSON.stringify(packageEntry[field] ?? {}) !== JSON.stringify(nextValue)) {
680
+ packageEntry[field] = nextValue;
681
+ changed = true;
682
+ }
624
683
  }
625
684
  }
685
+ packageEntries[relativeDir] = packageEntry;
686
+ const linkKey = `node_modules/${packageName}`;
687
+ const linkEntry = packageEntries[linkKey] ?? {};
688
+ if (linkEntry.resolved !== relativeDir) {
689
+ linkEntry.resolved = relativeDir;
690
+ changed = true;
691
+ }
692
+ if (linkEntry.link !== true) {
693
+ linkEntry.link = true;
694
+ changed = true;
695
+ }
696
+ packageEntries[linkKey] = linkEntry;
697
+ }
698
+ if (!changed) return false;
699
+ lockfile.packages = packageEntries;
700
+ writeJson(lockfilePath, lockfile);
701
+ emitProgress(options, node, "lockfile", "Synchronized root workspace lockfile metadata before validation.");
702
+ return true;
703
+ }
704
+ async function runGitDependencySmoke(node, options, reference) {
705
+ if (reference.mode !== "dev-git-tag" || shouldSkipGitDependencySmoke(options)) return;
706
+ const tagName = reference.tagName ?? reference.version;
707
+ if (!reference.remoteUrl || !tagName) {
708
+ throw new RepositorySaveError(`Git dependency smoke cannot verify ${reference.packageName}; remote URL or tag is missing.`);
709
+ }
710
+ const tagRef = `refs/tags/${tagName}`;
711
+ emitProgress(options, node, "smoke", `Verifying ${reference.packageName} tag ${tagName} exists on ${reference.remoteUrl}.`);
712
+ try {
713
+ await runStreamingCommand(node, options, "smoke", "git", ["ls-remote", "--exit-code", "--tags", reference.remoteUrl, tagRef]);
714
+ } catch (error) {
715
+ const detail = error instanceof Error ? error.message : String(error);
626
716
  throw new RepositorySaveError([
627
- `Git dependency smoke install failed for ${reference.packageName} after 5 attempts.`,
628
- `Spec: ${installSpec}`,
629
- lastError ?? ""
717
+ `Git dependency smoke failed for ${reference.packageName}; tag is not reachable on the remote.`,
718
+ `Remote: ${reference.remoteUrl}`,
719
+ `Tag: ${tagName}`,
720
+ detail
630
721
  ].join("\n"));
631
- } finally {
632
- rmSync(tempRoot, { recursive: true, force: true });
633
722
  }
634
723
  }
635
724
  async function runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs = []) {
@@ -640,7 +729,7 @@ async function runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs =
640
729
  let lastError = null;
641
730
  const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
642
731
  const rootWorkspaceInstall = node.path === options.root && Array.isArray(packageJson?.workspaces);
643
- const installFlags = ["--package-lock-only", "--ignore-scripts"];
732
+ const installFlags = rootWorkspaceInstall ? ["--package-lock-only", "--ignore-scripts"] : node.branchMode === "project-save" ? ["--ignore-scripts"] : ["--package-lock-only", "--ignore-scripts"];
644
733
  const args = rootWorkspaceInstall ? gitDependencyRefreshSpecs.length > 0 ? ["install", ...gitDependencyRefreshSpecs, ...installFlags, "--force"] : ["install", ...installFlags] : gitDependencyRefreshSpecs.length > 0 ? ["install", ...gitDependencyRefreshSpecs, ...installFlags, "--force", "--workspaces=false"] : ["install", ...installFlags, "--workspaces=false"];
645
734
  for (let attempt = 1; attempt <= 5; attempt += 1) {
646
735
  emitProgress(options, node, "install", `npm ${args.join(" ")} attempt ${attempt}/5.`);
@@ -658,6 +747,36 @@ async function runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs =
658
747
  throw new RepositorySaveError(`npm install failed after 5 attempts.
659
748
  ${lastError ?? ""}`);
660
749
  }
750
+ async function runProjectVerificationInstallWithRetry(node, options) {
751
+ if (node.branchMode !== "project-save" || !hasNpmLockfile(node.path)) return;
752
+ if (shouldSkipNetworkInstall()) {
753
+ emitProgress(options, node, "install", "Skipped project verification dependency install because network install mode is disabled.");
754
+ return;
755
+ }
756
+ let lastError = null;
757
+ const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
758
+ const rootWorkspaceInstall = node.path === options.root && Array.isArray(packageJson?.workspaces);
759
+ if (rootWorkspaceInstall) {
760
+ emitProgress(options, node, "install", "Skipped root npm ci project verification install; lockfile dry-run and restored workspace links provide save-time dependency proof.");
761
+ return;
762
+ }
763
+ const args = rootWorkspaceInstall ? ["ci"] : ["ci", "--workspaces=false"];
764
+ for (let attempt = 1; attempt <= 5; attempt += 1) {
765
+ emitProgress(options, node, "install", `npm ${args.join(" ")} for project verification attempt ${attempt}/5.`);
766
+ try {
767
+ await runStreamingCommand(node, options, "install", "npm", args);
768
+ return;
769
+ } catch (error) {
770
+ lastError = error instanceof Error ? error.message : String(error);
771
+ }
772
+ if (attempt < 5) {
773
+ emitProgress(options, node, "install", "npm ci for project verification failed; retrying in 60 seconds.", "stderr");
774
+ spawnSync("sleep", ["60"], { stdio: "ignore" });
775
+ }
776
+ }
777
+ throw new RepositorySaveError(`Project verification dependency install failed after 5 attempts.
778
+ ${lastError ?? ""}`);
779
+ }
661
780
  function lockfileValidationCommand(node, options) {
662
781
  const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
663
782
  const rootWorkspaceInstall = node.path === options.root && Array.isArray(packageJson?.workspaces);
@@ -668,6 +787,7 @@ async function validateRepositoryLockfile(node, options) {
668
787
  if (!hasNpmLockfile(node.path)) {
669
788
  return { status: "skipped", command: null, issues: [], error: "no npm lockfile" };
670
789
  }
790
+ syncRootWorkspaceLockfileMetadata(node, options);
671
791
  const issues = collectDeploymentLockfileWorkspaceIssues(node.path).map((issue) => `${issue.filePath}: ${issue.packageName} ${issue.reason}`);
672
792
  if (issues.length > 0) {
673
793
  throw new RepositorySaveError([
@@ -713,38 +833,99 @@ async function validateRepositoryLockfile(node, options) {
713
833
  function hasScript(node, scriptName) {
714
834
  return typeof node.scripts[scriptName] === "string" && node.scripts[scriptName].length > 0;
715
835
  }
836
+ function manifestVerifyCommand(node, key) {
837
+ return node.manifestVerifyCommands[key] ?? null;
838
+ }
839
+ function hasAnyVerificationCommand(node) {
840
+ return hasScript(node, "verify:action") || hasScript(node, "verify:local") || hasScript(node, "verify") || Boolean(manifestVerifyCommand(node, "local")) || Boolean(manifestVerifyCommand(node, "fast"));
841
+ }
716
842
  async function runScript(node, options, scriptName) {
717
843
  await runStreamingCommand(node, options, "verify", "npm", ["run", scriptName]);
718
844
  }
845
+ async function runManifestVerifyCommand(node, options, verifyMode, key) {
846
+ const manifestCommand = manifestVerifyCommand(node, key);
847
+ if (!manifestCommand) {
848
+ throw new RepositorySaveError(`${node.name} is missing a ${key} verification command in treeseed.package.yaml.`);
849
+ }
850
+ const command = `${manifestCommand.command} ${manifestCommand.args.join(" ")}`;
851
+ const cacheInput = {
852
+ workspaceRoot: options.root,
853
+ repoName: node.name,
854
+ repoPath: node.path,
855
+ command,
856
+ verifyMode,
857
+ env: process.env
858
+ };
859
+ const cached = readTreeseedVerificationCache(cacheInput);
860
+ if (cached) {
861
+ emitProgress(options, node, "verify", `[verify][cache] Reused ${node.name} ${manifestCommand.label} for ${cached.headSha.slice(0, 12)}.`);
862
+ return { cached: true };
863
+ }
864
+ const started = Date.now();
865
+ await runStreamingCommand(node, options, "verify", manifestCommand.command, manifestCommand.args, { cwd: manifestCommand.cwd });
866
+ writeTreeseedVerificationCache(cacheInput, Date.now() - started);
867
+ return { cached: false };
868
+ }
869
+ async function runCachedScript(node, options, verifyMode, scriptName) {
870
+ const command = `npm run ${scriptName}`;
871
+ const cacheInput = {
872
+ workspaceRoot: options.root,
873
+ repoName: node.name,
874
+ repoPath: node.path,
875
+ command,
876
+ verifyMode,
877
+ env: process.env
878
+ };
879
+ const cached = readTreeseedVerificationCache(cacheInput);
880
+ if (cached) {
881
+ emitProgress(options, node, "verify", `[verify][cache] Reused ${node.name} ${scriptName} for ${cached.headSha.slice(0, 12)}.`);
882
+ return { cached: true };
883
+ }
884
+ const started = Date.now();
885
+ await runScript(node, options, scriptName);
886
+ writeTreeseedVerificationCache(cacheInput, Date.now() - started);
887
+ return { cached: false };
888
+ }
719
889
  async function runRepoVerification(node, options, verifyMode) {
720
890
  if (verifyMode === "skip") {
721
891
  emitProgress(options, node, "verify", "Skipped verification by request.");
722
892
  return { mode: verifyMode, status: "skipped", primary: null, fallbackUsed: false, error: null };
723
893
  }
724
- if (node.kind !== "package") {
725
- emitProgress(options, node, "verify", "Skipped package verification for project repository.");
894
+ if (node.kind !== "package" && !hasAnyVerificationCommand(node)) {
895
+ emitProgress(options, node, "verify", "Skipped verification because project repository does not declare a Treeseed verify script.");
726
896
  return { mode: verifyMode, status: "skipped", primary: null, fallbackUsed: false, error: null };
727
897
  }
898
+ await runProjectVerificationInstallWithRetry(node, options);
728
899
  if (verifyMode === "local-only") {
900
+ if (hasScript(node, "verify:local")) {
901
+ await runCachedScript(node, options, verifyMode, "verify:local");
902
+ return { mode: verifyMode, status: "passed", primary: "verify:local", fallbackUsed: false, error: null };
903
+ }
904
+ if (manifestVerifyCommand(node, "local")) {
905
+ await runManifestVerifyCommand(node, options, verifyMode, "local");
906
+ return { mode: verifyMode, status: "passed", primary: "manifest:local", fallbackUsed: false, error: null };
907
+ }
908
+ if (manifestVerifyCommand(node, "fast")) {
909
+ await runManifestVerifyCommand(node, options, verifyMode, "fast");
910
+ return { mode: verifyMode, status: "passed", primary: "manifest:fast", fallbackUsed: false, error: null };
911
+ }
729
912
  if (!hasScript(node, "verify:local")) {
730
- throw new RepositorySaveError(`Package ${node.name} is missing required verify:local script.`);
913
+ throw new RepositorySaveError(`${node.kind === "package" ? "Package" : "Project"} ${node.name} is missing required verify:local script.`);
731
914
  }
732
- await runScript(node, options, "verify:local");
733
- return { mode: verifyMode, status: "passed", primary: "verify:local", fallbackUsed: false, error: null };
734
915
  }
735
- if (!hasScript(node, "verify:action") && !hasScript(node, "verify:local")) {
736
- throw new RepositorySaveError(`Package ${node.name} is missing required verify:action or verify:local script.`);
916
+ if (!hasAnyVerificationCommand(node)) {
917
+ throw new RepositorySaveError(`${node.kind === "package" ? "Package" : "Project"} ${node.name} is missing required verify:action, verify:local, or verify script.`);
737
918
  }
738
919
  if (hasScript(node, "verify:action")) {
739
920
  try {
740
- await runScript(node, options, "verify:action");
921
+ await runCachedScript(node, options, verifyMode, "verify:action");
741
922
  return { mode: verifyMode, status: "passed", primary: "verify:action", fallbackUsed: false, error: null };
742
923
  } catch (error) {
743
924
  if (!hasScript(node, "verify:local")) {
744
925
  throw error;
745
926
  }
746
927
  emitProgress(options, node, "verify", "verify:action failed; falling back to verify:local.", "stderr");
747
- await runScript(node, options, "verify:local");
928
+ await runCachedScript(node, options, verifyMode, "verify:local");
748
929
  return {
749
930
  mode: verifyMode,
750
931
  status: "passed",
@@ -754,7 +935,19 @@ async function runRepoVerification(node, options, verifyMode) {
754
935
  };
755
936
  }
756
937
  }
757
- await runScript(node, options, "verify:local");
938
+ if (hasScript(node, "verify:local")) {
939
+ await runCachedScript(node, options, verifyMode, "verify:local");
940
+ return { mode: verifyMode, status: "passed", primary: "verify:local", fallbackUsed: true, error: null };
941
+ }
942
+ if (manifestVerifyCommand(node, "local")) {
943
+ await runManifestVerifyCommand(node, options, verifyMode, "local");
944
+ return { mode: verifyMode, status: "passed", primary: "manifest:local", fallbackUsed: true, error: null };
945
+ }
946
+ if (manifestVerifyCommand(node, "fast")) {
947
+ await runManifestVerifyCommand(node, options, verifyMode, "fast");
948
+ return { mode: verifyMode, status: "passed", primary: "manifest:fast", fallbackUsed: true, error: null };
949
+ }
950
+ await runCachedScript(node, options, verifyMode, "verify");
758
951
  return { mode: verifyMode, status: "passed", primary: "verify:local", fallbackUsed: true, error: null };
759
952
  }
760
953
  function pullRebaseFromOrigin(node, options, branch) {
@@ -1007,7 +1200,7 @@ function refreshRepositoryNodePackageMetadata(node) {
1007
1200
  node.packageJson = packageJson;
1008
1201
  node.scripts = packageScripts(packageJson);
1009
1202
  node.remoteUrl = originRemoteUrlSafe(node.path);
1010
- if (node.kind === "package") {
1203
+ if (node.kind === "package" && packageJson) {
1011
1204
  node.name = repoDisplayName(node.path, packageJson);
1012
1205
  }
1013
1206
  }
@@ -1117,7 +1310,7 @@ function repoPlanCommands(node, options, plannedVersion, plannedDependencySpec,
1117
1310
  commands.push(`update package.json version to ${plannedVersion}`);
1118
1311
  commands.push("npm install --workspaces=false # explicitly refresh changed git-tag dependencies with --force; retry up to 5 times with 60s delay");
1119
1312
  } else if (node.kind === "project" && dependencyUpdates.length > 0 && hasNpmLockfile(node.path)) {
1120
- 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");
1313
+ commands.push(rootWorkspaceInstall ? "npm install --package-lock-only --ignore-scripts # refresh root workspace lockfile without installing git dependencies" : "npm install --workspaces=false # refresh project lockfile after internal dependency updates");
1121
1314
  }
1122
1315
  if (hasNpmLockfile(node.path) && (node.kind === "project" || plannedVersion || dependencyUpdates.length > 0 || node.submoduleDependencies.length > 0)) {
1123
1316
  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");
@@ -1128,24 +1321,36 @@ function repoPlanCommands(node, options, plannedVersion, plannedDependencySpec,
1128
1321
  commands.push(
1129
1322
  remoteExists ? `git pull --rebase --recurse-submodules=no origin ${branch}` : `skip pull --rebase # origin/${branch} does not exist yet`
1130
1323
  );
1131
- if (node.kind === "package") {
1132
- const verifyMode = options.verifyMode ?? "action-first";
1133
- if (verifyMode === "skip") {
1134
- commands.push("skip package verification");
1135
- } else if (verifyMode === "local-only") {
1324
+ const verifyMode = options.verifyMode ?? "action-first";
1325
+ if (verifyMode === "skip") {
1326
+ commands.push(node.kind === "package" ? "skip package verification" : "skip project verification");
1327
+ } else if (hasScript(node, "verify:action") || hasScript(node, "verify:local") || hasScript(node, "verify")) {
1328
+ if (verifyMode === "local-only") {
1136
1329
  commands.push("npm run verify:local");
1137
- } else {
1330
+ } else if (hasScript(node, "verify:action")) {
1138
1331
  commands.push("npm run verify:action # fallback to npm run verify:local on failure");
1332
+ } else if (hasScript(node, "verify:local")) {
1333
+ commands.push("npm run verify:local");
1334
+ } else {
1335
+ commands.push("npm run verify");
1139
1336
  }
1337
+ } else if (manifestVerifyCommand(node, "local") || manifestVerifyCommand(node, "fast")) {
1338
+ const command = verifyMode === "local-only" ? manifestVerifyCommand(node, "local") ?? manifestVerifyCommand(node, "fast") : manifestVerifyCommand(node, "local") ?? manifestVerifyCommand(node, "fast");
1339
+ if (command) commands.push(`${command.command} ${command.args.join(" ")} # treeseed.package.yaml verification`);
1340
+ } else if (node.kind !== "package") {
1341
+ commands.push("skip verification # project repository has no Treeseed verify script");
1342
+ }
1343
+ if (node.kind === "package") {
1140
1344
  if (plannedVersion) {
1141
1345
  commands.push(`git tag -a ${plannedVersion} -m <${plannedVersion.includes("-dev.") ? "dev metadata" : "release"}>`);
1142
1346
  commands.push(remoteExists ? `git push origin ${branch} ${plannedVersion}` : `git push -u origin ${branch} ${plannedVersion}`);
1143
1347
  if (plannedDependencySpec && node.branchMode === "package-dev-save") {
1144
- commands.push(`smoke install ${plannedDependencySpec}`);
1348
+ commands.push(`git ls-remote --exit-code --tags origin refs/tags/${plannedVersion} # validate dependency tag reachability`);
1145
1349
  }
1350
+ } else {
1351
+ commands.push(remoteExists ? `git push origin ${branch}` : `git push -u origin ${branch}`);
1146
1352
  }
1147
1353
  } else {
1148
- commands.push("skip package verification # project repository");
1149
1354
  commands.push(remoteExists ? `git push origin ${branch}` : `git push -u origin ${branch}`);
1150
1355
  }
1151
1356
  return commands;
@@ -1173,7 +1378,7 @@ function planRepositorySave(options) {
1173
1378
  return dependency?.dirty || Boolean(dependency?.plannedVersion);
1174
1379
  });
1175
1380
  const dirty = hasMeaningfulChanges(node.path);
1176
- const packageNeedsVersion = node.kind === "package" && (dirty || dependencyChanged || submoduleChanged);
1381
+ const packageNeedsVersion = canManagePackageJsonVersion(node) && (dirty || dependencyChanged || submoduleChanged);
1177
1382
  const currentVersion = typeof node.packageJson?.version === "string" ? node.packageJson.version : null;
1178
1383
  const plannedVersion = packageNeedsVersion ? selectPackageVersion(node, options).version : null;
1179
1384
  let plannedDependencySpec = null;
@@ -1269,6 +1474,7 @@ async function refreshAndValidateRootWorkspaceLockfileForSave(options) {
1269
1474
  packageJsonPath: packageJson ? packageJsonPath : null,
1270
1475
  packageJson,
1271
1476
  scripts: packageScripts(packageJson),
1477
+ manifestVerifyCommands: emptyManifestVerifyCommands(),
1272
1478
  remoteUrl: originRemoteUrlSafe(repoDir),
1273
1479
  dependencies: [],
1274
1480
  dependents: [],
@@ -1303,7 +1509,7 @@ async function saveOneRepository(node, options, state) {
1303
1509
  const submodulePointers = collectSubmodulePointerChanges(node, state.finalizedCommits);
1304
1510
  const submodulesChanged = submodulePointers.length > 0;
1305
1511
  const packageHasMeaningfulChanges = hasMeaningfulChanges(node.path);
1306
- const packageNeedsVersion = node.kind === "package" && (packageHasMeaningfulChanges || dependencyChanged || submodulesChanged || packageVersionTagConflictsWithHead(node, options));
1512
+ const packageNeedsVersion = canManagePackageJsonVersion(node) && (packageHasMeaningfulChanges || dependencyChanged || submodulesChanged || packageVersionTagConflictsWithHead(node, options));
1307
1513
  let plannedVersion = null;
1308
1514
  if (packageNeedsVersion) {
1309
1515
  const selection = selectPackageVersion(node, options);
@@ -1351,7 +1557,7 @@ async function saveOneRepository(node, options, state) {
1351
1557
  return report;
1352
1558
  }
1353
1559
  }
1354
- if (node.id === ".") {
1560
+ if (node.kind === "project" || node.kind === "package" && !canManagePackageJsonVersion(node)) {
1355
1561
  const rebase2 = pullRebaseFromOrigin(node, options, branch);
1356
1562
  const push = pushCurrentBranch(node, options, branch);
1357
1563
  report.pushed = push.pushed;
@@ -1376,7 +1582,7 @@ async function saveOneRepository(node, options, state) {
1376
1582
  return report;
1377
1583
  }
1378
1584
  }
1379
- if (node.id === ".") {
1585
+ if (node.kind === "project" || node.kind === "package" && !canManagePackageJsonVersion(node)) {
1380
1586
  const rebase2 = pullRebaseFromOrigin(node, options, branch);
1381
1587
  const push = pushCurrentBranch(node, options, branch);
1382
1588
  report.pushed = push.pushed;
@@ -1421,7 +1627,7 @@ async function saveOneRepository(node, options, state) {
1421
1627
  return report;
1422
1628
  }
1423
1629
  }
1424
- if (node.id === ".") {
1630
+ if (node.kind === "project" || node.kind === "package" && !canManagePackageJsonVersion(node)) {
1425
1631
  const rebase2 = pullRebaseFromOrigin(node, options, branch);
1426
1632
  const push = pushCurrentBranch(node, options, branch);
1427
1633
  report.pushed = push.pushed;
@@ -1437,12 +1643,19 @@ async function saveOneRepository(node, options, state) {
1437
1643
  report.committed = true;
1438
1644
  const rebase = pullRebaseFromOrigin(node, options, branch);
1439
1645
  const verifyMode = options.verifyMode ?? "action-first";
1646
+ if (node.kind === "project" && node.path === options.root && Array.isArray(node.packageJson?.workspaces)) {
1647
+ const linkReport = ensureLocalWorkspaceLinks(options.root);
1648
+ const restoredLinks = Array.isArray(linkReport.created) ? linkReport.created.length : 0;
1649
+ if (restoredLinks > 0) {
1650
+ emitProgress(options, node, "install", `Restored ${restoredLinks} local workspace package link${restoredLinks === 1 ? "" : "s"} before project verification.`);
1651
+ }
1652
+ }
1440
1653
  if (node.kind === "package") {
1441
1654
  ensureRemoteAccessBeforeVerification(node, options, state);
1442
1655
  }
1443
1656
  report.verification = await runRepoVerification(node, options, verifyMode);
1444
1657
  report.verified = report.verification.status === "passed";
1445
- if (node.kind === "package") {
1658
+ if (canManagePackageJsonVersion(node)) {
1446
1659
  const version = plannedVersion ?? String(readJson(resolve(node.path, "package.json")).version ?? report.version ?? "");
1447
1660
  const tagMessage = ensurePackageTagReady(node, options, version, branch, options.workflowRunId);
1448
1661
  report.tagName = version;
@@ -64,6 +64,7 @@ export declare function loadCliDeployConfig(tenantRoot: any): {
64
64
  previewProjectName: string | undefined;
65
65
  productionBranch: string;
66
66
  stagingBranch: string;
67
+ buildCommand: string | undefined;
67
68
  buildOutputDir: string | undefined;
68
69
  } | undefined;
69
70
  r2: {