@treeseed/sdk 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/copilot.d.ts +15 -0
  2. package/dist/copilot.js +75 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +18 -0
  5. package/dist/managed-dependencies.d.ts +56 -0
  6. package/dist/managed-dependencies.js +668 -0
  7. package/dist/operations/providers/default.js +30 -1
  8. package/dist/operations/services/commit-message-provider.d.ts +33 -0
  9. package/dist/operations/services/commit-message-provider.js +319 -0
  10. package/dist/operations/services/config-runtime.js +41 -20
  11. package/dist/operations/services/git-remote-policy.d.ts +9 -0
  12. package/dist/operations/services/git-remote-policy.js +55 -0
  13. package/dist/operations/services/git-workflow.js +22 -3
  14. package/dist/operations/services/github-api.js +9 -4
  15. package/dist/operations/services/knowledge-coop-launch.js +4 -0
  16. package/dist/operations/services/local-dev.js +7 -2
  17. package/dist/operations/services/package-reference-policy.d.ts +70 -0
  18. package/dist/operations/services/package-reference-policy.js +314 -0
  19. package/dist/operations/services/project-platform.d.ts +4 -0
  20. package/dist/operations/services/project-platform.js +30 -4
  21. package/dist/operations/services/railway-deploy.d.ts +4 -1
  22. package/dist/operations/services/railway-deploy.js +76 -38
  23. package/dist/operations/services/repository-save-orchestrator.d.ts +172 -0
  24. package/dist/operations/services/repository-save-orchestrator.js +1462 -0
  25. package/dist/operations/services/workspace-dependency-mode.d.ts +70 -0
  26. package/dist/operations/services/workspace-dependency-mode.js +404 -0
  27. package/dist/operations/services/workspace-preflight.js +5 -0
  28. package/dist/operations/services/workspace-save.js +10 -6
  29. package/dist/operations-registry.js +5 -0
  30. package/dist/operations-types.d.ts +1 -0
  31. package/dist/platform/books-data.js +4 -1
  32. package/dist/platform/env.yaml +6 -3
  33. package/dist/reconcile/builtin-adapters.js +37 -7
  34. package/dist/scripts/cleanup-markdown.js +4 -0
  35. package/dist/scripts/publish-package.js +5 -0
  36. package/dist/scripts/tenant-workflow-action.js +11 -2
  37. package/dist/verification.js +24 -12
  38. package/dist/workflow/operations.d.ts +381 -55
  39. package/dist/workflow/operations.js +718 -258
  40. package/dist/workflow-state.d.ts +40 -1
  41. package/dist/workflow-state.js +220 -17
  42. package/dist/workflow-support.d.ts +3 -0
  43. package/dist/workflow-support.js +34 -0
  44. package/dist/workflow.d.ts +19 -3
  45. package/dist/workflow.js +3 -3
  46. package/dist/wrangler-d1.js +6 -1
  47. package/package.json +17 -1
  48. package/templates/github/deploy.workflow.yml +28 -13
