@treeseed/sdk 0.6.9 → 0.6.10
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 +1 -1
- package/dist/index.js +2 -0
- package/dist/managed-dependencies.d.ts +23 -0
- package/dist/managed-dependencies.js +133 -12
- package/dist/operations/providers/default.js +35 -0
- package/dist/operations/services/config-runtime.js +16 -3
- package/dist/operations/services/deploy.js +14 -1
- package/dist/operations/services/export-runtime.js +4 -0
- package/dist/operations/services/git-workflow.d.ts +2 -0
- package/dist/operations/services/git-workflow.js +39 -4
- package/dist/operations/services/github-api.d.ts +10 -0
- package/dist/operations/services/github-api.js +20 -1
- package/dist/operations/services/github-automation.d.ts +3 -0
- package/dist/operations/services/repository-save-orchestrator.js +10 -4
- package/dist/operations/services/workspace-dependency-mode.js +10 -18
- package/dist/operations-registry.js +1 -0
- package/dist/scripts/patch-starlight-content-path.js +2 -1
- package/dist/workflow/operations.d.ts +259 -429
- package/dist/workflow/operations.js +652 -72
- package/dist/workflow/runs.d.ts +38 -0
- package/dist/workflow/runs.js +182 -15
- package/dist/workflow/worktrees.d.ts +39 -0
- package/dist/workflow/worktrees.js +224 -0
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.js +35 -2
- package/dist/workflow-support.d.ts +1 -1
- package/dist/workflow-support.js +2 -0
- package/dist/workflow.d.ts +14 -1
- package/package.json +1 -1
- package/templates/github/deploy.workflow.yml +100 -5
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 ?? "")
|
|
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
|
-
|
|
127
|
+
const packageJsonPath = require2.resolve(`${packageName}/package.json`);
|
|
128
|
+
if (existsSync(packageJsonPath)) {
|
|
129
|
+
return packageJsonPath;
|
|
130
|
+
}
|
|
108
131
|
} catch {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
249
|
-
authPath: resolve(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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);
|