@treeseed/sdk 0.6.7 → 0.6.9

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 (49) 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 +50 -23
  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 +330 -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/prepare.js +14 -0
  36. package/dist/scripts/publish-package.js +5 -0
  37. package/dist/scripts/tenant-workflow-action.js +11 -2
  38. package/dist/verification.js +46 -13
  39. package/dist/workflow/operations.d.ts +381 -55
  40. package/dist/workflow/operations.js +725 -261
  41. package/dist/workflow-state.d.ts +40 -1
  42. package/dist/workflow-state.js +220 -17
  43. package/dist/workflow-support.d.ts +3 -0
  44. package/dist/workflow-support.js +34 -0
  45. package/dist/workflow.d.ts +19 -3
  46. package/dist/workflow.js +3 -3
  47. package/dist/wrangler-d1.js +6 -1
  48. package/package.json +17 -1
  49. package/templates/github/deploy.workflow.yml +59 -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,
@@ -1406,9 +1411,14 @@ function collectTreeseedConfigSeedValues(tenantRoot, scope, env = process.env) {
1406
1411
  }
1407
1412
  return filterEnvironmentValuesByRegistry({
1408
1413
  ...machineValues,
1409
- ...Object.fromEntries(Object.entries(env).map(([key, value]) => [key, value ?? void 0]))
1414
+ ...nonEmptyEnvironmentValues(env)
1410
1415
  }, registry, scope);
1411
1416
  }