@@ -0,0 +1,668 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createWriteStream, existsSync, mkdirSync, rmSync, renameSync, chmodSync, copyFileSync, readFileSync, readdirSync } from "node:fs";
3
+ import { request as httpRequest } from "node:http";
4
+ import { request as httpsRequest } from "node:https";
5
+ import { homedir, platform as osPlatform, arch as osArch } from "node:os";
6
+ import { basename, dirname, resolve } from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { createRequire } from "node:module";
9
+ const require2 = createRequire(import.meta.url);
10
+ const GH_VERSION = "2.90.0";
11
+ const GH_CHECKSUMS_SHA256 = "95cbb66008dc467cf402724025f07551d2a949b3cc830146206a2797b963966c";
12
+ const GH_RELEASE_BASE_URL = `https://github.com/cli/cli/releases/download/v${GH_VERSION}`;
13
+ const GH_ASSETS = [
14
+ { platform: "linux", arch: "x64", assetName: `gh_${GH_VERSION}_linux_amd64.tar.gz`, archiveKind: "tar.gz" },
15
+ { platform: "linux", arch: "arm64", assetName: `gh_${GH_VERSION}_linux_arm64.tar.gz`, archiveKind: "tar.gz" },
16
+ { platform: "darwin", arch: "x64", assetName: `gh_${GH_VERSION}_macOS_amd64.zip`, archiveKind: "zip" },
17
+ { platform: "darwin", arch: "arm64", assetName: `gh_${GH_VERSION}_macOS_arm64.zip`, archiveKind: "zip" }
18
+ ];
19
+ const NPM_TOOLS = [
20
+ { name: "wrangler", packageName: "wrangler", binName: "wrangler", version: "4.86.0" },
21
+ { name: "railway", packageName: "@railway/cli", binName: "railway", version: "4.44.0" },
22
+ { name: "copilot", packageName: "@github/copilot", binName: "copilot", version: "1.0.39" },
23
+ { name: "copilot-language-server", packageName: "@github/copilot-language-server", binName: "copilot-language-server", version: "1.480.0" }
24
+ ];
25
+ const NPM_PACKAGES = [
26
+ { name: "copilot-sdk", packageName: "@github/copilot-sdk", version: "0.3.0" }
27
+ ];
28
+ function report(input) {
29
+ return {
30
+ binaryPath: input.binaryPath ?? null,
31
+ ...input
32
+ };
33
+ }
34
+ function sha256File(filePath) {
35
+ return createHash("sha256").update(readFileSync(filePath)).digest("hex");
36
+ }
37
+ function resolveToolsHome(env = process.env) {
38
+ if (env.TREESEED_TOOLS_HOME?.trim()) {
39
+ return resolve(env.TREESEED_TOOLS_HOME);
40
+ }
41
+ if (env.XDG_CACHE_HOME?.trim()) {
42
+ return resolve(env.XDG_CACHE_HOME, "treeseed", "tools");
43
+ }
44
+ return resolve(homedir(), ".cache", "treeseed", "tools");
45
+ }
46
+ function createTreeseedManagedToolEnv(env = process.env) {
47
+ const toolsHome = resolveToolsHome(env);
48
+ const ghBinDir = resolve(toolsHome, "gh", GH_VERSION, platformKey(), "bin");
49
+ const pathKey = process.platform === "win32" ? "Path" : "PATH";
50
+ const existingPath = env[pathKey] ?? env.PATH ?? "";
51
+ return {
52
+ ...env,
53
+ GH_CONFIG_DIR: env.TREESEED_GH_CONFIG_DIR ?? resolve(toolsHome, "gh-config"),
54
+ GH_PROMPT_DISABLED: "1",
55
+ GH_NO_UPDATE_NOTIFIER: "1",
56
+ [pathKey]: [ghBinDir, existingPath].filter(Boolean).join(process.platform === "win32" ? ";" : ":")
57
+ };
58
+ }
59
+ function platformKey(platform = osPlatform(), arch = osArch()) {
60
+ return `${platform}-${arch}`;
61
+ }
62
+ function currentPlatformAsset() {
63
+ const platform = osPlatform();
64
+ const arch = osArch();
65
+ if (platform !== "linux" && platform !== "darwin") {
66
+ return null;
67
+ }
68
+ if (arch !== "x64" && arch !== "arm64") {
69
+ return null;
70
+ }
71
+ return GH_ASSETS.find((asset) => asset.platform === platform && asset.arch === arch) ?? null;
72
+ }
73
+ function managedGhBin(env = process.env) {
74
+ return resolve(resolveToolsHome(env), "gh", GH_VERSION, platformKey(), "bin", "gh");
75
+ }
76
+ function locateSystemBinary(command, spawn = spawnSync, env = process.env) {
77
+ if (process.platform === "win32") {
78
+ return null;
79
+ }
80
+ const result = spawn("bash", ["-lc", `command -v ${command}`], {
81
+ stdio: "pipe",
82
+ encoding: "utf8",
83
+ env
84
+ });
85
+ return result.status === 0 ? String(result.stdout ?? "").trim() || null : null;
86
+ }
87
+ function checkCommand(command, args, options = {}) {
88
+ const run = options.spawn ?? spawnSync;
89
+ const result = run(command, args, {
90
+ cwd: options.cwd,
91
+ env: options.env,
92
+ stdio: "pipe",
93
+ encoding: "utf8",
94
+ timeout: 15e3
95
+ });
96
+ return {
97
+ ok: result.status === 0,
98
+ status: result.status ?? 1,
99
+ stdout: String(result.stdout ?? "").trim(),
100
+ stderr: String(result.stderr ?? "").trim(),
101
+ detail: `${result.stderr ?? ""}
102
+ ${result.stdout ?? ""}`.trim() || result.error?.message || ""
103
+ };
104
+ }
105
+ function resolvePackageJsonPath(packageName) {
106
+ try {
107
+ return require2.resolve(`${packageName}/package.json`);
108
+ } 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
+ }
114
+ }
115
+ throw new Error(`Unable to resolve package manifest for "${packageName}".`);
116
+ }
117
+ }
118
+ function resolvePackageBinary(packageName, binName) {
119
+ const packageJsonPath = resolvePackageJsonPath(packageName);
120
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
121
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName];
122
+ if (!relativeBin) {
123
+ throw new Error(`Unable to resolve binary "${binName}" from package "${packageName}".`);
124
+ }
125
+ const resolvedBin = resolve(dirname(packageJsonPath), relativeBin);
126
+ if (existsSync(resolvedBin) || !relativeBin.startsWith("../")) {
127
+ return resolvedBin;
128
+ }
129
+ const packageLocalFallback = resolve(dirname(packageJsonPath), relativeBin.replace(/^\.\.\//u, ""));
130
+ return existsSync(packageLocalFallback) ? packageLocalFallback : resolvedBin;
131
+ }
132
+ function resolvePackageRoot(packageName) {
133
+ return dirname(resolvePackageJsonPath(packageName));
134
+ }
135
+ function findNpmTool(name) {
136
+ return NPM_TOOLS.find((tool) => tool.name === name) ?? null;
137
+ }
138
+ function npmBackedDependenciesAvailable() {
139
+ try {
140
+ for (const tool of NPM_TOOLS) {
141
+ const binaryPath = resolvePackageBinary(tool.packageName, tool.binName);
142
+ if (!existsSync(binaryPath)) {
143
+ return false;
144
+ }
145
+ }
146
+ for (const pkg of NPM_PACKAGES) {
147
+ const packageRoot = resolvePackageRoot(pkg.packageName);
148
+ if (!existsSync(packageRoot)) {
149
+ return false;
150
+ }
151
+ }
152
+ return true;
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+ function resolveNpmInstallCommand(env = process.env) {
158
+ const npmCommandOverride = env.TREESEED_NPM_INSTALL_COMMAND;
159
+ if (npmCommandOverride?.trim()) {
160
+ return {
161
+ command: npmCommandOverride,
162
+ args: ["install", "--no-audit", "--no-fund"],
163
+ display: [npmCommandOverride, "install", "--no-audit", "--no-fund"]
164
+ };
165
+ }
166
+ const npmExecPath = env.npm_execpath || env.NPM_EXEC_PATH;
167
+ if (npmExecPath?.trim()) {
168
+ return {
169
+ command: process.execPath,
170
+ args: [npmExecPath, "install", "--no-audit", "--no-fund"],
171
+ display: [process.execPath, npmExecPath, "install", "--no-audit", "--no-fund"]
172
+ };
173
+ }
174
+ return {
175
+ command: "npm",
176
+ args: ["install", "--no-audit", "--no-fund"],
177
+ display: ["npm", "install", "--no-audit", "--no-fund"]
178
+ };
179
+ }
180
+ function runNpmBootstrap(options) {
181
+ const tenantRoot = options.tenantRoot ? resolve(options.tenantRoot) : null;
182
+ const npmCommand = resolveNpmInstallCommand(options.env);
183
+ if (!tenantRoot || !existsSync(resolve(tenantRoot, "package.json"))) {
184
+ return [{
185
+ root: tenantRoot,
186
+ command: npmCommand.display,
187
+ status: "skipped",
188
+ exitCode: null,
189
+ detail: tenantRoot ? `No package.json found in ${tenantRoot}; npm install skipped.` : "No tenant root was provided; npm install skipped."
190
+ }];
191
+ }
192
+ if (options.env.TREESEED_MANAGED_NPM_INSTALL === "1") {
193
+ return [{
194
+ root: tenantRoot,
195
+ command: npmCommand.display,
196
+ status: "skipped",
197
+ exitCode: null,
198
+ detail: "npm install skipped because TREESEED_MANAGED_NPM_INSTALL=1 is set."
199
+ }];
200
+ }
201
+ const nodeModulesMissing = !existsSync(resolve(tenantRoot, "node_modules"));
202
+ const npmDepsMissing = !npmBackedDependenciesAvailable();
203
+ if (!options.force && !nodeModulesMissing && !npmDepsMissing) {
204
+ return [{
205
+ root: tenantRoot,
206
+ command: npmCommand.display,
207
+ status: "already-present",
208
+ exitCode: 0,
209
+ detail: "npm dependencies are already installed."
210
+ }];
211
+ }
212
+ options.write?.(`Installing npm dependencies in ${tenantRoot}...`);
213
+ const testNpmInstallStatus = options.env.NODE_ENV === "test" ? options.env.TREESEED_TEST_NPM_INSTALL_STATUS : void 0;
214
+ if (testNpmInstallStatus === "installed" || testNpmInstallStatus === "failed") {
215
+ const ok2 = testNpmInstallStatus === "installed";
216
+ return [{
217
+ root: tenantRoot,
218
+ command: npmCommand.display,
219
+ status: ok2 ? "installed" : "failed",
220
+ exitCode: ok2 ? 0 : 1,
221
+ detail: ok2 ? "npm install completed successfully." : "npm install failed."
222
+ }];
223
+ }
224
+ const result = options.spawn(npmCommand.command, npmCommand.args, {
225
+ cwd: tenantRoot,
226
+ env: {
227
+ ...options.env,
228
+ TREESEED_MANAGED_NPM_INSTALL: "1"
229
+ },
230
+ stdio: "pipe",
231
+ encoding: "utf8"
232
+ });
233
+ const detail = `${result.stderr ?? ""}
234
+ ${result.stdout ?? ""}`.trim() || result.error?.message || "";
235
+ const ok = result.status === 0 && !result.error;
236
+ return [{
237
+ root: tenantRoot,
238
+ command: npmCommand.display,
239
+ status: ok ? "installed" : "failed",
240
+ exitCode: result.status ?? 1,
241
+ detail: ok ? detail || "npm install completed successfully." : detail || "npm install failed."
242
+ }];
243
+ }
244
+ function formatTreeseedDependencyFailureDetails(result) {
245
+ const npmFailures = result.npmInstalls.filter((entry) => entry.status === "failed").map((entry) => {
246
+ const root = entry.root ?? "no tenant root";
247
+ const exit = entry.exitCode === null ? "unknown exit code" : `exit code ${entry.exitCode}`;
248
+ return `npm install in ${root}: ${entry.command.join(" ")} failed with ${exit}${entry.detail ? `: ${entry.detail}` : ""}`;
249
+ });
250
+ const toolFailures = result.reports.filter((entry) => entry.required && ["failed", "missing", "unsupported"].includes(entry.status)).map((entry) => `${entry.name}: ${entry.detail}`);
251
+ return [...npmFailures, ...toolFailures].join("\n- ") || "No dependency failure details were reported.";
252
+ }
253
+ function resolveTreeseedToolBinary(toolName, options = {}) {
254
+ if (toolName === "gh") {
255
+ const managed = managedGhBin(options.env);
256
+ if (existsSync(managed)) {
257
+ return managed;
258
+ }
259
+ return locateSystemBinary("gh", spawnSync, options.env ?? process.env);
260
+ }
261
+ const npmTool = findNpmTool(toolName);
262
+ if (npmTool) {
263
+ return resolvePackageBinary(npmTool.packageName, npmTool.binName);
264
+ }
265
+ if (toolName === "git" || toolName === "docker") {
266
+ return locateSystemBinary(toolName, spawnSync, options.env ?? process.env);
267
+ }
268
+ return null;
269
+ }
270
+ function resolveTreeseedToolCommand(toolName, options = {}) {
271
+ const binaryPath = resolveTreeseedToolBinary(toolName, options);
272
+ if (!binaryPath) {
273
+ return null;
274
+ }
275
+ if (findNpmTool(toolName)) {
276
+ return { command: process.execPath, argsPrefix: [binaryPath], binaryPath };
277
+ }
278
+ return { command: binaryPath, argsPrefix: [], binaryPath };
279
+ }
280
+ async function defaultDownloadFile(url, targetPath) {
281
+ const request = url.startsWith("https:") ? httpsRequest : httpRequest;
282
+ await new Promise((resolvePromise, rejectPromise) => {
283
+ const req = request(url, (response) => {
284
+ const status = response.statusCode ?? 0;
285
+ const location = response.headers.location;
286
+ if (status >= 300 && status < 400 && location) {
287
+ response.resume();
288
+ defaultDownloadFile(new URL(location, url).toString(), targetPath).then(resolvePromise, rejectPromise);
289
+ return;
290
+ }
291
+ if (status < 200 || status >= 300) {
292
+ response.resume();
293
+ rejectPromise(new Error(`Download failed (${status}) for ${url}`));
294
+ return;
295
+ }
296
+ mkdirSync(dirname(targetPath), { recursive: true });
297
+ const output = createWriteStream(targetPath);
298
+ response.pipe(output);
299
+ output.on("finish", () => {
300
+ output.close();
301
+ resolvePromise();
302
+ });
303
+ output.on("error", rejectPromise);
304
+ });
305
+ req.on("error", rejectPromise);
306
+ req.end();
307
+ });
308
+ }
309
+ function parseChecksums(contents, assetName) {
310
+ for (const line of contents.split(/\r?\n/u)) {
311
+ const [hash, name] = line.trim().split(/\s+/u);
312
+ if (name === assetName && /^[a-f0-9]{64}$/iu.test(hash ?? "")) {
313
+ return hash.toLowerCase();
314
+ }
315
+ }
316
+ return null;
317
+ }
318
+ function findExtractedGhBinary(root) {
319
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
320
+ const fullPath = resolve(root, entry.name);
321
+ if (entry.isDirectory()) {
322
+ const found = findExtractedGhBinary(fullPath);
323
+ if (found) return found;
324
+ continue;
325
+ }
326
+ if (entry.isFile() && basename(fullPath) === "gh" && basename(dirname(fullPath)) === "bin") {
327
+ return fullPath;
328
+ }
329
+ }
330
+ return null;
331
+ }
332
+ async function installGh(options) {
333
+ const asset = currentPlatformAsset();
334
+ if (!asset) {
335
+ return report({
336
+ name: "gh",
337
+ kind: "download",
338
+ version: GH_VERSION,
339
+ source: "not-applicable",
340
+ status: "unsupported",
341
+ required: true,
342
+ detail: "Managed GitHub CLI installation supports Linux and macOS on x64 or arm64."
343
+ });
344
+ }
345
+ const binaryPath = managedGhBin(options.env);
346
+ if (existsSync(binaryPath)) {
347
+ const check = checkCommand(binaryPath, ["--version"], {
348
+ cwd: options.tenantRoot,
349
+ env: createTreeseedManagedToolEnv(options.env),
350
+ spawn: options.spawn
351
+ });
352
+ if (check.ok && check.stdout.includes(GH_VERSION)) {
353
+ return report({
354
+ name: "gh",
355
+ kind: "download",
356
+ version: GH_VERSION,
357
+ source: "managed-cache",
358
+ binaryPath,
359
+ status: options.force ? "repaired" : "already-present",
360
+ required: true,
361
+ detail: check.stdout.split("\n")[0] ?? `GitHub CLI ${GH_VERSION} is installed.`
362
+ });
363
+ }
364
+ }
365
+ const toolsHome = resolveToolsHome(options.env);
366
+ const tmpRoot = resolve(toolsHome, ".tmp", `gh-${process.pid}-${Date.now()}`);
367
+ const archivePath = resolve(tmpRoot, asset.assetName);
368
+ const checksumsPath = resolve(tmpRoot, `gh_${GH_VERSION}_checksums.txt`);
369
+ const extractRoot = resolve(tmpRoot, "extract");
370
+ const installRoot = dirname(dirname(binaryPath));
371
+ const stagingRoot = `${installRoot}.staging-${process.pid}-${Date.now()}`;
372
+ try {
373
+ options.write?.(`Installing GitHub CLI ${GH_VERSION}...`);
374
+ rmSync(tmpRoot, { recursive: true, force: true });
375
+ mkdirSync(extractRoot, { recursive: true });
376
+ await options.downloadFile(`${GH_RELEASE_BASE_URL}/gh_${GH_VERSION}_checksums.txt`, checksumsPath);
377
+ const checksumsHash = sha256File(checksumsPath);
378
+ if (checksumsHash !== GH_CHECKSUMS_SHA256) {
379
+ throw new Error(`GitHub CLI checksums file hash mismatch: expected ${GH_CHECKSUMS_SHA256}, got ${checksumsHash}.`);
380
+ }
381
+ const expectedAssetHash = parseChecksums(readFileSync(checksumsPath, "utf8"), asset.assetName);
382
+ if (!expectedAssetHash) {
383
+ throw new Error(`GitHub CLI checksums file does not contain ${asset.assetName}.`);
384
+ }
385
+ await options.downloadFile(`${GH_RELEASE_BASE_URL}/${asset.assetName}`, archivePath);
386
+ const assetHash = sha256File(archivePath);
387
+ if (assetHash !== expectedAssetHash) {
388
+ throw new Error(`GitHub CLI archive hash mismatch for ${asset.assetName}: expected ${expectedAssetHash}, got ${assetHash}.`);
389
+ }
390
+ if (asset.archiveKind === "zip") {
391
+ const { default: extractZip } = await import("extract-zip");
392
+ await extractZip(archivePath, { dir: extractRoot });
393
+ } else {
394
+ const tar = await import("tar");
395
+ await tar.x({ file: archivePath, cwd: extractRoot });
396
+ }
397
+ const extractedBinary = findExtractedGhBinary(extractRoot);
398
+ if (!extractedBinary) {
399
+ throw new Error(`Unable to find gh binary in ${asset.assetName}.`);
400
+ }
401
+ rmSync(stagingRoot, { recursive: true, force: true });
402
+ mkdirSync(resolve(stagingRoot, "bin"), { recursive: true });
403
+ copyFileSync(extractedBinary, resolve(stagingRoot, "bin", "gh"));
404
+ chmodSync(resolve(stagingRoot, "bin", "gh"), 493);
405
+ rmSync(installRoot, { recursive: true, force: true });
406
+ mkdirSync(dirname(installRoot), { recursive: true });
407
+ renameSync(stagingRoot, installRoot);
408
+ const check = checkCommand(binaryPath, ["--version"], {
409
+ cwd: options.tenantRoot,
410
+ env: createTreeseedManagedToolEnv(options.env),
411
+ spawn: options.spawn
412
+ });
413
+ if (!check.ok) {
414
+ throw new Error(check.detail || "GitHub CLI failed after installation.");
415
+ }
416
+ return report({
417
+ name: "gh",
418
+ kind: "download",
419
+ version: GH_VERSION,
420
+ source: "managed-cache",
421
+ binaryPath,
422
+ status: existsSync(binaryPath) && options.force ? "repaired" : "installed",
423
+ required: true,
424
+ detail: check.stdout.split("\n")[0] ?? `GitHub CLI ${GH_VERSION} installed.`
425
+ });
426
+ } catch (error) {
427
+ return report({
428
+ name: "gh",
429
+ kind: "download",
430
+ version: GH_VERSION,
431
+ source: "managed-cache",
432
+ binaryPath,
433
+ status: "failed",
434
+ required: true,
435
+ detail: error instanceof Error ? error.message : String(error)
436
+ });
437
+ } finally {
438
+ rmSync(tmpRoot, { recursive: true, force: true });
439
+ rmSync(stagingRoot, { recursive: true, force: true });
440
+ }
441
+ }
442
+ function statusForNpmTool(tool, options) {
443
+ try {
444
+ const binaryPath = resolvePackageBinary(tool.packageName, tool.binName);
445
+ return report({
446
+ name: tool.name,
447
+ kind: "npm",
448
+ version: tool.version,
449
+ source: "package",
450
+ binaryPath,
451
+ status: existsSync(binaryPath) ? "already-present" : "missing",
452
+ 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.`
454
+ });
455
+ } catch (error) {
456
+ return report({
457
+ name: tool.name,
458
+ kind: "npm",
459
+ version: tool.version,
460
+ source: "package",
461
+ status: "failed",
462
+ required: true,
463
+ detail: error instanceof Error ? error.message : String(error)
464
+ });
465
+ }
466
+ }
467
+ function statusForNpmPackage(pkg) {
468
+ try {
469
+ const packageRoot = resolvePackageRoot(pkg.packageName);
470
+ return report({
471
+ name: pkg.name,
472
+ kind: "npm",
473
+ version: pkg.version,
474
+ source: "package",
475
+ binaryPath: null,
476
+ status: existsSync(packageRoot) ? "already-present" : "missing",
477
+ required: true,
478
+ detail: existsSync(packageRoot) ? `${pkg.packageName} is available from the Treeseed SDK dependency graph at ${packageRoot}.` : `${pkg.packageName} is missing from the installed package graph.`
479
+ });
480
+ } catch (error) {
481
+ return report({
482
+ name: pkg.name,
483
+ kind: "npm",
484
+ version: pkg.version,
485
+ source: "package",
486
+ status: "failed",
487
+ required: true,
488
+ detail: error instanceof Error ? error.message : String(error)
489
+ });
490
+ }
491
+ }
492
+ function systemStatus(name, required, options) {
493
+ const binaryPath = locateSystemBinary(name, options.spawn ?? spawnSync, options.env ?? process.env);
494
+ return report({
495
+ name,
496
+ kind: "system",
497
+ source: binaryPath ? "system" : "not-applicable",
498
+ binaryPath,
499
+ status: binaryPath ? "already-present" : required ? "missing" : "skipped",
500
+ required,
501
+ detail: binaryPath ? `${name} detected at ${binaryPath}.` : required ? `${name} is required and was not found on PATH.` : `${name} was not found on PATH.`
502
+ });
503
+ }
504
+ function installGhAct(options) {
505
+ const env = createTreeseedManagedToolEnv(options.env ?? process.env);
506
+ const gh = resolveTreeseedToolBinary("gh", { env });
507
+ const docker = locateSystemBinary("docker", options.spawn ?? spawnSync, options.env ?? process.env);
508
+ if (!docker) {
509
+ return report({
510
+ name: "gh-act",
511
+ kind: "extension",
512
+ version: "nektos/gh-act",
513
+ source: "managed-gh-config",
514
+ status: "skipped",
515
+ required: false,
516
+ detail: "Docker is not on PATH, so gh-act installation was skipped."
517
+ });
518
+ }
519
+ if (!gh) {
520
+ return report({
521
+ name: "gh-act",
522
+ kind: "extension",
523
+ version: "nektos/gh-act",
524
+ source: "managed-gh-config",
525
+ status: "failed",
526
+ required: false,
527
+ detail: "GitHub CLI is unavailable, so gh-act cannot be installed."
528
+ });
529
+ }
530
+ const existing = checkCommand(gh, ["act", "--version"], {
531
+ cwd: options.tenantRoot,
532
+ env,
533
+ spawn: options.spawn
534
+ });
535
+ if (existing.ok && !options.force) {
536
+ return report({
537
+ name: "gh-act",
538
+ kind: "extension",
539
+ version: "nektos/gh-act",
540
+ source: "managed-gh-config",
541
+ binaryPath: gh,
542
+ status: "already-present",
543
+ required: false,
544
+ detail: existing.stdout.split("\n")[0] ?? "gh-act is installed."
545
+ });
546
+ }
547
+ options.write?.("Installing GitHub CLI extension gh-act...");
548
+ const install = checkCommand(gh, ["extension", "install", "https://github.com/nektos/gh-act", ...options.force ? ["--force"] : []], {
549
+ cwd: options.tenantRoot,
550
+ env,
551
+ spawn: options.spawn
552
+ });
553
+ const postInstall = checkCommand(gh, ["act", "--version"], {
554
+ cwd: options.tenantRoot,
555
+ env,
556
+ spawn: options.spawn
557
+ });
558
+ return report({
559
+ name: "gh-act",
560
+ kind: "extension",
561
+ version: "nektos/gh-act",
562
+ source: "managed-gh-config",
563
+ binaryPath: gh,
564
+ status: postInstall.ok ? existing.ok ? "repaired" : "installed" : "failed",
565
+ required: false,
566
+ detail: postInstall.ok ? postInstall.stdout.split("\n")[0] ?? "gh-act is installed." : install.detail || postInstall.detail || "Unable to install gh-act."
567
+ });
568
+ }
569
+ async function installTreeseedDependencies(options = {}) {
570
+ const env = options.env ?? process.env;
571
+ const effectiveOptions = {
572
+ ...options,
573
+ env,
574
+ downloadFile: options.downloadFile ?? defaultDownloadFile,
575
+ spawn: options.spawn ?? spawnSync
576
+ };
577
+ mkdirSync(resolveToolsHome(env), { recursive: true });
578
+ mkdirSync(createTreeseedManagedToolEnv(env).GH_CONFIG_DIR, { recursive: true });
579
+ const npmInstalls = runNpmBootstrap(effectiveOptions);
580
+ const reports = [
581
+ systemStatus("git", true, effectiveOptions),
582
+ await installGh(effectiveOptions),
583
+ ...NPM_TOOLS.map((tool) => statusForNpmTool(tool, effectiveOptions)),
584
+ ...NPM_PACKAGES.map((pkg) => statusForNpmPackage(pkg)),
585
+ systemStatus("docker", false, effectiveOptions),
586
+ installGhAct(effectiveOptions)
587
+ ];
588
+ const ok = npmInstalls.every((entry) => entry.status !== "failed") && reports.every((entry) => !entry.required || !["failed", "missing", "unsupported"].includes(entry.status));
589
+ return {
590
+ ok,
591
+ toolsHome: resolveToolsHome(env),
592
+ ghConfigDir: createTreeseedManagedToolEnv(env).GH_CONFIG_DIR,
593
+ npmInstalls,
594
+ reports
595
+ };
596
+ }
597
+ function collectTreeseedDependencyStatus(options = {}) {
598
+ const env = options.env ?? process.env;
599
+ const ghBinary = managedGhBin(env);
600
+ const ghStatus = existsSync(ghBinary) ? report({
601
+ name: "gh",
602
+ kind: "download",
603
+ version: GH_VERSION,
604
+ source: "managed-cache",
605
+ binaryPath: ghBinary,
606
+ status: "already-present",
607
+ required: true,
608
+ detail: `GitHub CLI ${GH_VERSION} is installed in the Treeseed tool cache.`
609
+ }) : report({
610
+ name: "gh",
611
+ kind: "download",
612
+ version: GH_VERSION,
613
+ source: "managed-cache",
614
+ binaryPath: ghBinary,
615
+ status: "missing",
616
+ required: true,
617
+ detail: `GitHub CLI ${GH_VERSION} is not installed in the Treeseed tool cache.`
618
+ });
619
+ const reports = [
620
+ systemStatus("git", true, options),
621
+ ghStatus,
622
+ ...NPM_TOOLS.map((tool) => statusForNpmTool(tool, options)),
623
+ ...NPM_PACKAGES.map((pkg) => statusForNpmPackage(pkg)),
624
+ systemStatus("docker", false, options),
625
+ report({
626
+ name: "gh-act",
627
+ kind: "extension",
628
+ version: "nektos/gh-act",
629
+ source: "managed-gh-config",
630
+ binaryPath: ghBinary,
631
+ status: existsSync(ghBinary) ? "already-present" : "skipped",
632
+ required: false,
633
+ detail: existsSync(ghBinary) ? "gh-act status is checked during installation because it requires executing gh." : "gh-act is skipped until GitHub CLI is installed."
634
+ })
635
+ ];
636
+ return {
637
+ ok: reports.every((entry) => !entry.required || !["failed", "missing", "unsupported"].includes(entry.status)),
638
+ toolsHome: resolveToolsHome(env),
639
+ ghConfigDir: createTreeseedManagedToolEnv(env).GH_CONFIG_DIR,
640
+ npmInstalls: [],
641
+ reports
642
+ };
643
+ }
644
+ function formatTreeseedDependencyReport(result) {
645
+ return [
646
+ "Treeseed dependency status",
647
+ `Tools home: ${result.toolsHome}`,
648
+ `GitHub CLI config: ${result.ghConfigDir}`,
649
+ ...result.npmInstalls.map((entry) => {
650
+ const root = entry.root ? ` in ${entry.root}` : "";
651
+ const command = entry.command.length > 0 ? ` (${entry.command.join(" ")})` : "";
652
+ return `- npm install${root}: ${entry.status} - ${entry.detail}${command}`;
653
+ }),
654
+ ...result.reports.map((entry) => {
655
+ const path = entry.binaryPath ? ` (${entry.binaryPath})` : "";
656
+ return `- ${entry.name}: ${entry.status} - ${entry.detail}${path}`;
657
+ })
658
+ ].join("\n");
659
+ }
660
+ export {
661
+ collectTreeseedDependencyStatus,
662
+ createTreeseedManagedToolEnv,
663
+ formatTreeseedDependencyFailureDetails,
664
+ formatTreeseedDependencyReport,
665
+ installTreeseedDependencies,
666
+ resolveTreeseedToolBinary,
667
+ resolveTreeseedToolCommand
668
+ };