@treeseed/sdk 0.6.9 → 0.6.11

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.
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { parseGraphDsl } from './graph/dsl.ts';
14
14
  export { createDefaultGraphRankingProvider, DEFAULT_GRAPH_RANKING_PROVIDER } from './graph/ranking.ts';
15
15
  export { BUILTIN_MODEL_REGISTRY, MODEL_REGISTRY, buildBuiltinModelRegistry, buildModelRegistry, buildScopedModelRegistry, mergeModelRegistries, resolveModelDefinition, } from './model-registry.ts';
16
16
  export { normalizeAgentCliOptions, buildCopilotAllowToolArgs } from './cli-tools.ts';
17
- export { collectTreeseedDependencyStatus, createTreeseedManagedToolEnv, formatTreeseedDependencyReport, installTreeseedDependencies, resolveTreeseedToolBinary, resolveTreeseedToolCommand, } from './managed-dependencies.ts';
17
+ export { collectTreeseedDependencyStatus, collectTreeseedToolStatus, createTreeseedManagedToolEnv, formatTreeseedDependencyReport, installTreeseedDependencies, resolveTreeseedToolBinary, resolveTreeseedToolCommand, type TreeseedToolStatusResult, } from './managed-dependencies.ts';
18
18
  export { runTreeseedCopilotTask, type TreeseedCopilotTaskInput, type TreeseedCopilotTaskResult, } from './copilot.ts';
19
19
  export { findDispatchCapability, listSdkDispatchCapabilities, listWorkflowDispatchCapabilities, } from './dispatch.ts';
20
20
  export { executeSdkOperation, findSdkOperation, listSdkOperationNames, } from './sdk-dispatch.ts';