1417
+ function nonEmptyEnvironmentValues(env = process.env) {
1418
+ return Object.fromEntries(
1419
+ Object.entries(env).filter(([, value]) => typeof value === "string" && value.length > 0)
1420
+ );
1421
+ }
1412
1422
  function collectTreeseedConfigSeedValueSources(tenantRoot, scope, env = process.env) {
1413
1423
  warnDeprecatedTreeseedLocalEnvFiles(tenantRoot);
1414
1424
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
@@ -1453,7 +1463,8 @@ function resolveTreeseedLaunchEnvironment({
1453
1463
  }
1454
1464
  }
1455
1465
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
1456
- const seedValues = scope === "local" ? { ...baseEnv, ...machineValues } : { ...machineValues, ...baseEnv };
1466
+ const baseValues = nonEmptyEnvironmentValues(baseEnv);
1467
+ const seedValues = scope === "local" ? { ...baseValues, ...machineValues } : { ...machineValues, ...baseValues };
1457
1468
  const suggestedValues = getTreeseedEnvironmentSuggestedValues({
1458
1469
  scope,
1459
1470
  purpose: "deploy",
@@ -1465,7 +1476,7 @@ function resolveTreeseedLaunchEnvironment({
1465
1476
  const nonSecretSuggestedValues = Object.fromEntries(
1466
1477
  registry.entries.filter((entry) => entry.sensitivity !== "secret" && typeof suggestedValues[entry.id] === "string" && suggestedValues[entry.id].length > 0).map((entry) => [entry.id, suggestedValues[entry.id]])
1467
1478
  );
1468
- const scopedValues = scope === "local" ? { ...nonSecretSuggestedValues, ...baseEnv, ...machineValues } : { ...nonSecretSuggestedValues, ...machineValues, ...baseEnv };
1479
+ const scopedValues = scope === "local" ? { ...nonSecretSuggestedValues, ...baseValues, ...machineValues } : { ...nonSecretSuggestedValues, ...machineValues, ...baseValues };
1469
1480
  return {
1470
1481
  ...scopedValues,
1471
1482
  ...overrides
@@ -1541,18 +1552,17 @@ function runGh(args, { cwd, dryRun = false, input, env } = {}) {
1541
1552
  if (dryRun) {
1542
1553
  return { status: 0, stdout: "", stderr: "" };
1543
1554
  }
1544
- const result = spawnSync("gh", args, {
1555
+ const gh = resolveTreeseedToolBinary("gh", { env });
1556
+ if (!gh) {
1557
+ throw new Error("GitHub CLI `gh` is not installed.");
1558
+ }
1559
+ const result = spawnSync(gh, args, {
1545
1560
  cwd,
1546
1561
  stdio: input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
1547
1562
  encoding: "utf8",
1548
1563
  input,
1549
1564
  timeout: 15e3,
1550
- env: {
1551
- ...process.env,
1552
- ...env ?? {},
1553
- GH_PROMPT_DISABLED: "1",
1554
- GH_NO_UPDATE_NOTIFIER: "1"
1555
- }
1565
+ env: createTreeseedManagedToolEnv({ ...process.env, ...env ?? {} })
1556
1566
  });
1557
1567
  if (result.error?.code === "ETIMEDOUT") {
1558
1568
  throw new Error(`gh ${args.join(" ")} timed out`);
@@ -1566,7 +1576,11 @@ function runRailway(args, { cwd, dryRun = false, input } = {}) {
1566
1576
  if (dryRun) {
1567
1577
  return { status: 0, stdout: "", stderr: "" };
1568
1578
  }
1569
- const result = spawnSync("railway", args, {
1579
+ const railway = resolveTreeseedToolCommand("railway");
1580
+ if (!railway) {
1581
+ throw new Error("Railway CLI is unavailable.");
1582
+ }
1583
+ const result = spawnSync(railway.command, [...railway.argsPrefix, ...args], {
1570
1584
  cwd,
1571
1585
  stdio: input !== void 0 ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
1572
1586
  encoding: "utf8",
@@ -1578,6 +1592,9 @@ function runRailway(args, { cwd, dryRun = false, input } = {}) {
1578
1592
  return result;
1579
1593
  }
1580
1594
  function commandAvailable(command) {
1595
+ if (command === "gh" || command === "wrangler" || command === "railway" || command === "copilot") {
1596
+ return Boolean(resolveTreeseedToolBinary(command));
1597
+ }
1581
1598
  const result = spawnSync("bash", ["-lc", `command -v ${command}`], {
1582
1599
  stdio: "pipe",
1583
1600
  encoding: "utf8"
@@ -1612,8 +1629,10 @@ function toolStatus(name, available, detail, extra = {}) {
1612
1629
  };
1613
1630
  }
1614
1631
  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 });
1632
+ const managedEnv = createTreeseedManagedToolEnv(env);
1633
+ const ghBinary = resolveTreeseedToolBinary("gh", { env: managedEnv });
1634
+ const githubCli = !ghBinary ? toolStatus("githubCli", false, "GitHub CLI `gh` is not installed.") : (() => {
1635
+ const check = checkCommand(ghBinary, ["--version"], { cwd: tenantRoot, env: managedEnv });
1617
1636
  return toolStatus("githubCli", check.ok, check.ok ? check.stdout.split("\n")[0] ?? "GitHub CLI detected." : check.detail || "GitHub CLI check failed.");
1618
1637
  })();
1619
1638
  let ghActExtension = toolStatus("ghActExtension", false, "GitHub CLI extension `gh-act` is not installed.", {
@@ -1636,23 +1655,24 @@ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), inst
1636
1655
  );
1637
1656
  }
1638
1657
  })();
1639
- const railwayCheck = checkCommand("railway", ["--version"], { cwd: tenantRoot, env });
1658
+ const railwayCommand = resolveTreeseedToolCommand("railway", { env });
1659
+ const railwayCheck = railwayCommand ? checkCommand(railwayCommand.command, [...railwayCommand.argsPrefix, "--version"], { cwd: tenantRoot, env }) : { ok: false, stdout: "", detail: "Railway CLI is unavailable." };
1640
1660
  const railwayCli = toolStatus(
1641
1661
  "railwayCli",
1642
1662
  railwayCheck.ok,
1643
1663
  railwayCheck.ok ? railwayCheck.stdout.split("\n")[0] ?? "Railway CLI detected." : railwayCheck.detail || "Railway CLI is unavailable."
1644
1664
  );
1645
- if (githubCli.available) {
1646
- const check = checkCommand("gh", ["act", "--version"], { cwd: tenantRoot, env });
1665
+ if (githubCli.available && ghBinary) {
1666
+ const check = checkCommand(ghBinary, ["act", "--version"], { cwd: tenantRoot, env: managedEnv });
1647
1667
  if (check.ok) {
1648
1668
  ghActExtension = toolStatus("ghActExtension", true, check.stdout.split("\n")[0] ?? "gh-act is installed.", {
1649
1669
  attemptedInstall: false,
1650
1670
  installedDuringConfig: false
1651
1671
  });
1652
- } else if (installIfMissing) {
1672
+ } else if (installIfMissing && commandAvailable("docker")) {
1653
1673
  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 });
1674
+ const install = checkCommand(ghBinary, ["extension", "install", "https://github.com/nektos/gh-act"], { cwd: tenantRoot, env: managedEnv });
1675
+ const postInstall = checkCommand(ghBinary, ["act", "--version"], { cwd: tenantRoot, env: managedEnv });
1656
1676
  ghActExtension = toolStatus(
1657
1677
  "ghActExtension",
1658
1678
  postInstall.ok,
@@ -1663,6 +1683,11 @@ function ensureTreeseedActVerificationTooling({ tenantRoot = process.cwd(), inst
1663
1683
  installStatus: install.status
1664
1684
  }
1665
1685
  );
1686
+ } else if (installIfMissing) {
1687
+ ghActExtension = toolStatus("ghActExtension", false, "Docker is not on PATH, so gh-act installation was skipped.", {
1688
+ attemptedInstall: false,
1689
+ installedDuringConfig: false
1690
+ });
1666
1691
  } else {
1667
1692
  ghActExtension = toolStatus("ghActExtension", false, check.detail || "GitHub CLI extension `gh-act` is not installed.", {
1668
1693
  attemptedInstall: false,
@@ -1721,17 +1746,18 @@ function checkGitHubConnection({ tenantRoot, env }) {
1721
1746
  if (!env.GH_TOKEN) {
1722
1747
  return providerConnectionResult("github", false, "GH_TOKEN is not configured.", { skipped: true });
1723
1748
  }
1724
- if (!commandAvailable("gh")) {
1749
+ const gh = resolveTreeseedToolBinary("gh", { env });
1750
+ if (!gh) {
1725
1751
  return providerConnectionResult("github", false, "GitHub CLI `gh` is not installed.");
1726
1752
  }
1727
1753
  const repository = maybeResolveGitHubRepositorySlug(tenantRoot);
1728
1754
  const args = repository ? ["repo", "view", repository, "--json", "nameWithOwner", "--jq", ".nameWithOwner"] : ["api", "user", "--jq", ".login"];
1729
1755
  for (let attempt = 0; attempt < 3; attempt += 1) {
1730
- const result = spawnSync("gh", args, {
1756
+ const result = spawnSync(gh, args, {
1731
1757
  cwd: tenantRoot,
1732
1758
  stdio: "pipe",
1733
1759
  encoding: "utf8",
1734
- env: { ...process.env, ...env },
1760
+ env: createTreeseedManagedToolEnv({ ...process.env, ...env }),
1735
1761
  timeout: CLI_CHECK_TIMEOUT_MS
1736
1762
  });
1737
1763
  if (result.status === 0) {
@@ -1800,7 +1826,8 @@ async function checkRailwayConnection({ tenantRoot, env }) {
1800
1826
  const checkPromise = (async () => {
1801
1827
  for (let attempt = 0; attempt < 3; attempt += 1) {
1802
1828
  try {
1803
- const whoami = checkCommand("railway", ["whoami"], { cwd: tenantRoot, env });
1829
+ const railwayCommand = resolveTreeseedToolCommand("railway", { env });
1830
+ const whoami = railwayCommand ? checkCommand(railwayCommand.command, [...railwayCommand.argsPrefix, "whoami"], { cwd: tenantRoot, env }) : { ok: false, stdout: "", detail: "Railway CLI is unavailable." };
1804
1831
  if (!whoami.ok) {
1805
1832
  if (/rate.?limit|too many requests|429/iu.test(whoami.detail || "")) {
1806
1833
  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
+ };