@treeseed/sdk 0.6.7 → 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 +28 -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 +24 -14
@@ -55,6 +55,10 @@ import { repoRoot } from "../../operations/services/workspace-save.js";
55
55
  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
+ import {
59
+ formatTreeseedDependencyReport,
60
+ installTreeseedDependencies
61
+ } from "../../managed-dependencies.js";
58
62
  function operationResult(metadata, payload, options = {}) {
59
63
  return {
60
64
  operation: metadata.id,
@@ -64,7 +68,8 @@ function operationResult(metadata, payload, options = {}) {
64
68
  nextSteps: options.nextSteps,
65
69
  exitCode: options.exitCode ?? (options.ok === false ? 1 : 0),
66
70
  stdout: options.stdout,
67
- stderr: options.stderr
71
+ stderr: options.stderr,
72
+ report: options.report
68
73
  };
69
74
  }
70
75
  function failureResult(metadata, message, options = {}) {
@@ -325,6 +330,29 @@ class DoctorOperation extends BaseOperation {
325
330
  });
326
331
  }
327
332
  }
333
+ class InstallOperation extends BaseOperation {
334
+ async execute(input, context) {
335
+ const result = await installTreeseedDependencies({
336
+ tenantRoot: context.cwd,
337
+ force: input.force === true,
338
+ env: context.env,
339
+ write: context.outputFormat === "json" ? void 0 : context.write
340
+ });
341
+ const stdout = [formatTreeseedDependencyReport(result)];
342
+ return operationResult(this.metadata, result, {
343
+ ok: result.ok,
344
+ exitCode: result.ok ? 0 : 1,
345
+ stdout,
346
+ report: {
347
+ ok: result.ok,
348
+ toolsHome: result.toolsHome,
349
+ ghConfigDir: result.ghConfigDir,
350
+ npmInstalls: result.npmInstalls,
351
+ tools: result.reports
352
+ }
353
+ });
354
+ }
355
+ }
328
356
  class AuthLoginOperation extends BaseOperation {
329
357
  async execute(input, context) {
330
358
  const tenantRoot = context.cwd;
@@ -580,6 +608,7 @@ class DefaultTreeseedOperationsProvider {
580
608
  new TemplateOperation("template"),
581
609
  new SyncTemplateOperation("sync"),
582
610
  new DoctorOperation("doctor"),
611
+ new InstallOperation("install"),
583
612
  new AuthLoginOperation("auth:login"),
584
613
  new AuthLogoutOperation("auth:logout"),
585
614
  new AuthWhoAmIOperation("auth:whoami"),
@@ -0,0 +1,33 @@
1
+ export declare const DEFAULT_COMMIT_AI_MODEL = "@cf/google/gemma-4-26b-a4b-it";
2
+ export type CommitMessageProviderMode = 'auto' | 'cloudflare' | 'fallback' | 'generated';
3
+ export type CommitMessageContext = {
4
+ repoName: string;
5
+ repoPath: string;
6
+ branch: string;
7
+ kind: 'package' | 'project';
8
+ branchMode: 'package-release-main' | 'package-dev-save' | 'project-save';
9
+ changedFiles: string;
10
+ diff: string;
11
+ plannedVersion: string | null;
12
+ plannedTag: string | null;
13
+ userMessage?: string;
14
+ };
15
+ export type CommitMessageResult = {
16
+ message: string;
17
+ provider: 'cloudflare-workers-ai' | 'fallback';
18
+ fallbackUsed: boolean;
19
+ error: string | null;
20
+ };
21
+ export type CommitMessageProvider = {
22
+ generate(context: CommitMessageContext): Promise<string> | string;
23
+ };
24
+ type CommitMessageOptions = {
25
+ mode?: CommitMessageProviderMode;
26
+ provider?: CommitMessageProvider;
27
+ env?: NodeJS.ProcessEnv;
28
+ fetchImpl?: typeof fetch;
29
+ };
30
+ export declare function formatCommitMessage(type: string, scope: string, summary: string, bullets: string[]): string;
31
+ export declare function generateFallbackCommitMessage(context: CommitMessageContext): string;
32
+ export declare function generateRepositoryCommitMessage(context: CommitMessageContext, options?: CommitMessageOptions): Promise<CommitMessageResult>;
33
+ export {};
@@ -0,0 +1,319 @@
1
+ const DEFAULT_COMMIT_AI_MODEL = "@cf/google/gemma-4-26b-a4b-it";
2
+ const allowedTypes = /* @__PURE__ */ new Set(["feat", "fix", "refactor", "test", "docs", "build", "ci", "chore"]);
3
+ const danglingSubjectEndings = /* @__PURE__ */ new Set([
4
+ "a",
5
+ "an",
6
+ "add",
7
+ "and",
8
+ "as",
9
+ "at",
10
+ "allow",
11
+ "by",
12
+ "bump",
13
+ "change",
14
+ "for",
15
+ "from",
16
+ "implement",
17
+ "in",
18
+ "into",
19
+ "of",
20
+ "on",
21
+ "or",
22
+ "remove",
23
+ "set",
24
+ "support",
25
+ "sync",
26
+ "to",
27
+ "update",
28
+ "with"
29
+ ]);
30
+ const defaultTimeoutMs = 3e4;
31
+ const defaultMaxDiffChars = 12e3;
32
+ function envValue(env, key) {
33
+ const value = env[key];
34
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
35
+ }
36
+ function parseNumber(value, fallback) {
37
+ if (!value) return fallback;
38
+ const parsed = Number(value);
39
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
40
+ }
41
+ function stripControlCharacters(value) {
42
+ return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, "");
43
+ }
44
+ function normalizeWhitespace(value) {
45
+ return stripControlCharacters(value).replace(/\s+/gu, " ").trim();
46
+ }
47
+ function changedPaths(changedFiles) {
48
+ return changedFiles.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^([MADRCU?!]{1,2})\s+/u, "").trim()).filter(Boolean);
49
+ }
50
+ function conventionalParts(message) {
51
+ const value = String(message ?? "").trim();
52
+ const match = value.match(/^([a-z]+)(?:\(([a-z0-9-]+)\))?:\s*(.+)$/iu);
53
+ if (!match) return null;
54
+ const type = match[1].toLowerCase();
55
+ return {
56
+ type: allowedTypes.has(type) ? type : "chore",
57
+ scope: match[2]?.toLowerCase() ?? null,
58
+ summary: normalizeWhitespace(match[3])
59
+ };
60
+ }
61
+ function inferType(context) {
62
+ const conventional = conventionalParts(context.userMessage);
63
+ if (conventional) return conventional.type;
64
+ const paths = changedPaths(context.changedFiles);
65
+ if (paths.length > 0 && paths.every((path) => /(^|\/)(test|tests|__tests__)\/|\.test\.|\.spec\./u.test(path))) return "test";
66
+ if (paths.some((path) => path.startsWith(".github/") || path.includes("/workflows/"))) return "ci";
67
+ if (paths.some((path) => /(^|\/)(package|package-lock)\.json$/u.test(path) || path.includes("build") || path.includes("scripts/"))) return "build";
68
+ if (paths.some((path) => /\.(md|mdx|txt)$/u.test(path) || path.startsWith("docs/"))) return "docs";
69
+ if (context.diff.includes("fix") || context.diff.includes("bug") || context.diff.includes("fail")) return "fix";
70
+ if (context.diff.includes("refactor") || context.diff.includes("rename")) return "refactor";
71
+ return context.kind === "package" && context.branchMode === "package-release-main" ? "chore" : "feat";
72
+ }
73
+ function inferScope(context) {
74
+ const conventional = conventionalParts(context.userMessage);
75
+ if (conventional?.scope) return conventional.scope;
76
+ const counts = /* @__PURE__ */ new Map();
77
+ for (const path of changedPaths(context.changedFiles)) {
78
+ const scope = path.startsWith(".github/") ? "ci" : path.includes("workflow") || path.includes("repository-save-orchestrator") ? "workflow" : path.includes("package-reference") || path.includes("publish") || path.includes("release") ? "release" : path.includes("save") ? "save" : path.includes("cli/") || path.startsWith("src/cli") || path.includes("operations-registry") ? "cli" : path.includes("config") || path.endsWith(".yaml") || path.endsWith(".yml") ? "config" : path.includes("test") || path.includes("__tests__") ? "tests" : path.endsWith(".md") || path.endsWith(".mdx") ? "docs" : path.includes("package.json") || path.includes("package-lock.json") || path.includes("build") ? "build" : context.branchMode === "package-dev-save" ? "save" : "workflow";
79
+ counts.set(scope, (counts.get(scope) ?? 0) + 1);
80
+ }
81
+ return [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))[0]?.[0] ?? (context.branchMode === "package-release-main" ? "release" : "save");
82
+ }
83
+ function summaryFromHint(message) {
84
+ const conventional = conventionalParts(message);
85
+ const hint = conventional?.summary ?? normalizeWhitespace(String(message ?? ""));
86
+ if (!hint) return null;
87
+ return hint.replace(/^(added|adds)\b/iu, "add").replace(/^(updated|updates)\b/iu, "update").replace(/^(fixed|fixes)\b/iu, "fix").replace(/^(prevented|prevents)\b/iu, "prevent").replace(/^(allowed|allows)\b/iu, "allow").replace(/^(implemented|implements)\b/iu, "implement");
88
+ }
89
+ function lastSummaryWord(summary) {
90
+ return normalizeWhitespace(summary).split(/\s+/u).at(-1)?.toLowerCase().replace(/[^a-z0-9-]/gu, "") ?? "";
91
+ }
92
+ function repairSummaryEnding(summary, fallback = "record changes") {
93
+ const words = normalizeWhitespace(summary).replace(/[.。]+$/u, "").split(/\s+/u).filter(Boolean);
94
+ while (words.length > 2 && danglingSubjectEndings.has(lastSummaryWord(words.join(" ")))) {
95
+ words.pop();
96
+ }
97
+ const repaired = words.join(" ").trim();
98
+ if (!repaired || danglingSubjectEndings.has(lastSummaryWord(repaired))) {
99
+ return fallback;
100
+ }
101
+ return repaired;
102
+ }
103
+ function fallbackSummary(context, type, scope) {
104
+ const hint = summaryFromHint(context.userMessage);
105
+ if (hint) return repairSummaryEnding(hint);
106
+ if (context.branchMode === "package-release-main") return "prepare stable release";
107
+ if (scope === "cli") return "allow save without message";
108
+ if (scope === "release" || scope === "ci") return "guard dev tags from publish";
109
+ if (scope === "workflow" || scope === "save") return "generate save commit messages";
110
+ if (type === "test") return "cover save workflow behavior";
111
+ if (type === "docs") return "update workflow guidance";
112
+ if (type === "build") return "update package build metadata";
113
+ return "record workflow changes";
114
+ }
115
+ function truncateSubject(type, scope, summary) {
116
+ const prefix = `${type}(${scope}): `;
117
+ const cleanSummary = repairSummaryEnding(summary);
118
+ const maxSummaryLength = Math.max(10, 50 - prefix.length);
119
+ if (cleanSummary.length <= maxSummaryLength) return `${prefix}${cleanSummary}`;
120
+ const sliced = cleanSummary.slice(0, maxSummaryLength).replace(/\s+\S*$/u, "").trim();
121
+ const repaired = repairSummaryEnding(sliced || cleanSummary.slice(0, maxSummaryLength).trim());
122
+ return `${prefix}${repaired}`;
123
+ }
124
+ function wrapText(value, width = 72) {
125
+ const words = normalizeWhitespace(value).split(/\s+/u).filter(Boolean);
126
+ const lines = [];
127
+ let line = "";
128
+ for (const word of words) {
129
+ if (!line) {
130
+ line = word;
131
+ } else if (`${line} ${word}`.length <= width) {
132
+ line = `${line} ${word}`;
133
+ } else {
134
+ lines.push(line);
135
+ line = word;
136
+ }
137
+ }
138
+ if (line) lines.push(line);
139
+ return lines;
140
+ }
141
+ function formatBullet(text) {
142
+ const lines = wrapText(text, 70);
143
+ return lines.map((line, index) => `${index === 0 ? "-" : " "} ${line}`).join("\n");
144
+ }
145
+ function formatCommitMessage(type, scope, summary, bullets) {
146
+ const normalizedType = allowedTypes.has(type) ? type : "chore";
147
+ const normalizedScope = scope.toLowerCase().replace(/[^a-z0-9-]+/gu, "-").replace(/^-+|-+$/gu, "") || "workflow";
148
+ const subject = truncateSubject(normalizedType, normalizedScope, summary);
149
+ const body = bullets.map((bullet) => normalizeWhitespace(bullet)).filter(Boolean).slice(0, 3).map(formatBullet).join("\n");
150
+ return `${subject}
151
+
152
+ ${body}`;
153
+ }
154
+ function generateFallbackCommitMessage(context) {
155
+ const type = inferType(context);
156
+ const scope = inferScope(context);
157
+ const summary = fallbackSummary(context, type, scope);
158
+ const bullets = [
159
+ context.userMessage?.trim() ? `Uses the provided save hint to describe why the ${scope} checkpoint is necessary.` : `Records the current ${scope} changes in the standard save workflow.`,
160
+ context.branchMode === "package-dev-save" ? "Keeps development package state on Git tags without publishing stable NPM releases." : context.branchMode === "package-release-main" ? "Prepares stable package metadata for the main branch release path." : "Preserves the project branch state for the parent workflow.",
161
+ "Keeps the message deterministic when AI generation is unavailable."
162
+ ];
163
+ return formatCommitMessage(type, scope, summary, bullets);
164
+ }
165
+ function assertCommitTemplate(message) {
166
+ const normalized = stripControlCharacters(message).replace(/^```(?:text)?\s*/iu, "").replace(/```\s*$/u, "").trim();
167
+ const [subject = "", ...rest] = normalized.split(/\r?\n/u);
168
+ if (!/^(feat|fix|refactor|test|docs|build|ci|chore)\([a-z0-9-]+\): .+/u.test(subject.trim())) {
169
+ throw new Error("AI commit message did not use the required subject template.");
170
+ }
171
+ const summary = subject.replace(/^[a-z]+\([^)]+\):\s*/u, "").trim();
172
+ if (danglingSubjectEndings.has(lastSummaryWord(summary))) {
173
+ throw new Error("AI commit message subject appears truncated.");
174
+ }
175
+ const bullets = rest.map((line) => line.trim()).filter((line) => line.startsWith("- "));
176
+ if (bullets.length === 0) {
177
+ throw new Error("AI commit message did not include body bullets.");
178
+ }
179
+ return formatCommitMessage(
180
+ subject.split("(")[0],
181
+ subject.match(/\(([^)]+)\)/u)?.[1] ?? "workflow",
182
+ subject.replace(/^[a-z]+\([^)]+\):\s*/u, ""),
183
+ bullets.map((line) => line.replace(/^-\s*/u, ""))
184
+ );
185
+ }
186
+ function cloudflareEndpoint(accountId, model, gatewayId) {
187
+ const normalizedModel = model.replace(/^\/+/u, "");
188
+ if (gatewayId) {
189
+ return `https://gateway.ai.cloudflare.com/v1/${encodeURIComponent(accountId)}/${encodeURIComponent(gatewayId)}/workers-ai/${normalizedModel}`;
190
+ }
191
+ return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/run/${normalizedModel}`;
192
+ }
193
+ function cloudflarePrompt(context, maxDiffChars) {
194
+ return {
195
+ system: [
196
+ "Generate exactly one Git commit message.",
197
+ "Use this template:",
198
+ "type(scope): short imperative summary",
199
+ "",
200
+ "- Explain why this change is necessary.",
201
+ "- Mention any side effects or constraints.",
202
+ "- Use 72 character wrap.",
203
+ "",
204
+ "Use only these types: feat, fix, refactor, test, docs, build, ci, chore.",
205
+ "Infer scope from the changed area, not from the repository name.",
206
+ "Do not include repository name or package version in save messages.",
207
+ "Return only the commit message."
208
+ ].join("\n"),
209
+ user: [
210
+ `Branch: ${context.branch}`,
211
+ `Mode: ${context.branchMode}`,
212
+ `Kind: ${context.kind}`,
213
+ context.userMessage ? `User hint: ${context.userMessage}` : "User hint: none",
214
+ context.plannedTag ? `Planned tag: ${context.plannedTag}` : null,
215
+ "Changed files:",
216
+ context.changedFiles || "(none)",
217
+ "Diff:",
218
+ context.diff.slice(0, maxDiffChars) || "(none)"
219
+ ].filter((line) => line !== null).join("\n")
220
+ };
221
+ }
222
+ function extractCloudflareText(payload) {
223
+ const record = payload && typeof payload === "object" ? payload : {};
224
+ const result = record.result && typeof record.result === "object" ? record.result : record;
225
+ for (const key of ["response", "text", "output"]) {
226
+ if (typeof result[key] === "string") return result[key];
227
+ }
228
+ const choices = result.choices;
229
+ if (Array.isArray(choices)) {
230
+ const first = choices[0];
231
+ const message = first?.message;
232
+ if (typeof message?.content === "string") return message.content;
233
+ if (typeof first?.text === "string") return first.text;
234
+ }
235
+ return null;
236
+ }
237
+ async function generateCloudflareCommitMessage(context, env, fetchImpl) {
238
+ const token = envValue(env, "CLOUDFLARE_API_TOKEN");
239
+ const accountId = envValue(env, "CLOUDFLARE_ACCOUNT_ID");
240
+ const model = envValue(env, "TREESEED_COMMIT_AI_MODEL") ?? DEFAULT_COMMIT_AI_MODEL;
241
+ if (!token || !accountId) {
242
+ throw new Error("Cloudflare commit AI requires CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.");
243
+ }
244
+ const timeoutMs = parseNumber(envValue(env, "TREESEED_COMMIT_AI_TIMEOUT_MS"), defaultTimeoutMs);
245
+ const maxDiffChars = parseNumber(envValue(env, "TREESEED_COMMIT_AI_MAX_DIFF_CHARS"), defaultMaxDiffChars);
246
+ const prompt = cloudflarePrompt(context, maxDiffChars);
247
+ const controller = new AbortController();
248
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
249
+ try {
250
+ const response = await fetchImpl(cloudflareEndpoint(accountId, model, envValue(env, "TREESEED_COMMIT_AI_GATEWAY_ID")), {
251
+ method: "POST",
252
+ headers: {
253
+ Authorization: `Bearer ${token}`,
254
+ "Content-Type": "application/json"
255
+ },
256
+ body: JSON.stringify({
257
+ messages: [
258
+ { role: "system", content: prompt.system },
259
+ { role: "user", content: prompt.user }
260
+ ]
261
+ }),
262
+ signal: controller.signal
263
+ });
264
+ if (!response.ok) {
265
+ throw new Error(`Cloudflare commit AI failed with HTTP ${response.status}.`);
266
+ }
267
+ const text = extractCloudflareText(await response.json());
268
+ if (!text) {
269
+ throw new Error("Cloudflare commit AI returned an empty response.");
270
+ }
271
+ return assertCommitTemplate(text);
272
+ } finally {
273
+ clearTimeout(timeout);
274
+ }
275
+ }
276
+ async function generateRepositoryCommitMessage(context, options = {}) {
277
+ const env = options.env ?? process.env;
278
+ const mode = options.mode ?? envValue(env, "TREESEED_COMMIT_MESSAGE_PROVIDER") ?? "auto";
279
+ const fallback = () => generateFallbackCommitMessage(context);
280
+ if (mode === "fallback") {
281
+ return { message: fallback(), provider: "fallback", fallbackUsed: false, error: null };
282
+ }
283
+ if (mode === "generated" && options.provider) {
284
+ try {
285
+ return {
286
+ message: assertCommitTemplate(await Promise.resolve(options.provider.generate(context))),
287
+ provider: "cloudflare-workers-ai",
288
+ fallbackUsed: false,
289
+ error: null
290
+ };
291
+ } catch (error) {
292
+ return { message: fallback(), provider: "fallback", fallbackUsed: true, error: error instanceof Error ? error.message : String(error) };
293
+ }
294
+ }
295
+ const hasCloudflareConfig = Boolean(envValue(env, "CLOUDFLARE_API_TOKEN") && envValue(env, "CLOUDFLARE_ACCOUNT_ID"));
296
+ if ((mode === "auto" || mode === "generated") && !hasCloudflareConfig) {
297
+ return { message: fallback(), provider: "fallback", fallbackUsed: false, error: null };
298
+ }
299
+ try {
300
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
301
+ if (typeof fetchImpl !== "function") {
302
+ throw new Error("Global fetch is unavailable for Cloudflare commit AI.");
303
+ }
304
+ return {
305
+ message: await generateCloudflareCommitMessage(context, env, fetchImpl),
306
+ provider: "cloudflare-workers-ai",
307
+ fallbackUsed: false,
308
+ error: null
309
+ };
310
+ } catch (error) {
311
+ return { message: fallback(), provider: "fallback", fallbackUsed: true, error: error instanceof Error ? error.message : String(error) };
312
+ }
313
+ }
314
+ export {
315
+ DEFAULT_COMMIT_AI_MODEL,
316
+ formatCommitMessage,
317
+ generateFallbackCommitMessage,
318
+ generateRepositoryCommitMessage
319
+ };
@@ -47,6 +47,11 @@ import {
47
47
  } from "./github-api.js";
48
48
  import { loadCliDeployConfig, packageScriptPath, resolveWranglerBin, withProcessCwd } from "./runtime-tools.js";
49
49
  import { PRODUCTION_BRANCH, STAGING_BRANCH } from "./git-workflow.js";
50
+ import {
51
+ createTreeseedManagedToolEnv,
52
+ resolveTreeseedToolBinary,
53
+ resolveTreeseedToolCommand
54
+ } from "../../managed-dependencies.js";
50
55
  import {
51
56
  assertTreeseedKeyAgentResponse,
52
57
  getTreeseedKeyAgentPaths,
@@ -1541,18 +1546,17 @@ function runGh(args, { cwd, dryRun = false, input, env } = {}) {
1541
1546
  if (dryRun) {
1542
1547
  return { status: 0, stdout: "", stderr: "" };
1543
1548
  }
1544
- const result = spawnSync("gh", args, {
1549
+ const gh = resolveTreeseedToolBinary("gh", { env });
1550
+ if (!gh) {
1551
+ throw new Error("GitHub CLI `gh` is not installed.");
1552
+ }
1553
+ const result = spawnSync(gh, args, {
1545
1554
  cwd,
1546
1555
  stdio: input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
1547
1556
  encoding: "utf8",
1548
1557
  input,
1549
1558
  timeout: 15e3,
1550
- env: {
1551
- ...process.env,
1552
- ...env ?? {},
1553
- GH_PROMPT_DISABLED: "1",
1554
- GH_NO_UPDATE_NOTIFIER: "1"
1555
- }
1559
+ env: createTreeseedManagedToolEnv({ ...process.env, ...env ?? {} })
1556
1560
  });
1557
1561
  if (result.error?.code === "ETIMEDOUT") {
1558
1562
  throw new Error(`gh ${args.join(" ")} timed out`);
@@ -1566,7 +1570,11 @@ function runRailway(args, { cwd, dryRun = false, input } = {}) {
1566
1570
  if (dryRun) {
1567
1571
  return { status: 0, stdout: "", stderr: "" };
1568
1572
  }
1569
- const result = spawnSync("railway", args, {
1573
+ const railway = resolveTreeseedToolCommand("railway");
1574
+ if (!railway) {
1575
+ throw new Error("Railway CLI is unavailable.");
1576
+ }
1577
+ const result = spawnSync(railway.command, [...railway.argsPrefix, ...args], {
1570
1578
  cwd,
1571
1579
  stdio: input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
1572
1580
  encoding: "utf8",
@@ -1578,6 +1586,9 @@ function runRailway(args, { cwd, dryRun = false, input } = {}) {
1578
1586
  return result;
1579
1587
  }
1580
1588
  function commandAvailable(command) {
1589
+ if (command === "gh" || command === "wrangler" || command === "railway" || command === "copilot") {
1590
+ return Boolean(resolveTreeseedToolBinary(command));
1591
+ }
1581
1592
  const result = spawnSync("bash", ["-lc", `command -v ${command}`], {
1582
1593
  stdio: "pipe",
1583
1594
  encoding: "utf8"
@@ -1612,8 +1623,10 @@ function toolStatus(name, available, detail, extra = {}) {
1612
1623
  };
1613
1624
  }
1614
1625
  function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), installIfMissing = true, env = process.env, write } = {}) {
1615
- const githubCli = !commandAvailable("gh") ? toolStatus("githubCli", false, "GitHub CLI `gh` is not installed.") : (() => {
1616
- const check = checkCommand("gh", ["--version"], { cwd: tenantRoot, env });
1626
+ const managedEnv = createTreeseedManagedToolEnv(env);
1627
+ const ghBinary = resolveTreeseedToolBinary("gh", { env: managedEnv });
1628
+ const githubCli = !ghBinary ? toolStatus("githubCli", false, "GitHub CLI `gh` is not installed.") : (() => {
1629
+ const check = checkCommand(ghBinary, ["--version"], { cwd: tenantRoot, env: managedEnv });
1617
1630
  return toolStatus("githubCli", check.ok, check.ok ? check.stdout.split("\n")[0] ?? "GitHub CLI detected." : check.detail || "GitHub CLI check failed.");
1618
1631
  })();
1619
1632
  let ghActExtension = toolStatus("ghActExtension", false, "GitHub CLI extension `gh-act` is not installed.", {
@@ -1636,23 +1649,24 @@ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), inst
1636
1649
  );
1637
1650
  }
1638
1651
  })();
1639
- const railwayCheck = checkCommand("railway", ["--version"], { cwd: tenantRoot, env });
1652
+ const railwayCommand = resolveTreeseedToolCommand("railway", { env });
1653
+ const railwayCheck = railwayCommand ? checkCommand(railwayCommand.command, [...railwayCommand.argsPrefix, "--version"], { cwd: tenantRoot, env }) : { ok: false, stdout: "", detail: "Railway CLI is unavailable." };
1640
1654
  const railwayCli = toolStatus(
1641
1655
  "railwayCli",
1642
1656
  railwayCheck.ok,
1643
1657
  railwayCheck.ok ? railwayCheck.stdout.split("\n")[0] ?? "Railway CLI detected." : railwayCheck.detail || "Railway CLI is unavailable."
1644
1658
  );
1645
- if (githubCli.available) {
1646
- const check = checkCommand("gh", ["act", "--version"], { cwd: tenantRoot, env });
1659
+ if (githubCli.available && ghBinary) {
1660
+ const check = checkCommand(ghBinary, ["act", "--version"], { cwd: tenantRoot, env: managedEnv });
1647
1661
  if (check.ok) {
1648
1662
  ghActExtension = toolStatus("ghActExtension", true, check.stdout.split("\n")[0] ?? "gh-act is installed.", {
1649
1663
  attemptedInstall: false,
1650
1664
  installedDuringConfig: false
1651
1665
  });
1652
- } else if (installIfMissing) {
1666
+ } else if (installIfMissing && commandAvailable("docker")) {
1653
1667
  write?.("Installing GitHub CLI extension `gh-act`...");
1654
- const install = checkCommand("gh", ["extension", "install", "https://github.com/nektos/gh-act"], { cwd: tenantRoot, env });
1655
- const postInstall = checkCommand("gh", ["act", "--version"], { cwd: tenantRoot, env });
1668
+ const install = checkCommand(ghBinary, ["extension", "install", "https://github.com/nektos/gh-act"], { cwd: tenantRoot, env: managedEnv });
1669
+ const postInstall = checkCommand(ghBinary, ["act", "--version"], { cwd: tenantRoot, env: managedEnv });
1656
1670
  ghActExtension = toolStatus(
1657
1671
  "ghActExtension",
1658
1672
  postInstall.ok,
@@ -1663,6 +1677,11 @@ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), inst
1663
1677
  installStatus: install.status
1664
1678
  }
1665
1679
  );
1680
+ } else if (installIfMissing) {
1681
+ ghActExtension = toolStatus("ghActExtension", false, "Docker is not on PATH, so gh-act installation was skipped.", {
1682
+ attemptedInstall: false,
1683
+ installedDuringConfig: false
1684
+ });
1666
1685
  } else {
1667
1686
  ghActExtension = toolStatus("ghActExtension", false, check.detail || "GitHub CLI extension `gh-act` is not installed.", {
1668
1687
  attemptedInstall: false,
@@ -1721,17 +1740,18 @@ function checkGitHubConnection({ tenantRoot, env }) {
1721
1740
  if (!env.GH_TOKEN) {
1722
1741
  return providerConnectionResult("github", false, "GH_TOKEN is not configured.", { skipped: true });
1723
1742
  }
1724
- if (!commandAvailable("gh")) {
1743
+ const gh = resolveTreeseedToolBinary("gh", { env });
1744
+ if (!gh) {
1725
1745
  return providerConnectionResult("github", false, "GitHub CLI `gh` is not installed.");
1726
1746
  }
1727
1747
  const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
1728
1748
  const args = repository ? ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"] : ["api", "user", "--jq", ".login"];
1729
1749
  for (let attempt = 0; attempt < 3; attempt += 1) {
1730
- const result = spawnSync("gh", args, {
1750
+ const result = spawnSync(gh, args, {
1731
1751
  cwd: tenantRoot,
1732
1752
  stdio: "pipe",
1733
1753
  encoding: "utf8",
1734
- env: { ...process.env, ...env },
1754
+ env: createTreeseedManagedToolEnv({ ...process.env, ...env }),
1735
1755
  timeout: CLI_CHECK_TIMEOUT_MS
1736
1756
  });
1737
1757
  if (result.status === 0) {
@@ -1800,7 +1820,8 @@ async function checkRailwayConnection({ tenantRoot, env }) {
1800
1820
  const checkPromise = (async () => {
1801
1821
  for (let attempt = 0; attempt < 3; attempt += 1) {
1802
1822
  try {
1803
- const whoami = checkCommand("railway", ["whoami"], { cwd: tenantRoot, env });
1823
+ const railwayCommand = resolveTreeseedToolCommand("railway", { env });
1824
+ const whoami = railwayCommand ? checkCommand(railwayCommand.command, [...railwayCommand.argsPrefix, "whoami"], { cwd: tenantRoot, env }) : { ok: false, stdout: "", detail: "Railway CLI is unavailable." };
1804
1825
  if (!whoami.ok) {
1805
1826
  if (/rate.?limit|too many requests|429/iu.test(whoami.detail || "")) {
1806
1827
  return providerConnectionResult(
@@ -0,0 +1,9 @@
1
+ export type GitRemoteWriteMode = 'ssh-pushurl' | 'off';
2
+ export declare function sshPushUrlForRemote(remoteUrl: string | null | undefined): string | null;
3
+ export declare function configuredPushUrl(repoDir: string, remoteName?: string): string | null;
4
+ export declare function remoteWriteUrl(repoDir: string, remoteName?: string): string | null;
5
+ export declare function ensureSshPushUrlForOrigin(repoDir: string, remoteUrl: string | null | undefined, mode?: GitRemoteWriteMode): {
6
+ changed: boolean;
7
+ pushUrl: string | null;
8
+ reason: string;
9
+ };
@@ -0,0 +1,55 @@
1
+ import { run } from "./workspace-tools.js";
2
+ function normalizeRemote(remoteUrl) {
3
+ return String(remoteUrl ?? "").trim();
4
+ }
5
+ function sshPushUrlForRemote(remoteUrl) {
6
+ const remote = normalizeRemote(remoteUrl).replace(/^git\+/u, "");
7
+ if (!remote || remote.startsWith("/") || remote.startsWith("./") || remote.startsWith("../")) {
8
+ return null;
9
+ }
10
+ if (/^(?:file|ssh):\/\//u.test(remote) || /^git@[^:]+:.+/u.test(remote)) {
11
+ return null;
12
+ }
13
+ const httpsMatch = remote.match(/^https:\/\/([^/]+)\/(.+?)(?:\.git)?$/u);
14
+ if (!httpsMatch) {
15
+ return null;
16
+ }
17
+ return `git@${httpsMatch[1]}:${httpsMatch[2]}.git`;
18
+ }
19
+ function configuredPushUrl(repoDir, remoteName = "origin") {
20
+ try {
21
+ return run("git", ["config", "--get", `remote.${remoteName}.pushurl`], { cwd: repoDir, capture: true }).trim() || null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ function remoteWriteUrl(repoDir, remoteName = "origin") {
27
+ const pushUrl = configuredPushUrl(repoDir, remoteName);
28
+ if (pushUrl) return pushUrl;
29
+ try {
30
+ return run("git", ["remote", "get-url", remoteName], { cwd: repoDir, capture: true }).trim() || null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function ensureSshPushUrlForOrigin(repoDir, remoteUrl, mode = "ssh-pushurl") {
36
+ if (mode === "off") {
37
+ return { changed: false, pushUrl: configuredPushUrl(repoDir), reason: "disabled" };
38
+ }
39
+ const nextPushUrl = sshPushUrlForRemote(remoteUrl);
40
+ if (!nextPushUrl) {
41
+ return { changed: false, pushUrl: configuredPushUrl(repoDir), reason: "not-https" };
42
+ }
43
+ const currentPushUrl = configuredPushUrl(repoDir);
44
+ if (currentPushUrl === nextPushUrl) {
45
+ return { changed: false, pushUrl: currentPushUrl, reason: "already-configured" };
46
+ }
47
+ run("git", ["remote", "set-url", "--push", "origin", nextPushUrl], { cwd: repoDir });
48
+ return { changed: true, pushUrl: nextPushUrl, reason: currentPushUrl ? "updated" : "configured" };
49
+ }
50
+ export {
51
+ configuredPushUrl,
52
+ ensureSshPushUrlForOrigin,
53
+ remoteWriteUrl,
54
+ sshPushUrlForRemote
55
+ };