package/dist/index.js CHANGED
@@ -86,6 +86,7 @@ import {
86
86
  import { normalizeAgentCliOptions, buildCopilotAllowToolArgs } from "./cli-tools.js";
87
87
  import {
88
88
  collectTreeseedDependencyStatus,
89
+ collectTreeseedToolStatus,
89
90
  createTreeseedManagedToolEnv,
90
91
  formatTreeseedDependencyReport,
91
92
  installTreeseedDependencies,
@@ -196,6 +197,7 @@ export {
196
197
  canonicalizeFrontmatter,
197
198
  collectTreeseedDependencyStatus,
198
199
  collectTreeseedReconcileStatus,
200
+ collectTreeseedToolStatus,
199
201
  createControlPlaneReporter,
200
202
  createDefaultGraphRankingProvider,
201
203
  createFilesystemContentSource,
@@ -25,6 +25,28 @@ export type TreeseedDependencyInstallResult = {
25
25
  npmInstalls: TreeseedNpmInstallReport[];
26
26
  reports: TreeseedDependencyReport[];
27
27
  };
28
+ export type TreeseedToolInvocation = {
29
+ mode: 'direct' | 'node' | 'unavailable';
30
+ command: string | null;
31
+ argsPrefix: string[];
32
+ binaryPath: string | null;
33
+ };
34
+ export type TreeseedToolReport = TreeseedDependencyReport & {
35
+ invocation: TreeseedToolInvocation;
36
+ };
37
+ export type TreeseedToolStatusResult = TreeseedDependencyInstallResult & {
38
+ tools: TreeseedToolReport[];
39
+ auth: {
40
+ github: {
41
+ checked: boolean;
42
+ authenticated: boolean;
43
+ binaryPath: string | null;
44
+ command: string[];
45
+ detail: string;
46
+ remediation: string[];
47
+ };
48
+ };
49
+ };
28
50
  type DependencyInstallerOptions = {
29
51
  tenantRoot?: string;
30
52
  force?: boolean;
@@ -52,5 +74,6 @@ export declare function resolveTreeseedToolCommand(toolName: TreeseedManagedTool
52
74
  } | null;
53
75
  export declare function installTreeseedDependencies(options?: DependencyInstallerOptions): Promise<TreeseedDependencyInstallResult>;
54
76
  export declare function collectTreeseedDependencyStatus(options?: DependencyInstallerOptions): TreeseedDependencyInstallResult;
77
+ export declare function collectTreeseedToolStatus(options?: DependencyInstallerOptions): TreeseedToolStatusResult;
55
78
  export declare function formatTreeseedDependencyReport(result: TreeseedDependencyInstallResult): string;
56
79
  export {};
@@ -73,6 +73,26 @@ function currentPlatformAsset() {
73
73
  function managedGhBin(env = process.env) {
74
74
  return resolve(resolveToolsHome(env), "gh", GH_VERSION, platformKey(), "bin", "gh");
75
75
  }
76
+ function tokenEnv(env = process.env) {
77
+ const ghToken = env.GH_TOKEN?.trim() || env.GITHUB_TOKEN?.trim() || "";
78
+ return ghToken ? {
79
+ ...env,
80
+ GH_TOKEN: ghToken,
81
+ GITHUB_TOKEN: ghToken
82
+ } : env;
83
+ }
84
+ function cleanCommandPathOutput(output) {
85
+ const lines = output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
86
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
87
+ if (lines[index]?.startsWith("/")) {
88
+ return lines[index] ?? null;
89
+ }
90
+ }
91
+ return lines[lines.length - 1] ?? null;
92
+ }
93
+ function redactSensitiveOutput(output) {
94
+ return output.replace(/^(\s*-\s*Token:\s*).+$/gim, "$1***").replace(/\b(?:github_pat|ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_*.-]+/gu, "***");
95
+ }
76
96
  function locateSystemBinary(command, spawn = spawnSync, env = process.env) {
77
97
  if (process.platform === "win32") {
78
98
  return null;
@@ -82,7 +102,7 @@ function locateSystemBinary(command, spawn = spawnSync, env = process.env) {
82
102
  encoding: "utf8",
83
103
  env
84
104
  });
85
- return result.status === 0 ? String(result.stdout ?? "").trim() || null : null;
105
+ return result.status === 0 ? cleanCommandPathOutput(String(result.stdout ?? "")) : null;
86
106
  }
87
107
  function checkCommand(command, args, options = {}) {
88
108
  const run = options.spawn ?? spawnSync;
@@ -104,15 +124,25 @@ ${result.stdout ?? ""}`.trim() || result.error?.message || ""
104
124
  }
105
125
  function resolvePackageJsonPath(packageName) {
106
126
  try {
107
- return require2.resolve(`${packageName}/package.json`);
127
+ const packageJsonPath = require2.resolve(`${packageName}/package.json`);
128
+ if (existsSync(packageJsonPath)) {
129
+ return packageJsonPath;
130
+ }
108
131
  } catch {
109
- for (const searchPath of require2.resolve.paths(packageName) ?? []) {
110
- const candidate = resolve(searchPath, packageName, "package.json");
111
- if (existsSync(candidate)) {
112
- return candidate;
113
- }
132
+ }
133
+ for (const searchPath of require2.resolve.paths(packageName) ?? []) {
134
+ const candidate = resolve(searchPath, packageName, "package.json");
135
+ if (existsSync(candidate)) {
136
+ return candidate;
114
137
  }
115
- throw new Error(`Unable to resolve package manifest for "${packageName}".`);
138
+ }
139
+ throw new Error(`Unable to resolve package manifest for "${packageName}".`);
140
+ }
141
+ function resolvePackageJsonPathOptional(packageName) {
142
+ try {
143
+ return resolvePackageJsonPath(packageName);
144
+ } catch {
145
+ return null;
116
146
  }
117
147
  }
118
148
  function resolvePackageBinary(packageName, binName) {
@@ -129,6 +159,27 @@ function resolvePackageBinary(packageName, binName) {
129
159
  const packageLocalFallback = resolve(dirname(packageJsonPath), relativeBin.replace(/^\.\.\//u, ""));
130
160
  return existsSync(packageLocalFallback) ? packageLocalFallback : resolvedBin;
131
161
  }
162
+ function resolvePackageBinaryOptional(packageName, binName) {
163
+ const packageJsonPath = resolvePackageJsonPathOptional(packageName);
164
+ if (!packageJsonPath) {
165
+ return null;
166
+ }
167
+ try {
168
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
169
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
170
+ if (!relativeBin) {
171
+ return null;
172
+ }
173
+ const resolvedBin = resolve(dirname(packageJsonPath), relativeBin);
174
+ if (existsSync(resolvedBin) || !relativeBin.startsWith("../")) {
175
+ return resolvedBin;
176
+ }
177
+ const packageLocalFallback = resolve(dirname(packageJsonPath), relativeBin.replace(/^\.\.\//u, ""));
178
+ return existsSync(packageLocalFallback) ? packageLocalFallback : resolvedBin;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
132
183
  function resolvePackageRoot(packageName) {
133
184
  return dirname(resolvePackageJsonPath(packageName));
134
185
  }
@@ -260,7 +311,7 @@ function resolveTreeseedToolBinary(toolName, options = {}) {
260
311
  }
261
312
  const npmTool = findNpmTool(toolName);
262
313
  if (npmTool) {
263
- return resolvePackageBinary(npmTool.packageName, npmTool.binName);
314
+ return resolvePackageBinaryOptional(npmTool.packageName, npmTool.binName);
264
315
  }
265
316
  if (toolName === "git" || toolName === "docker") {
266
317
  return locateSystemBinary(toolName, spawnSync, options.env ?? process.env);
@@ -277,6 +328,60 @@ function resolveTreeseedToolCommand(toolName, options = {}) {
277
328
  }
278
329
  return { command: binaryPath, argsPrefix: [], binaryPath };
279
330
  }
331
+ function invocationForTool(toolName, env = process.env) {
332
+ const command = resolveTreeseedToolCommand(toolName, { env });
333
+ if (!command) {
334
+ return {
335
+ mode: "unavailable",
336
+ command: null,
337
+ argsPrefix: [],
338
+ binaryPath: null
339
+ };
340
+ }
341
+ return {
342
+ mode: findNpmTool(toolName) ? "node" : "direct",
343
+ command: command.command,
344
+ argsPrefix: command.argsPrefix,
345
+ binaryPath: command.binaryPath
346
+ };
347
+ }
348
+ function checkGitHubAuth(options) {
349
+ const env = tokenEnv(options.env ?? process.env);
350
+ const gh = resolveTreeseedToolCommand("gh", { env });
351
+ const command = gh ? [gh.command, ...gh.argsPrefix, "auth", "status", "--hostname", "github.com"] : [];
352
+ const remediation = [
353
+ "Run `npx trsd install --json` to install or inspect managed tools.",
354
+ "Run `npx trsd secrets:unlock` or provide TREESEED_KEY_PASSPHRASE so machine secrets can be decrypted.",
355
+ "Verify GH_TOKEN is configured in machine.yaml or the environment."
356
+ ];
357
+ if (!gh) {
358
+ return {
359
+ checked: true,
360
+ authenticated: false,
361
+ binaryPath: null,
362
+ command,
363
+ detail: "GitHub CLI `gh` is unavailable.",
364
+ remediation
365
+ };
366
+ }
367
+ const result = (options.spawn ?? spawnSync)(gh.command, [...gh.argsPrefix, "auth", "status", "--hostname", "github.com"], {
368
+ cwd: options.tenantRoot,
369
+ env: createTreeseedManagedToolEnv(env),
370
+ stdio: "pipe",
371
+ encoding: "utf8",
372
+ timeout: 15e3
373
+ });
374
+ const detail = redactSensitiveOutput(`${result.stderr ?? ""}
375
+ ${result.stdout ?? ""}`.trim()) || (result.status === 0 ? "GitHub CLI authentication succeeded." : "GitHub CLI authentication failed.");
376
+ return {
377
+ checked: true,
378
+ authenticated: result.status === 0,
379
+ binaryPath: gh.binaryPath,
380
+ command,
381
+ detail,
382
+ remediation
383
+ };
384
+ }
280
385
  async function defaultDownloadFile(url, targetPath) {
281
386
  const request = url.startsWith("https:") ? httpsRequest : httpRequest;
282
387
  await new Promise((resolvePromise, rejectPromise) => {
@@ -441,16 +546,16 @@ async function installGh(options) {
441
546
  }
442
547
  function statusForNpmTool(tool, options) {
443
548
  try {
444
- const binaryPath = resolvePackageBinary(tool.packageName, tool.binName);
549
+ const binaryPath = resolvePackageBinaryOptional(tool.packageName, tool.binName);
445
550
  return report({
446
551
  name: tool.name,
447
552
  kind: "npm",
448
553
  version: tool.version,
449
554
  source: "package",
450
555
  binaryPath,
451
- status: existsSync(binaryPath) ? "already-present" : "missing",
556
+ status: binaryPath && existsSync(binaryPath) ? "already-present" : "missing",
452
557
  required: true,
453
- detail: existsSync(binaryPath) ? `${tool.packageName} is available from the Treeseed SDK dependency graph.` : `${tool.packageName} binary ${tool.binName} is missing from the installed package.`
558
+ detail: binaryPath && existsSync(binaryPath) ? `${tool.packageName} is available from the Treeseed SDK dependency graph.` : `${tool.packageName} binary ${tool.binName} is missing from the installed package.`
454
559
  });
455
560
  } catch (error) {
456
561
  return report({
@@ -641,6 +746,21 @@ function collectTreeseedDependencyStatus(options = {}) {
641
746
  reports
642
747
  };
643
748
  }
749
+ function collectTreeseedToolStatus(options = {}) {
750
+ const env = options.env ?? process.env;
751
+ const status = collectTreeseedDependencyStatus(options);
752
+ const tools = status.reports.map((entry) => ({
753
+ ...entry,
754
+ invocation: invocationForTool(entry.name, env)
755
+ }));
756
+ return {
757
+ ...status,
758
+ tools,
759
+ auth: {
760
+ github: checkGitHubAuth(options)
761
+ }
762
+ };
763
+ }
644
764
  function formatTreeseedDependencyReport(result) {
645
765
  return [
646
766
  "Treeseed dependency status",
@@ -659,6 +779,7 @@ function formatTreeseedDependencyReport(result) {
659
779
  }
660
780
  export {
661
781
  collectTreeseedDependencyStatus,
782
+ collectTreeseedToolStatus,
662
783
  createTreeseedManagedToolEnv,
663
784
  formatTreeseedDependencyFailureDetails,
664
785
  formatTreeseedDependencyReport,
@@ -56,6 +56,7 @@ import { run } from "../../operations/services/workspace-tools.js";
56
56
  import { resolveTreeseedWorkflowState } from "../../workflow-state.js";
57
57
  import { TreeseedWorkflowError, TreeseedWorkflowSdk } from "../../workflow.js";
58
58
  import {
59
+ collectTreeseedToolStatus,
59
60
  formatTreeseedDependencyReport,
60
61
  installTreeseedDependencies
61
62
  } from "../../managed-dependencies.js";
@@ -353,6 +354,39 @@ class InstallOperation extends BaseOperation {
353
354
  });
354
355
  }
355
356
  }
357
+ class ToolsOperation extends BaseOperation {
358
+ async execute(_input, context) {
359
+ const result = collectTreeseedToolStatus({
360
+ tenantRoot: context.cwd,
361
+ env: operationEnv(context),
362
+ spawn: context.spawn
363
+ });
364
+ const stdout = [
365
+ "Treeseed managed tools",
366
+ `Tools home: ${result.toolsHome}`,
367
+ `GitHub CLI config: ${result.ghConfigDir}`,
368
+ ...result.tools.map((entry) => {
369
+ const invocation = entry.invocation.command ? `${entry.invocation.command}${entry.invocation.argsPrefix.length > 0 ? ` ${entry.invocation.argsPrefix.join(" ")}` : ""}` : "(unavailable)";
370
+ return `- ${entry.name}: ${entry.status} (${entry.binaryPath ?? "no binary"}; ${entry.invocation.mode}; ${invocation})`;
371
+ }),
372
+ `GitHub auth: ${result.auth.github.authenticated ? "authenticated" : "not authenticated"} - ${result.auth.github.detail}`
373
+ ];
374
+ return operationResult(this.metadata, result, {
375
+ ok: true,
376
+ exitCode: 0,
377
+ stdout,
378
+ report: {
379
+ ok: true,
380
+ dependenciesOk: result.ok,
381
+ toolsHome: result.toolsHome,
382
+ ghConfigDir: result.ghConfigDir,
383
+ npmInstalls: result.npmInstalls,
384
+ tools: result.tools,
385
+ auth: result.auth
386
+ }
387
+ });
388
+ }
389
+ }
356
390
  class AuthLoginOperation extends BaseOperation {
357
391
  async execute(input, context) {
358
392
  const tenantRoot = context.cwd;
@@ -609,6 +643,7 @@ class DefaultTreeseedOperationsProvider {
609
643
  new SyncTemplateOperation("sync"),
610
644
  new DoctorOperation("doctor"),
611
645
  new InstallOperation("install"),
646
+ new ToolsOperation("tools"),
612
647
  new AuthLoginOperation("auth:login"),
613
648
  new AuthLogoutOperation("auth:logout"),
614
649
  new AuthWhoAmIOperation("auth:whoami"),
@@ -69,6 +69,7 @@ const MACHINE_CONFIG_RELATIVE_PATH = ".treeseed/config/machine.yaml";
69
69
  const MACHINE_KEY_HOME_RELATIVE_PATH = ".treeseed/config/machine.key";
70
70
  const LEGACY_MACHINE_KEY_RELATIVE_PATH = ".treeseed/config/machine.key";
71
71
  const REMOTE_AUTH_RELATIVE_PATH = ".treeseed/config/remote-auth.json";
72
+ const WORKTREE_METADATA_RELATIVE_PATH = ".treeseed/worktree.json";
72
73
  const TEMPLATE_CATALOG_CACHE_RELATIVE_PATH = "treeseed/cache/template-catalog.json";
73
74
  const TENANT_ENVIRONMENT_OVERLAY_PATH = "src/env.yaml";
74
75
  const CLOUDFLARE_ACCOUNT_ID_PLACEHOLDER = "replace-with-cloudflare-account-id";
@@ -243,14 +244,26 @@ function findNearestTreeseedMachineConfig(startRoot = process.cwd()) {
243
244
  return null;
244
245
  }
245
246
  function getTreeseedMachineConfigPaths(tenantRoot) {
247
+ const configRoot = resolveManagedWorktreeMachineConfigRoot(tenantRoot);
246
248
  const homeRoot = process.env.HOME && process.env.HOME.trim().length > 0 ? process.env.HOME : homedir();
247
249
  return {
248
- configPath: resolve(tenantRoot, MACHINE_CONFIG_RELATIVE_PATH),
249
- authPath: resolve(tenantRoot, REMOTE_AUTH_RELATIVE_PATH),
250
+ configPath: resolve(configRoot, MACHINE_CONFIG_RELATIVE_PATH),
251
+ authPath: resolve(configRoot, REMOTE_AUTH_RELATIVE_PATH),
250
252
  keyPath: resolve(homeRoot, MACHINE_KEY_HOME_RELATIVE_PATH),
251
- legacyKeyPath: resolve(tenantRoot, LEGACY_MACHINE_KEY_RELATIVE_PATH)
253
+ legacyKeyPath: resolve(configRoot, LEGACY_MACHINE_KEY_RELATIVE_PATH)
252
254
  };
253
255
  }
256
+ function resolveManagedWorktreeMachineConfigRoot(tenantRoot) {
257
+ const metadataPath = resolve(tenantRoot, WORKTREE_METADATA_RELATIVE_PATH);
258
+ if (!existsSync(metadataPath)) return tenantRoot;
259
+ try {
260
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8")) ?? {};
261
+ const primaryRoot = metadata.kind === "treeseed.workflow.worktree" && typeof metadata.primaryRoot === "string" ? metadata.primaryRoot : null;
262
+ return primaryRoot && existsSync(resolve(primaryRoot, MACHINE_CONFIG_RELATIVE_PATH)) ? primaryRoot : tenantRoot;
263
+ } catch {
264
+ return tenantRoot;
265
+ }
266
+ }
254
267
  function keyAgentScriptPath() {
255
268
  return packageScriptPath("key-agent.ts");
256
269
  }
@@ -10,6 +10,7 @@ const DEFAULT_COMPATIBILITY_DATE = "2026-04-05";
10
10
  const DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
11
11
  const GENERATED_ROOT = ".treeseed/generated";
12
12
  const STATE_ROOT = ".treeseed/state";
13
+ const WORKTREE_METADATA_RELATIVE_PATH = ".treeseed/worktree.json";
13
14
  const PERSISTENT_SCOPES = /* @__PURE__ */ new Set(["local", "staging", "prod"]);
14
15
  const MANAGED_SERVICE_KEYS = ["api", "manager", "worker", "workdayStart", "workdayReport"];
15
16
  const TRESEED_ENVELOPE_SCHEMA_GENERATION = "runtime-envelopes-v1";
@@ -204,11 +205,23 @@ function targetDirectoryParts(target) {
204
205
  function targetKey(target) {
205
206
  return target.kind === "persistent" ? target.scope : `branch:${target.branchName}`;
206
207
  }
208
+ function resolveManagedWorktreeStateRoot(tenantRoot) {
209
+ const metadataPath = resolve(tenantRoot, WORKTREE_METADATA_RELATIVE_PATH);
210
+ if (!existsSync(metadataPath)) return tenantRoot;
211
+ try {
212
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8")) ?? {};
213
+ const primaryRoot = metadata.kind === "treeseed.workflow.worktree" && typeof metadata.primaryRoot === "string" ? metadata.primaryRoot : null;
214
+ return primaryRoot && existsSync(resolve(primaryRoot, STATE_ROOT)) ? primaryRoot : tenantRoot;
215
+ } catch {
216
+ return tenantRoot;
217
+ }
218
+ }
207
219
  function resolveTargetPaths(tenantRoot, scopeOrTarget = "prod") {
208
220
  const target = normalizeTarget(scopeOrTarget);
209
221
  const pathParts = targetDirectoryParts(target);
222
+ const stateRoot = resolveManagedWorktreeStateRoot(tenantRoot);
210
223
  const generatedRoot = resolve(tenantRoot, GENERATED_ROOT, ...pathParts);
211
- const statePath = resolve(tenantRoot, STATE_ROOT, ...pathParts, "deploy.json");
224
+ const statePath = resolve(stateRoot, STATE_ROOT, ...pathParts, "deploy.json");
212
225
  return {
213
226
  target,
214
227
  generatedRoot,
@@ -66,6 +66,10 @@ function resolveIgnorePatterns(config) {
66
66
  ".treeseed/exports/**",
67
67
  "**/.treeseed/exports",
68
68
  "**/.treeseed/exports/**",
69
+ ".treeseed/worktrees",
70
+ ".treeseed/worktrees/**",
71
+ "**/.treeseed/worktrees",
72
+ "**/.treeseed/worktrees/**",
69
73
  ...config.export?.ignore ?? []
70
74
  ];
71
75
  }
@@ -21,6 +21,8 @@ export declare function checkoutTaskBranchFromStaging(cwd: any, branchName: any,
21
21
  remoteBranch: boolean;
22
22
  };
23
23
  export declare function syncBranchWithOrigin(repoDir: any, branchName: any): void;
24
+ export declare function checkoutDetachedOriginBranch(repoDir: any, branchName: any): void;
25
+ export declare function pushHeadToBranch(repoDir: any, branchName: any): void;
24
26
  export declare function createFeatureBranchFromStaging(cwd: any, branchName: any): {
25
27
  repoDir: string;
26
28
  branchName: any;
@@ -23,6 +23,20 @@ function repoHasStagedChanges(repoDir) {
23
23
  return true;
24
24
  }
25
25
  }
26
+ function conflictedFiles(repoDir) {
27
+ return runGit(["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir, capture: true }).split("\n").map((line) => line.trim()).filter(Boolean);
28
+ }
29
+ function resolveGeneratedPackageMetadataConflicts(repoDir) {
30
+ const files = conflictedFiles(repoDir);
31
+ if (files.length === 0) return false;
32
+ const generatedMetadataFiles = /* @__PURE__ */ new Set(["package.json", "package-lock.json"]);
33
+ if (files.some((file) => !generatedMetadataFiles.has(file))) {
34
+ return false;
35
+ }
36
+ runGit(["checkout", "--theirs", "--", ...files], { cwd: repoDir });
37
+ runGit(["add", "--", ...files], { cwd: repoDir });
38
+ return true;
39
+ }
26
40
  function headCommit(repoDir, ref = "HEAD") {
27
41
  return runGit(["rev-parse", ref], { cwd: repoDir, capture: true }).trim();
28
42
  }
@@ -77,7 +91,10 @@ function checkoutBranch(repoDir, branchName) {
77
91
  function checkoutTaskBranchFromStaging(cwd, branchName, { createIfMissing = true, pushIfCreated = false } = {}) {
78
92
  const repoDir = assertCleanWorktree(cwd);
79
93
  fetchOrigin(repoDir);
80
- syncBranchWithOrigin(repoDir, STAGING_BRANCH);
94
+ const stagingBaseRef = remoteBranchExists(repoDir, STAGING_BRANCH) ? `origin/${STAGING_BRANCH}` : branchExists(repoDir, STAGING_BRANCH) ? STAGING_BRANCH : null;
95
+ if (!stagingBaseRef) {
96
+ throw new Error(`Base branch "${STAGING_BRANCH}" does not exist locally or on origin.`);
97
+ }
81
98
  if (currentBranch(repoDir) === branchName) {
82
99
  return {
83
100
  repoDir,
@@ -117,8 +134,7 @@ function checkoutTaskBranchFromStaging(cwd, branchName, { createIfMissing = true
117
134
  if (!createIfMissing) {
118
135
  throw new Error(`Branch "${branchName}" does not exist locally or on origin.`);
119
136
  }
120
- checkoutBranch(repoDir, STAGING_BRANCH);
121
- runGit(["checkout", "-b", branchName], { cwd: repoDir });
137
+ runGit(["checkout", "-b", branchName, stagingBaseRef], { cwd: repoDir });
122
138
  if (pushIfCreated) {
123
139
  pushBranch(repoDir, branchName, { setUpstream: true });
124
140
  }
@@ -142,6 +158,17 @@ function syncBranchWithOrigin(repoDir, branchName) {
142
158
  runGit(["merge", "--ff-only", `origin/${branchName}`], { cwd: repoDir });
143
159
  }
144
160
  }
161
+ function checkoutDetachedOriginBranch(repoDir, branchName) {
162
+ fetchOrigin(repoDir);
163
+ if (!remoteBranchExists(repoDir, branchName)) {
164
+ throw new Error(`Remote branch "origin/${branchName}" does not exist.`);
165
+ }
166
+ runGit(["checkout", "--detach", `origin/${branchName}`], { cwd: repoDir });
167
+ }
168
+ function pushHeadToBranch(repoDir, branchName) {
169
+ ensureWritableOrigin(repoDir);
170
+ runGit(["push", "origin", `HEAD:${branchName}`], { cwd: repoDir });
171
+ }
145
172
  function createFeatureBranchFromStaging(cwd, branchName) {
146
173
  const result = checkoutTaskBranchFromStaging(cwd, branchName, {
147
174
  createIfMissing: true,
@@ -209,7 +236,13 @@ function squashMergeBranchIntoStaging(cwd, featureBranch, message, { pushTarget
209
236
  const repoDir = assertCleanWorktree(cwd);
210
237
  fetchOrigin(repoDir);
211
238
  syncBranchWithOrigin(repoDir, STAGING_BRANCH);
212
- runGit(["merge", "--squash", featureBranch], { cwd: repoDir });
239
+ try {
240
+ runGit(["merge", "--squash", featureBranch], { cwd: repoDir });
241
+ } catch (error) {
242
+ if (!resolveGeneratedPackageMetadataConflicts(repoDir)) {
243
+ throw error;
244
+ }
245
+ }
213
246
  let committed = false;
214
247
  if (repoHasStagedChanges(repoDir)) {
215
248
  runGit(["commit", "-m", message], { cwd: repoDir });
@@ -349,6 +382,7 @@ export {
349
382
  assertFeatureBranch,
350
383
  branchExists,
351
384
  checkoutBranch,
385
+ checkoutDetachedOriginBranch,
352
386
  checkoutTaskBranchFromStaging,
353
387
  createDeprecatedTaskTag,
354
388
  createFeatureBranchFromStaging,
@@ -367,6 +401,7 @@ export {
367
401
  mergeStagingIntoMain,
368
402
  prepareReleaseBranches,
369
403
  pushBranch,
404
+ pushHeadToBranch,
370
405
  remoteBranchExists,
371
406
  squashMergeBranchIntoStaging,
372
407
  syncBranchWithOrigin,
@@ -27,6 +27,13 @@ export interface GitHubWorkflowRunSummary {
27
27
  headSha: string | null;
28
28
  headBranch: string | null;
29
29
  }
30
+ export interface GitHubWorkflowJobSummary {
31
+ id: number;
32
+ name: string;
33
+ status: string | null;
34
+ conclusion: string | null;
35
+ url: string | null;
36
+ }
30
37
  export declare function resolveGitHubApiToken(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
31
38
  export declare function parseGitHubRepositorySlug(value: string): {
32
39
  owner: string;
@@ -131,8 +138,11 @@ export declare function waitForGitHubWorkflowRunCompletion(repository: string |
131
138
  workflow: string;
132
139
  runId: number;
133
140
  headSha: string | null;
141
+ branch: string | null;
134
142
  conclusion: string | null;
135
143
  url: string | null;
144
+ jobs: GitHubWorkflowJobSummary[];
145
+ failedJobs: GitHubWorkflowJobSummary[];
136
146
  }>;
137
147
  export declare function ensureGitHubBranchFromBase(repository: string | {
138
148
  owner: string;
@@ -523,6 +523,15 @@ function normalizeWorkflowRun(run) {
523
523
  headBranch: typeof run.head_branch === "string" ? run.head_branch : null
524
524
  };
525
525
  }
526
+ function normalizeWorkflowJob(job) {
527
+ return {
528
+ id: Number(job.id ?? 0),
529
+ name: String(job.name ?? ""),
530
+ status: typeof job.status === "string" ? job.status : null,
531
+ conclusion: typeof job.conclusion === "string" ? job.conclusion : null,
532
+ url: typeof job.html_url === "string" ? job.html_url : null
533
+ };
534
+ }
526
535
  function sleep(ms) {
527
536
  return new Promise((resolve) => setTimeout(resolve, ms));
528
537
  }
@@ -557,14 +566,24 @@ async function waitForGitHubWorkflowRunCompletion(repository, {
557
566
  });
558
567
  const normalized = normalizeWorkflowRun(current.data);
559
568
  if (normalized.status === "completed") {
569
+ const jobs = await client.rest.actions.listJobsForWorkflowRun({
570
+ owner,
571
+ repo: name,
572
+ run_id: match.id,
573
+ per_page: 100
574
+ });
575
+ const normalizedJobs = jobs.data.jobs.map((job) => normalizeWorkflowJob(job));
560
576
  return {
561
577
  status: "completed",
562
578
  repository: `${owner}/${name}`,
563
579
  workflow,
564
580
  runId: normalized.id,
565
581
  headSha: normalized.headSha,
582
+ branch: normalized.headBranch,
566
583
  conclusion: normalized.conclusion,
567
- url: normalized.url
584
+ url: normalized.url,
585
+ jobs: normalizedJobs,
586
+ failedJobs: normalizedJobs.filter((job) => job.conclusion && job.conclusion !== "success" && job.conclusion !== "skipped")
568
587
  };
569
588
  }
570
589
  await sleep(pollSeconds * 1e3);
@@ -281,8 +281,11 @@ export declare function waitForGitHubWorkflowCompletion(tenantRoot: any, { repos
281
281
  workflow: string;
282
282
  runId: number;
283
283
  headSha: string | null;
284
+ branch: string | null;
284
285
  conclusion: string | null;
285
286
  url: string | null;
287
+ jobs: import("./github-api.ts").GitHubWorkflowJobSummary[];
288
+ failedJobs: import("./github-api.ts").GitHubWorkflowJobSummary[];
286
289
  } | {
287
290
  status: string;
288
291
  reason: string;
@@ -540,14 +540,14 @@ function applyPackageVersion(node, version) {
540
540
  function shouldSkipNetworkInstall() {
541
541
  return getGitHubAutomationMode() === "stub" || process.env.TREESEED_SAVE_NPM_INSTALL_MODE === "skip";
542
542
  }
543
- function shouldSkipGitDependencySmoke() {
544
- return shouldSkipNetworkInstall() || process.env.TREESEED_GIT_DEPENDENCY_SMOKE === "skip";
543
+ function shouldSkipGitDependencySmoke(options) {
544
+ return shouldSkipNetworkInstall() || process.env.TREESEED_GIT_DEPENDENCY_SMOKE === "skip" || options?.verifyMode === "skip";
545
545
  }
546
546
  function hasNpmLockfile(repoDir) {
547
547
  return existsSync(resolve(repoDir, "package-lock.json")) || existsSync(resolve(repoDir, "npm-shrinkwrap.json"));
548
548
  }
549
549
  async function runGitDependencySmoke(node, options, reference) {
550
- if (reference.mode !== "dev-git-tag" || shouldSkipGitDependencySmoke()) return;
550
+ if (reference.mode !== "dev-git-tag" || shouldSkipGitDependencySmoke(options)) return;
551
551
  const installSpec = reference.installSpec ?? reference.spec;
552
552
  const tempRoot = mkdtempSync(resolve(tmpdir(), "treeseed-git-dep-smoke-"));
553
553
  const npmCacheRoot = resolve(tempRoot, ".npm-cache");
@@ -593,7 +593,8 @@ async function runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs =
593
593
  let lastError = null;
594
594
  const packageJson = node.packageJson ?? (existsSync(resolve(node.path, "package.json")) ? readJson(resolve(node.path, "package.json")) : null);
595
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"];
596
+ const installFlags = ["--package-lock-only", "--ignore-scripts"];
597
+ 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"];
597
598
  for (let attempt = 1; attempt <= 5; attempt += 1) {
598
599
  emitProgress(options, node, "install", `npm ${args.join(" ")} attempt ${attempt}/5.`);
599
600
  try {
@@ -1230,6 +1231,11 @@ async function saveOneRepository(node, options, state) {
1230
1231
  report.install = await runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs);
1231
1232
  }
1232
1233
  if (hasNpmLockfile(node.path) && (node.kind === "project" || packageNeedsVersion || dependencyChanged || submodulesChanged)) {
1234
+ const lockfileIssues = collectDeploymentLockfileWorkspaceIssues(node.path);
1235
+ if (node.kind === "project" && lockfileIssues.length > 0 && !shouldSkipNetworkInstall()) {
1236
+ emitProgress(options, node, "lockfile", "Refreshing package-lock.json before validation.");
1237
+ report.install = await runNpmInstallWithRetry(node, options, gitDependencyRefreshSpecs);
1238
+ }
1233
1239
  report.lockfileValidation = await validateRepositoryLockfile(node, options);
1234
1240
  }
1235
1241
  const dirty = hasMeaningfulChanges(node.path);