@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.
- package/dist/copilot.d.ts +15 -0
- package/dist/copilot.js +75 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/managed-dependencies.d.ts +56 -0
- package/dist/managed-dependencies.js +668 -0
- package/dist/operations/providers/default.js +30 -1
- package/dist/operations/services/commit-message-provider.d.ts +33 -0
- package/dist/operations/services/commit-message-provider.js +319 -0
- package/dist/operations/services/config-runtime.js +41 -20
- package/dist/operations/services/git-remote-policy.d.ts +9 -0
- package/dist/operations/services/git-remote-policy.js +55 -0
- package/dist/operations/services/git-workflow.js +22 -3
- package/dist/operations/services/github-api.js +9 -4
- package/dist/operations/services/knowledge-coop-launch.js +4 -0
- package/dist/operations/services/local-dev.js +7 -2
- package/dist/operations/services/package-reference-policy.d.ts +70 -0
- package/dist/operations/services/package-reference-policy.js +314 -0
- package/dist/operations/services/project-platform.d.ts +4 -0
- package/dist/operations/services/project-platform.js +28 -4
- package/dist/operations/services/railway-deploy.d.ts +4 -1
- package/dist/operations/services/railway-deploy.js +76 -38
- package/dist/operations/services/repository-save-orchestrator.d.ts +172 -0
- package/dist/operations/services/repository-save-orchestrator.js +1462 -0
- package/dist/operations/services/workspace-dependency-mode.d.ts +70 -0
- package/dist/operations/services/workspace-dependency-mode.js +404 -0
- package/dist/operations/services/workspace-preflight.js +5 -0
- package/dist/operations/services/workspace-save.js +10 -6
- package/dist/operations-registry.js +5 -0
- package/dist/operations-types.d.ts +1 -0
- package/dist/platform/books-data.js +4 -1
- package/dist/platform/env.yaml +6 -3
- package/dist/reconcile/builtin-adapters.js +37 -7
- package/dist/scripts/cleanup-markdown.js +4 -0
- package/dist/scripts/publish-package.js +5 -0
- package/dist/scripts/tenant-workflow-action.js +11 -2
- package/dist/verification.js +24 -12
- package/dist/workflow/operations.d.ts +381 -55
- package/dist/workflow/operations.js +718 -258
- package/dist/workflow-state.d.ts +40 -1
- package/dist/workflow-state.js +220 -17
- package/dist/workflow-support.d.ts +3 -0
- package/dist/workflow-support.js +34 -0
- package/dist/workflow.d.ts +19 -3
- package/dist/workflow.js +3 -3
- package/dist/wrangler-d1.js +6 -1
- package/package.json +17 -1
- 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
|
|
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
|
|
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
|
|
1616
|
-
|
|
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
|
|
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(
|
|
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(
|
|
1655
|
-
const postInstall = checkCommand(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
+
};
|