@visulima/vis 1.0.0-alpha.11 → 1.0.0-alpha.13
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/CHANGELOG.md +101 -0
- package/LICENSE.md +559 -186
- package/README.md +18 -0
- package/dist/bin.js +1 -9
- package/dist/config/index.d.ts +477 -556
- package/dist/config/index.js +1 -2
- package/dist/generate/index.js +1 -3
- package/dist/packem_chunks/applyDefaults.js +2 -336
- package/dist/packem_chunks/bin.js +234 -9552
- package/dist/packem_chunks/doctor-probe.js +2 -112
- package/dist/packem_chunks/fix.js +11 -234
- package/dist/packem_chunks/handler.js +1 -99
- package/dist/packem_chunks/handler10.js +2 -53
- package/dist/packem_chunks/handler11.js +1 -32
- package/dist/packem_chunks/handler12.js +5 -100
- package/dist/packem_chunks/handler13.js +1 -25
- package/dist/packem_chunks/handler14.js +18 -916
- package/dist/packem_chunks/handler15.js +15 -201
- package/dist/packem_chunks/handler16.js +1 -124
- package/dist/packem_chunks/handler17.js +1 -13
- package/dist/packem_chunks/handler18.js +1 -106
- package/dist/packem_chunks/handler19.js +1 -19
- package/dist/packem_chunks/handler2.js +2 -75
- package/dist/packem_chunks/handler20.js +5 -29
- package/dist/packem_chunks/handler21.js +1 -222
- package/dist/packem_chunks/handler22.js +1 -237
- package/dist/packem_chunks/handler23.js +5 -101
- package/dist/packem_chunks/handler24.js +1 -110
- package/dist/packem_chunks/handler25.js +3 -402
- package/dist/packem_chunks/handler26.js +1 -13
- package/dist/packem_chunks/handler27.js +1 -63
- package/dist/packem_chunks/handler28.js +7 -34
- package/dist/packem_chunks/handler29.js +21 -456
- package/dist/packem_chunks/handler3.js +4 -95
- package/dist/packem_chunks/handler30.js +3 -170
- package/dist/packem_chunks/handler31.js +1 -530
- package/dist/packem_chunks/handler32.js +2 -214
- package/dist/packem_chunks/handler33.js +25 -119
- package/dist/packem_chunks/handler34.js +2 -630
- package/dist/packem_chunks/handler35.js +3 -283
- package/dist/packem_chunks/handler36.js +22 -542
- package/dist/packem_chunks/handler37.js +410 -744
- package/dist/packem_chunks/handler38.js +22 -989
- package/dist/packem_chunks/handler39.js +22 -574
- package/dist/packem_chunks/handler4.js +2 -90
- package/dist/packem_chunks/handler40.js +22 -1685
- package/dist/packem_chunks/handler41.js +6 -1088
- package/dist/packem_chunks/handler42.js +5 -797
- package/dist/packem_chunks/handler43.js +10 -2658
- package/dist/packem_chunks/handler44.js +51 -3784
- package/dist/packem_chunks/handler45.js +25 -2574
- package/dist/packem_chunks/handler46.js +3 -3769
- package/dist/packem_chunks/handler47.js +21 -1485
- package/dist/packem_chunks/handler48.js +42 -0
- package/dist/packem_chunks/handler5.js +8 -174
- package/dist/packem_chunks/handler6.js +1 -95
- package/dist/packem_chunks/handler7.js +1 -115
- package/dist/packem_chunks/handler8.js +1 -12
- package/dist/packem_chunks/handler9.js +1 -29
- package/dist/packem_chunks/heal-accept.js +10 -522
- package/dist/packem_chunks/heal.js +14 -673
- package/dist/packem_chunks/index.js +7 -873
- package/dist/packem_chunks/loader.js +1 -23
- package/dist/packem_chunks/tar.js +3 -0
- package/dist/packem_shared/ai-analysis-hm8d2W7z.js +67 -0
- package/dist/packem_shared/ai-cache-DoiF80AR.js +1 -0
- package/dist/packem_shared/ai-fix-nn4zOE95.js +43 -0
- package/dist/packem_shared/cache-directory-CwHlJhgx.js +1 -0
- package/dist/packem_shared/dependency-scan-COr5n63B.js +2 -0
- package/dist/packem_shared/docker-D6OGr5_S.js +2 -0
- package/dist/packem_shared/failure-log-iUVLf6ts.js +2 -0
- package/dist/packem_shared/flakiness-D9wf0t56.js +1 -0
- package/dist/packem_shared/giget-CcEy_Elm.js +2 -0
- package/dist/packem_shared/index-DH-5hsrC.js +1 -0
- package/dist/packem_shared/otel-DxDUPJJH.js +6 -0
- package/dist/packem_shared/otelPlugin-CQq6poq8.js +1 -0
- package/dist/packem_shared/registry-CkubDdiY.js +2 -0
- package/dist/packem_shared/run-summary-utils-BfBvjzhY.js +1 -0
- package/dist/packem_shared/runtime-check-BXZ43CBW.js +1 -0
- package/dist/packem_shared/selectors-BylODRiM.js +3 -0
- package/dist/packem_shared/symbols-CQmER5MT.js +1 -0
- package/dist/packem_shared/toolchain-BgBOUHII.js +5 -0
- package/dist/packem_shared/typosquats-CcZl99B1.js +1 -0
- package/dist/packem_shared/use-measured-height-DjYgUOKk.js +1 -0
- package/dist/packem_shared/utils-DrNg0XTR.js +1 -0
- package/dist/packem_shared/verify-Baj5mFJ7.js +1 -0
- package/dist/packem_shared/vis-update-app-D1jl0UZZ.js +1 -0
- package/dist/packem_shared/xxh3-DrAUNq4n.js +1 -0
- package/index.js +556 -727
- package/package.json +19 -29
- package/schemas/project.schema.json +739 -297
- package/schemas/vis-config.schema.json +3365 -384
- package/templates/buildkite-ci/template.yml +20 -20
- package/dist/packem_shared/VisUpdateApp-D-Yz_wvg.js +0 -1316
- package/dist/packem_shared/_commonjsHelpers-BqLXS_qQ.js +0 -5
- package/dist/packem_shared/ai-analysis-CHeB1joD.js +0 -367
- package/dist/packem_shared/ai-cache-Be_jexe4.js +0 -142
- package/dist/packem_shared/ai-fix-B9iQVcD2.js +0 -379
- package/dist/packem_shared/cache-directory-2qvs4goY.js +0 -98
- package/dist/packem_shared/catalog-BJTtyi-O.js +0 -1371
- package/dist/packem_shared/dependency-scan-A0KSklpG.js +0 -188
- package/dist/packem_shared/docker-2iZzc280.js +0 -181
- package/dist/packem_shared/failure-log-Cz3Z4SKL.js +0 -100
- package/dist/packem_shared/flakiness-goTxXuCX.js +0 -180
- package/dist/packem_shared/otel-DCvqCTz_.js +0 -158
- package/dist/packem_shared/otelPlugin-DFaLDvJf.js +0 -3
- package/dist/packem_shared/registry-CbqXI0rc.js +0 -272
- package/dist/packem_shared/run-summary-utils-PVMl4aIh.js +0 -130
- package/dist/packem_shared/runtime-check-Cobi3p6l.js +0 -127
- package/dist/packem_shared/selectors-SM69TfqC.js +0 -194
- package/dist/packem_shared/symbols-Ta7g2nU-.js +0 -14
- package/dist/packem_shared/toolchain-BdZd9eBi.js +0 -975
- package/dist/packem_shared/typosquats-C-bCh3PX.js +0 -1210
- package/dist/packem_shared/use-measured-height-CNP0vT4M.js +0 -20
- package/dist/packem_shared/utils-CthVdBPS.js +0 -40
- package/dist/packem_shared/xxh3-Ck8mXNg1.js +0 -239
|
@@ -1,673 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
// Fallback to createRequire
|
|
17
|
-
return __cjs_require(module);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const {
|
|
21
|
-
spawn
|
|
22
|
-
} = __cjs_getBuiltinModule("node:child_process");
|
|
23
|
-
import { yellow, bold, dim, red, green } from '@visulima/colorize';
|
|
24
|
-
import { relative } from '@visulima/path';
|
|
25
|
-
import { readLastRunSummary } from '@visulima/task-runner';
|
|
26
|
-
import { a as aggregateFailureContext, r as runFixAnalysis, b as applyFixProposal, c as resolvePatchPath } from '../packem_shared/ai-fix-B9iQVcD2.js';
|
|
27
|
-
const {
|
|
28
|
-
readFile
|
|
29
|
-
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
30
|
-
import { p as pail } from './bin.js';
|
|
31
|
-
import { r as readRunSummaryById } from '../packem_shared/run-summary-utils-PVMl4aIh.js';
|
|
32
|
-
|
|
33
|
-
const parsePrNumberFromGithubRef = (ref) => {
|
|
34
|
-
if (!ref) {
|
|
35
|
-
return void 0;
|
|
36
|
-
}
|
|
37
|
-
const match = /^refs\/pull\/(\d+)\//.exec(ref);
|
|
38
|
-
return match ? Number.parseInt(match[1], 10) : void 0;
|
|
39
|
-
};
|
|
40
|
-
const readPrNumberFromGithubEvent = async (eventPath) => {
|
|
41
|
-
if (!eventPath) {
|
|
42
|
-
return { prNumber: void 0, sha: void 0 };
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
const raw = await readFile(eventPath, "utf8");
|
|
46
|
-
const payload = JSON.parse(raw);
|
|
47
|
-
const prNumber = payload.pull_request?.number ?? payload.issue?.number ?? payload.number;
|
|
48
|
-
const sha = payload.pull_request?.head?.sha;
|
|
49
|
-
return { prNumber, sha };
|
|
50
|
-
} catch {
|
|
51
|
-
return { prNumber: void 0, sha: void 0 };
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
const detectGithubActions = async (env) => {
|
|
55
|
-
const refPrNumber = parsePrNumberFromGithubRef(env.GITHUB_REF);
|
|
56
|
-
const { prNumber: payloadPrNumber, sha: payloadSha } = refPrNumber === void 0 ? await readPrNumberFromGithubEvent(env.GITHUB_EVENT_PATH) : { prNumber: refPrNumber, sha: void 0 };
|
|
57
|
-
return {
|
|
58
|
-
apiBaseUrl: void 0,
|
|
59
|
-
buildId: void 0,
|
|
60
|
-
buildNumber: void 0,
|
|
61
|
-
prNumber: refPrNumber ?? payloadPrNumber,
|
|
62
|
-
provider: "github-actions",
|
|
63
|
-
repo: env.GITHUB_REPOSITORY,
|
|
64
|
-
// Prefer the PR head SHA from the event payload over GITHUB_SHA — for
|
|
65
|
-
// pull_request events GITHUB_SHA is the synthetic merge commit, which
|
|
66
|
-
// is not what reviewers see in the PR diff.
|
|
67
|
-
sha: payloadSha ?? env.GITHUB_SHA,
|
|
68
|
-
token: env.GITHUB_TOKEN
|
|
69
|
-
};
|
|
70
|
-
};
|
|
71
|
-
const detectGitlabCi = (env) => {
|
|
72
|
-
const mrIid = env.CI_MERGE_REQUEST_IID;
|
|
73
|
-
const prNumber = mrIid !== void 0 && mrIid !== "" ? Number.parseInt(mrIid, 10) : void 0;
|
|
74
|
-
const apiBaseUrl = env.CI_API_V4_URL;
|
|
75
|
-
const token = env.GITLAB_TOKEN ?? env.CI_TOKEN;
|
|
76
|
-
return {
|
|
77
|
-
apiBaseUrl,
|
|
78
|
-
buildId: void 0,
|
|
79
|
-
buildNumber: void 0,
|
|
80
|
-
prNumber: Number.isFinite(prNumber) ? prNumber : void 0,
|
|
81
|
-
provider: "gitlab-ci",
|
|
82
|
-
// GitLab accepts URL-encoded namespace/project or numeric ID.
|
|
83
|
-
// CI_PROJECT_ID is more reliable when the project has been
|
|
84
|
-
// renamed; CI_PROJECT_PATH is more readable. Prefer the ID.
|
|
85
|
-
repo: env.CI_PROJECT_ID ?? env.CI_PROJECT_PATH,
|
|
86
|
-
sha: env.CI_COMMIT_SHA,
|
|
87
|
-
token
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
const detectBuildkite = (env) => {
|
|
91
|
-
const rawPr = env.BUILDKITE_PULL_REQUEST;
|
|
92
|
-
const prNumber = rawPr !== void 0 && rawPr !== "" && rawPr !== "false" ? Number.parseInt(rawPr, 10) : void 0;
|
|
93
|
-
const rawBuildNumber = env.BUILDKITE_BUILD_NUMBER;
|
|
94
|
-
const buildNumber = rawBuildNumber !== void 0 && rawBuildNumber !== "" ? Number.parseInt(rawBuildNumber, 10) : void 0;
|
|
95
|
-
const org = env.BUILDKITE_ORGANIZATION_SLUG;
|
|
96
|
-
const pipeline = env.BUILDKITE_PIPELINE_SLUG;
|
|
97
|
-
const repo = org !== void 0 && org !== "" && pipeline !== void 0 && pipeline !== "" ? `${org}/${pipeline}` : void 0;
|
|
98
|
-
const apiBaseUrl = (env.BUILDKITE_API_BASE_URL ?? "https://api.buildkite.com").replace(/\/+$/, "");
|
|
99
|
-
return {
|
|
100
|
-
apiBaseUrl,
|
|
101
|
-
buildId: env.BUILDKITE_BUILD_ID,
|
|
102
|
-
buildNumber: Number.isFinite(buildNumber) ? buildNumber : void 0,
|
|
103
|
-
prNumber: Number.isFinite(prNumber) ? prNumber : void 0,
|
|
104
|
-
provider: "buildkite",
|
|
105
|
-
repo,
|
|
106
|
-
sha: env.BUILDKITE_COMMIT,
|
|
107
|
-
// Only the REST fallback needs an explicit token; the
|
|
108
|
-
// `buildkite-agent annotate` CLI uses the auto-injected
|
|
109
|
-
// BUILDKITE_AGENT_ACCESS_TOKEN, which we don't surface here
|
|
110
|
-
// because callers don't speak that protocol directly.
|
|
111
|
-
token: env.BUILDKITE_API_TOKEN
|
|
112
|
-
};
|
|
113
|
-
};
|
|
114
|
-
const detectCiContext = async (env = process.env) => {
|
|
115
|
-
if (env.GITHUB_ACTIONS === "true") {
|
|
116
|
-
return await detectGithubActions(env);
|
|
117
|
-
}
|
|
118
|
-
if (env.GITLAB_CI === "true") {
|
|
119
|
-
return detectGitlabCi(env);
|
|
120
|
-
}
|
|
121
|
-
if (env.BUILDKITE === "true") {
|
|
122
|
-
return detectBuildkite(env);
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
apiBaseUrl: void 0,
|
|
126
|
-
buildId: void 0,
|
|
127
|
-
buildNumber: void 0,
|
|
128
|
-
prNumber: void 0,
|
|
129
|
-
provider: "unknown",
|
|
130
|
-
repo: void 0,
|
|
131
|
-
sha: void 0,
|
|
132
|
-
token: void 0
|
|
133
|
-
};
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const runGhComment = (ghBin, prNumber, body, repo) => new Promise((resolve) => {
|
|
137
|
-
const args = ["pr", "comment", String(prNumber), "--body-file", "-"];
|
|
138
|
-
if (repo) {
|
|
139
|
-
args.push("--repo", repo);
|
|
140
|
-
}
|
|
141
|
-
const child = spawn(ghBin, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
142
|
-
let stderr = "";
|
|
143
|
-
child.stderr?.setEncoding("utf8");
|
|
144
|
-
child.stderr?.on("data", (chunk) => {
|
|
145
|
-
stderr += chunk;
|
|
146
|
-
});
|
|
147
|
-
child.once("error", () => {
|
|
148
|
-
resolve({ exitCode: 127, stderr });
|
|
149
|
-
});
|
|
150
|
-
child.once("close", (code) => {
|
|
151
|
-
resolve({ exitCode: code ?? -1, stderr });
|
|
152
|
-
});
|
|
153
|
-
child.stdin?.on("error", () => {
|
|
154
|
-
});
|
|
155
|
-
child.stdin?.end(body);
|
|
156
|
-
});
|
|
157
|
-
const postViaGithubRest = async (fetchImpl, repo, prNumber, body, token) => {
|
|
158
|
-
const url = `https://api.github.com/repos/${repo}/issues/${String(prNumber)}/comments`;
|
|
159
|
-
try {
|
|
160
|
-
const response = await fetchImpl(url, {
|
|
161
|
-
body: JSON.stringify({ body }),
|
|
162
|
-
headers: {
|
|
163
|
-
Accept: "application/vnd.github+json",
|
|
164
|
-
Authorization: `Bearer ${token}`,
|
|
165
|
-
"Content-Type": "application/json",
|
|
166
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
167
|
-
},
|
|
168
|
-
method: "POST"
|
|
169
|
-
});
|
|
170
|
-
if (!response.ok) {
|
|
171
|
-
const text = await response.text().catch(() => "");
|
|
172
|
-
return { error: `GitHub REST returned ${String(response.status)}: ${text.slice(0, 500)}`, ok: false };
|
|
173
|
-
}
|
|
174
|
-
return { ok: true };
|
|
175
|
-
} catch (error) {
|
|
176
|
-
return { error: error instanceof Error ? error.message : String(error), ok: false };
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
const postViaGitlabRest = async (fetchImpl, apiBaseUrl, repo, mrIid, body, token) => {
|
|
180
|
-
const projectId = encodeURIComponent(repo);
|
|
181
|
-
const url = `${apiBaseUrl.replace(/\/+$/, "")}/projects/${projectId}/merge_requests/${String(mrIid)}/notes`;
|
|
182
|
-
try {
|
|
183
|
-
const response = await fetchImpl(url, {
|
|
184
|
-
body: JSON.stringify({ body }),
|
|
185
|
-
headers: {
|
|
186
|
-
"Content-Type": "application/json",
|
|
187
|
-
// GitLab's `PRIVATE-TOKEN` header is its native auth shape;
|
|
188
|
-
// `Authorization: Bearer` also works for OAuth tokens but
|
|
189
|
-
// we route to PRIVATE-TOKEN to support both PAT and project
|
|
190
|
-
// tokens uniformly.
|
|
191
|
-
"PRIVATE-TOKEN": token
|
|
192
|
-
},
|
|
193
|
-
method: "POST"
|
|
194
|
-
});
|
|
195
|
-
if (!response.ok) {
|
|
196
|
-
const text = await response.text().catch(() => "");
|
|
197
|
-
return { error: `GitLab REST returned ${String(response.status)}: ${text.slice(0, 500)}`, ok: false };
|
|
198
|
-
}
|
|
199
|
-
return { ok: true };
|
|
200
|
-
} catch (error) {
|
|
201
|
-
return { error: error instanceof Error ? error.message : String(error), ok: false };
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
const runBuildkiteAnnotate = (binary, body, style, contextName) => new Promise((resolve) => {
|
|
205
|
-
const args = ["annotate", "--style", style, "--context", contextName];
|
|
206
|
-
const child = spawn(binary, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
207
|
-
let stderr = "";
|
|
208
|
-
child.stderr?.setEncoding("utf8");
|
|
209
|
-
child.stderr?.on("data", (chunk) => {
|
|
210
|
-
stderr += chunk;
|
|
211
|
-
});
|
|
212
|
-
child.once("error", () => {
|
|
213
|
-
resolve({ exitCode: 127, stderr });
|
|
214
|
-
});
|
|
215
|
-
child.once("close", (code) => {
|
|
216
|
-
resolve({ exitCode: code ?? -1, stderr });
|
|
217
|
-
});
|
|
218
|
-
child.stdin?.on("error", () => {
|
|
219
|
-
});
|
|
220
|
-
child.stdin?.end(body);
|
|
221
|
-
});
|
|
222
|
-
const postViaBuildkiteRest = async (fetchImpl, apiBaseUrl, repo, buildNumber, body, style, contextName, token) => {
|
|
223
|
-
const [organization, pipeline] = repo.split("/", 2);
|
|
224
|
-
if (!organization || !pipeline) {
|
|
225
|
-
return { error: `Buildkite repo identifier \`${repo}\` is not in {org}/{pipeline} form.`, ok: false };
|
|
226
|
-
}
|
|
227
|
-
const url = `${apiBaseUrl}/v2/organizations/${encodeURIComponent(organization)}/pipelines/${encodeURIComponent(pipeline)}/builds/${String(buildNumber)}/annotations`;
|
|
228
|
-
try {
|
|
229
|
-
const response = await fetchImpl(url, {
|
|
230
|
-
body: JSON.stringify({ body, context: contextName, style }),
|
|
231
|
-
headers: {
|
|
232
|
-
Authorization: `Bearer ${token}`,
|
|
233
|
-
"Content-Type": "application/json"
|
|
234
|
-
},
|
|
235
|
-
method: "POST"
|
|
236
|
-
});
|
|
237
|
-
if (!response.ok) {
|
|
238
|
-
const text = await response.text().catch(() => "");
|
|
239
|
-
return { error: `Buildkite REST returned ${String(response.status)}: ${text.slice(0, 500)}`, ok: false };
|
|
240
|
-
}
|
|
241
|
-
return { ok: true };
|
|
242
|
-
} catch (error) {
|
|
243
|
-
return { error: error instanceof Error ? error.message : String(error), ok: false };
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
const postBuildkiteAnnotation = async (body, context, buildkiteAgentBin, fetchImpl) => {
|
|
247
|
-
const contextName = context.buildId ? `vis-ai-heal-${context.buildId}` : "vis-ai-heal";
|
|
248
|
-
const style = "info";
|
|
249
|
-
const cliResult = await runBuildkiteAnnotate(buildkiteAgentBin, body, style, contextName);
|
|
250
|
-
if (cliResult.exitCode === 0) {
|
|
251
|
-
return { method: "buildkite-cli", posted: true };
|
|
252
|
-
}
|
|
253
|
-
if (!context.apiBaseUrl || !context.repo || context.buildNumber === void 0 || !context.token) {
|
|
254
|
-
const missing = [];
|
|
255
|
-
if (!context.repo) missing.push("BUILDKITE_ORGANIZATION_SLUG / BUILDKITE_PIPELINE_SLUG");
|
|
256
|
-
if (context.buildNumber === void 0) missing.push("BUILDKITE_BUILD_NUMBER");
|
|
257
|
-
if (!context.token) missing.push("BUILDKITE_API_TOKEN (with `write_build_annotations` scope)");
|
|
258
|
-
return {
|
|
259
|
-
error: `buildkite-agent annotate exited ${String(cliResult.exitCode)} (${cliResult.stderr.trim().slice(0, 200)}); cannot fall back to REST without ${missing.join(", ")}`,
|
|
260
|
-
method: "buildkite-cli",
|
|
261
|
-
posted: false
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
const restResult = await postViaBuildkiteRest(fetchImpl, context.apiBaseUrl, context.repo, context.buildNumber, body, style, contextName, context.token);
|
|
265
|
-
if (restResult.ok) {
|
|
266
|
-
return { method: "rest", posted: true };
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
269
|
-
error: `buildkite-agent annotate exited ${String(cliResult.exitCode)}; REST fallback also failed: ${restResult.error ?? "unknown"}`,
|
|
270
|
-
method: "rest",
|
|
271
|
-
posted: false
|
|
272
|
-
};
|
|
273
|
-
};
|
|
274
|
-
const postGithubComment = async (body, context, ghBin, fetchImpl) => {
|
|
275
|
-
if (context.prNumber === void 0) {
|
|
276
|
-
return { method: "skipped", posted: false };
|
|
277
|
-
}
|
|
278
|
-
const ghResult = await runGhComment(ghBin, context.prNumber, body, context.repo);
|
|
279
|
-
if (ghResult.exitCode === 0) {
|
|
280
|
-
return { method: "gh-cli", posted: true };
|
|
281
|
-
}
|
|
282
|
-
if (!context.repo || !context.token) {
|
|
283
|
-
return {
|
|
284
|
-
error: `gh exited ${String(ghResult.exitCode)} (${ghResult.stderr.trim().slice(0, 200)}); cannot fall back to REST without GITHUB_REPOSITORY + GITHUB_TOKEN`,
|
|
285
|
-
method: "gh-cli",
|
|
286
|
-
posted: false
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
const restResult = await postViaGithubRest(fetchImpl, context.repo, context.prNumber, body, context.token);
|
|
290
|
-
if (restResult.ok) {
|
|
291
|
-
return { method: "rest", posted: true };
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
error: `gh exited ${String(ghResult.exitCode)}; REST fallback also failed: ${restResult.error ?? "unknown"}`,
|
|
295
|
-
method: "rest",
|
|
296
|
-
posted: false
|
|
297
|
-
};
|
|
298
|
-
};
|
|
299
|
-
const postGitlabComment = async (body, context, fetchImpl) => {
|
|
300
|
-
if (context.prNumber === void 0) {
|
|
301
|
-
return { method: "skipped", posted: false };
|
|
302
|
-
}
|
|
303
|
-
if (!context.apiBaseUrl || !context.repo) {
|
|
304
|
-
return {
|
|
305
|
-
error: "GitLab CI context is missing CI_API_V4_URL or CI_PROJECT_ID; cannot post note.",
|
|
306
|
-
method: "rest",
|
|
307
|
-
posted: false
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
if (!context.token) {
|
|
311
|
-
return {
|
|
312
|
-
error: "GitLab CI context has no token. CI_JOB_TOKEN cannot post MR notes — set GITLAB_TOKEN to a personal/project access token with `api` scope.",
|
|
313
|
-
method: "rest",
|
|
314
|
-
posted: false
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
const restResult = await postViaGitlabRest(fetchImpl, context.apiBaseUrl, context.repo, context.prNumber, body, context.token);
|
|
318
|
-
if (restResult.ok) {
|
|
319
|
-
return { method: "rest", posted: true };
|
|
320
|
-
}
|
|
321
|
-
return { error: restResult.error, method: "rest", posted: false };
|
|
322
|
-
};
|
|
323
|
-
const postPrComment = async (options) => {
|
|
324
|
-
const { body, buildkiteAgentBin = "buildkite-agent", context, fetchImpl = globalThis.fetch, ghBin = "gh" } = options;
|
|
325
|
-
if (context.provider === "github-actions") {
|
|
326
|
-
return await postGithubComment(body, context, ghBin, fetchImpl);
|
|
327
|
-
}
|
|
328
|
-
if (context.provider === "gitlab-ci") {
|
|
329
|
-
return await postGitlabComment(body, context, fetchImpl);
|
|
330
|
-
}
|
|
331
|
-
if (context.provider === "buildkite") {
|
|
332
|
-
return await postBuildkiteAnnotation(body, context, buildkiteAgentBin, fetchImpl);
|
|
333
|
-
}
|
|
334
|
-
return { method: "skipped", posted: false };
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
const summarizeApply = (results) => {
|
|
338
|
-
let applied = 0;
|
|
339
|
-
let failed = 0;
|
|
340
|
-
for (const result of results) {
|
|
341
|
-
if (result.status === "applied") {
|
|
342
|
-
applied += 1;
|
|
343
|
-
} else {
|
|
344
|
-
failed += 1;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
return { applied, failed };
|
|
348
|
-
};
|
|
349
|
-
const formatDisplayPath = (workspaceRoot, cwd, file) => {
|
|
350
|
-
const absolute = resolvePatchPath(workspaceRoot, cwd, file);
|
|
351
|
-
const rel = relative(workspaceRoot, absolute);
|
|
352
|
-
return rel === "" || rel.startsWith("..") ? absolute : rel;
|
|
353
|
-
};
|
|
354
|
-
const validateFixByRerun = (workspaceRoot, project, target, timeoutMs) => new Promise((resolve) => {
|
|
355
|
-
const visBin = process.argv[1];
|
|
356
|
-
if (!visBin) {
|
|
357
|
-
resolve({ exitCode: -1, stderr: "Cannot locate vis bin (process.argv[1] missing).", stdout: "" });
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
const args = [visBin, "run", target, "--projects", project, "--no-cache", "--summarize", "--fail-fast"];
|
|
361
|
-
const child = spawn(process.execPath, args, {
|
|
362
|
-
cwd: workspaceRoot,
|
|
363
|
-
env: { ...process.env, NO_COLOR: "1" },
|
|
364
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
365
|
-
});
|
|
366
|
-
let stdout = "";
|
|
367
|
-
let stderr = "";
|
|
368
|
-
const timer = setTimeout(() => {
|
|
369
|
-
child.kill("SIGTERM");
|
|
370
|
-
setTimeout(() => child.kill("SIGKILL"), 2e3).unref();
|
|
371
|
-
}, timeoutMs);
|
|
372
|
-
child.stdout?.setEncoding("utf8");
|
|
373
|
-
child.stdout?.on("data", (chunk) => {
|
|
374
|
-
stdout += chunk;
|
|
375
|
-
});
|
|
376
|
-
child.stderr?.setEncoding("utf8");
|
|
377
|
-
child.stderr?.on("data", (chunk) => {
|
|
378
|
-
stderr += chunk;
|
|
379
|
-
});
|
|
380
|
-
child.once("error", (error) => {
|
|
381
|
-
clearTimeout(timer);
|
|
382
|
-
resolve({ exitCode: -1, stderr: error.message, stdout });
|
|
383
|
-
});
|
|
384
|
-
child.once("close", (code) => {
|
|
385
|
-
clearTimeout(timer);
|
|
386
|
-
resolve({ exitCode: code ?? -1, stderr, stdout });
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
const MAX_COMMENT_BYTES = 6e4;
|
|
390
|
-
const pickFence = (content) => {
|
|
391
|
-
let longest = 0;
|
|
392
|
-
const matches = content.match(/`{3,}/g);
|
|
393
|
-
if (matches) {
|
|
394
|
-
for (const m of matches) {
|
|
395
|
-
longest = Math.max(longest, m.length);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return "`".repeat(Math.max(3, longest + 1));
|
|
399
|
-
};
|
|
400
|
-
const renderProposalDiff = (proposal, workspaceRoot, cwd) => {
|
|
401
|
-
if (proposal.patches.length === 0) {
|
|
402
|
-
return "_No patches proposed._";
|
|
403
|
-
}
|
|
404
|
-
const blocks = [];
|
|
405
|
-
for (const [index, patch] of proposal.patches.entries()) {
|
|
406
|
-
const displayPath = formatDisplayPath(workspaceRoot, cwd, patch.file);
|
|
407
|
-
const lines = [];
|
|
408
|
-
lines.push(`**[${String(index + 1)}] \`${displayPath}\`**`);
|
|
409
|
-
if (patch.reason) {
|
|
410
|
-
lines.push(`_${patch.reason}_`);
|
|
411
|
-
}
|
|
412
|
-
const fence = pickFence(`${patch.oldString}
|
|
413
|
-
${patch.newString}`);
|
|
414
|
-
lines.push(`${fence}diff`);
|
|
415
|
-
for (const line of patch.oldString.split("\n")) {
|
|
416
|
-
lines.push(`- ${line}`);
|
|
417
|
-
}
|
|
418
|
-
for (const line of patch.newString.split("\n")) {
|
|
419
|
-
lines.push(`+ ${line}`);
|
|
420
|
-
}
|
|
421
|
-
lines.push(fence);
|
|
422
|
-
blocks.push(lines.join("\n"));
|
|
423
|
-
}
|
|
424
|
-
return blocks.join("\n\n");
|
|
425
|
-
};
|
|
426
|
-
const renderCommentBody = (proposal, failureContext, workspaceRoot, sha) => {
|
|
427
|
-
const header = [];
|
|
428
|
-
header.push("## vis ai heal — proposed fix");
|
|
429
|
-
header.push("");
|
|
430
|
-
header.push(`Failing task: \`${failureContext.taskId}\` (provider: \`${proposal.provider}\`, confidence: \`${proposal.confidence}\`)`);
|
|
431
|
-
if (sha) {
|
|
432
|
-
header.push(`Run anchored at \`${sha.slice(0, 7)}\`.`);
|
|
433
|
-
}
|
|
434
|
-
header.push("");
|
|
435
|
-
header.push("### Root cause");
|
|
436
|
-
header.push(proposal.explanation || "_(no explanation)_");
|
|
437
|
-
header.push("");
|
|
438
|
-
const diffSection = [];
|
|
439
|
-
diffSection.push("### Proposed patch");
|
|
440
|
-
diffSection.push(renderProposalDiff(proposal, workspaceRoot, failureContext.cwd));
|
|
441
|
-
diffSection.push("");
|
|
442
|
-
const footer = [];
|
|
443
|
-
footer.push("### Validation");
|
|
444
|
-
footer.push(`Re-ran \`${failureContext.taskId}\` after applying the patch on the CI runner — task **passed**.`);
|
|
445
|
-
footer.push("");
|
|
446
|
-
footer.push("### Apply locally");
|
|
447
|
-
footer.push("```sh");
|
|
448
|
-
footer.push(`vis ai fix ${failureContext.taskId} --apply`);
|
|
449
|
-
footer.push("```");
|
|
450
|
-
footer.push("");
|
|
451
|
-
footer.push("---");
|
|
452
|
-
footer.push("");
|
|
453
|
-
footer.push("_Auto-generated by `vis ai heal`. Auto-commit is on the roadmap; for now the patch lives in this comment and is yours to accept or reject._");
|
|
454
|
-
const full = [...header, ...diffSection, ...footer].join("\n");
|
|
455
|
-
if (Buffer.byteLength(full, "utf8") <= MAX_COMMENT_BYTES) {
|
|
456
|
-
return full;
|
|
457
|
-
}
|
|
458
|
-
const truncatedDiff = [
|
|
459
|
-
"### Proposed patch",
|
|
460
|
-
`_Patch set is too large for a comment (${String(proposal.patches.length)} files). Run \`vis ai fix ${failureContext.taskId} --apply\` locally to inspect and apply it._`,
|
|
461
|
-
""
|
|
462
|
-
];
|
|
463
|
-
return [...header, ...truncatedDiff, ...footer].join("\n");
|
|
464
|
-
};
|
|
465
|
-
const renderProposalForLog = (proposal, workspaceRoot, cwd) => {
|
|
466
|
-
const lines = [];
|
|
467
|
-
lines.push(bold(`Proposal (${proposal.provider}, confidence: ${proposal.confidence})`));
|
|
468
|
-
lines.push(proposal.explanation || dim("<no explanation>"));
|
|
469
|
-
for (const [index, patch] of proposal.patches.entries()) {
|
|
470
|
-
lines.push("");
|
|
471
|
-
lines.push(`[${String(index + 1)}] ${formatDisplayPath(workspaceRoot, cwd, patch.file)}`);
|
|
472
|
-
if (patch.reason) {
|
|
473
|
-
lines.push(dim(` reason: ${patch.reason}`));
|
|
474
|
-
}
|
|
475
|
-
for (const line of patch.oldString.split("\n")) {
|
|
476
|
-
lines.push(red(` - ${line}`));
|
|
477
|
-
}
|
|
478
|
-
for (const line of patch.newString.split("\n")) {
|
|
479
|
-
lines.push(green(` + ${line}`));
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
return lines.join("\n");
|
|
483
|
-
};
|
|
484
|
-
const findFirstFailedTask = async (workspaceRoot, runId) => {
|
|
485
|
-
const summary = runId === void 0 ? await readLastRunSummary(workspaceRoot) : await readRunSummaryById(workspaceRoot, runId);
|
|
486
|
-
if (!summary) {
|
|
487
|
-
return void 0;
|
|
488
|
-
}
|
|
489
|
-
for (const task of summary.tasks) {
|
|
490
|
-
const failed = task.exitCode !== void 0 && task.exitCode !== 0;
|
|
491
|
-
if (failed) {
|
|
492
|
-
return {
|
|
493
|
-
project: task.target?.project,
|
|
494
|
-
runId: summary.id,
|
|
495
|
-
target: task.target?.target,
|
|
496
|
-
taskId: task.taskId
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return void 0;
|
|
501
|
-
};
|
|
502
|
-
const DEFAULT_VALIDATION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
503
|
-
const findHealCandidate = async (workspaceRoot, runId) => {
|
|
504
|
-
const failed = await findFirstFailedTask(workspaceRoot, runId);
|
|
505
|
-
if (!failed) {
|
|
506
|
-
return { outcome: "no-failed-task" };
|
|
507
|
-
}
|
|
508
|
-
if (!failed.project || !failed.target) {
|
|
509
|
-
return { failedTask: failed, outcome: "missing-metadata" };
|
|
510
|
-
}
|
|
511
|
-
const failureContext = await aggregateFailureContext(workspaceRoot, failed.taskId, { runId: failed.runId });
|
|
512
|
-
if (!failureContext) {
|
|
513
|
-
return { failedTask: { project: failed.project, runId: failed.runId, target: failed.target, taskId: failed.taskId }, outcome: "no-failure-context" };
|
|
514
|
-
}
|
|
515
|
-
return {
|
|
516
|
-
failedTask: { project: failed.project, runId: failed.runId, target: failed.target, taskId: failed.taskId },
|
|
517
|
-
failureContext,
|
|
518
|
-
outcome: "ready"
|
|
519
|
-
};
|
|
520
|
-
};
|
|
521
|
-
const proposeAndApply = async (toolbox, candidate) => {
|
|
522
|
-
const { logger, options, visConfig, workspaceRoot: wsRoot } = toolbox;
|
|
523
|
-
const workspaceRoot = wsRoot ?? process.cwd();
|
|
524
|
-
const dryRun = options.dryRun === true;
|
|
525
|
-
const aiConfig = visConfig?.ai;
|
|
526
|
-
const proposal = await runFixAnalysis(candidate.failureContext, logger, {
|
|
527
|
-
cache: options.noCache !== true,
|
|
528
|
-
config: aiConfig
|
|
529
|
-
});
|
|
530
|
-
if (!proposal) {
|
|
531
|
-
return { outcome: "no-proposal" };
|
|
532
|
-
}
|
|
533
|
-
if (proposal.cannotFix) {
|
|
534
|
-
return { detail: proposal.cannotFix, outcome: "cannot-fix", proposal };
|
|
535
|
-
}
|
|
536
|
-
if (proposal.patches.length === 0) {
|
|
537
|
-
return { outcome: "empty-patches", proposal };
|
|
538
|
-
}
|
|
539
|
-
if (dryRun) {
|
|
540
|
-
return { outcome: "dry-run", proposal };
|
|
541
|
-
}
|
|
542
|
-
const applyResults = await applyFixProposal(workspaceRoot, candidate.failureContext.cwd, proposal);
|
|
543
|
-
const applySummary = summarizeApply(applyResults);
|
|
544
|
-
if (applySummary.applied === 0) {
|
|
545
|
-
return { applyResults, outcome: "no-patches-applied", proposal };
|
|
546
|
-
}
|
|
547
|
-
return { applyResults, outcome: "applied", proposal };
|
|
548
|
-
};
|
|
549
|
-
const validateAppliedFix = async (toolbox, candidate, deps = {}) => {
|
|
550
|
-
const workspaceRoot = toolbox.workspaceRoot ?? process.cwd();
|
|
551
|
-
const validationTimeoutMs = toolbox.options.validationTimeout === void 0 ? DEFAULT_VALIDATION_TIMEOUT_MS : toolbox.options.validationTimeout * 1e3;
|
|
552
|
-
const validate = deps.validate ?? ((project, target) => validateFixByRerun(workspaceRoot, project, target, validationTimeoutMs));
|
|
553
|
-
return await validate(candidate.failedTask.project, candidate.failedTask.target);
|
|
554
|
-
};
|
|
555
|
-
const postHealComment = async (workspaceRoot, proposal, failureContext, deps = {}) => {
|
|
556
|
-
const detectCi = deps.detectCi ?? detectCiContext;
|
|
557
|
-
const ciContext = await detectCi();
|
|
558
|
-
const surface = ciContext.provider === "gitlab-ci" ? "MR" : ciContext.provider === "buildkite" ? "annotation" : "PR";
|
|
559
|
-
if (ciContext.provider === "unknown") {
|
|
560
|
-
return { ciContext, outcome: "no-ci", surface };
|
|
561
|
-
}
|
|
562
|
-
const commentBody = renderCommentBody(proposal, failureContext, workspaceRoot, ciContext.sha);
|
|
563
|
-
const postCommentImpl = deps.postComment ?? (async (body, context) => postPrComment({ body, context }));
|
|
564
|
-
const postResult = await postCommentImpl(commentBody, ciContext);
|
|
565
|
-
if (postResult.posted) {
|
|
566
|
-
return { ciContext, method: postResult.method, outcome: "posted", surface };
|
|
567
|
-
}
|
|
568
|
-
if (postResult.method === "skipped") {
|
|
569
|
-
return { ciContext, outcome: "no-pr", surface };
|
|
570
|
-
}
|
|
571
|
-
return { ciContext, error: postResult.error, method: postResult.method, outcome: "post-failed", surface };
|
|
572
|
-
};
|
|
573
|
-
const heal = async (toolbox, deps = {}) => {
|
|
574
|
-
const { logger, workspaceRoot: wsRoot } = toolbox;
|
|
575
|
-
const workspaceRoot = wsRoot ?? process.cwd();
|
|
576
|
-
const candidateResult = await findHealCandidate(workspaceRoot, toolbox.options.run);
|
|
577
|
-
if (candidateResult.outcome === "no-failed-task") {
|
|
578
|
-
pail.info("No failed tasks found in the latest run summary. Nothing to heal.");
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
if (candidateResult.outcome === "missing-metadata") {
|
|
582
|
-
pail.error(`Failed task ${candidateResult.failedTask.taskId} is missing project/target metadata in the run summary; cannot validate a fix.`);
|
|
583
|
-
process.exitCode = 1;
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
if (candidateResult.outcome === "no-failure-context") {
|
|
587
|
-
pail.error(`No failure log or run summary found for ${candidateResult.failedTask.taskId}.`);
|
|
588
|
-
process.exitCode = 1;
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
const candidate = { failedTask: candidateResult.failedTask, failureContext: candidateResult.failureContext };
|
|
592
|
-
pail.info(`Healing ${candidate.failedTask.taskId} (run ${candidate.failedTask.runId ?? "unknown"})`);
|
|
593
|
-
if (!candidate.failureContext.terminalOutputCaptured) {
|
|
594
|
-
pail.warn(`No captured terminal output for ${candidate.failedTask.taskId}; the AI proposal will be weaker without it.`);
|
|
595
|
-
}
|
|
596
|
-
const proposeResult = await proposeAndApply(toolbox, candidate);
|
|
597
|
-
if (proposeResult.outcome === "no-proposal") {
|
|
598
|
-
pail.error("AI fix proposal failed or no provider available.");
|
|
599
|
-
process.exitCode = 1;
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
if (proposeResult.outcome === "cannot-fix") {
|
|
603
|
-
pail.warn(`AI declined to fix: ${proposeResult.detail ?? "(no reason)"}`);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
if (proposeResult.outcome === "empty-patches") {
|
|
607
|
-
pail.warn("AI returned an empty patch set.");
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
const proposal = proposeResult.proposal;
|
|
611
|
-
logger.info(renderProposalForLog(proposal, workspaceRoot, candidate.failureContext.cwd));
|
|
612
|
-
if (proposeResult.outcome === "dry-run") {
|
|
613
|
-
pail.info("Dry run: skipping apply, validation, and PR comment.");
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
const applyResults = proposeResult.applyResults ?? [];
|
|
617
|
-
const applySummary = summarizeApply(applyResults);
|
|
618
|
-
pail.info(`Applied ${String(applySummary.applied)}/${String(applyResults.length)} patches.`);
|
|
619
|
-
if (proposeResult.outcome === "no-patches-applied") {
|
|
620
|
-
pail.error("No patches could be applied (all failed validation).");
|
|
621
|
-
process.exitCode = 1;
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
pail.info(`Re-running ${candidate.failedTask.taskId} to validate the fix...`);
|
|
625
|
-
const validation = await validateAppliedFix(toolbox, candidate, { validate: deps.validate });
|
|
626
|
-
if (validation.exitCode !== 0) {
|
|
627
|
-
pail.error(`Validation failed (exit ${String(validation.exitCode)}). Patch is not posted.`);
|
|
628
|
-
if (validation.stderr.trim().length > 0) {
|
|
629
|
-
logger.info(yellow("--- validation stderr (tail) ---"));
|
|
630
|
-
logger.info(validation.stderr.split("\n").slice(-20).join("\n"));
|
|
631
|
-
}
|
|
632
|
-
process.exitCode = 1;
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
pail.success(`Validation passed.`);
|
|
636
|
-
const postResult = await postHealComment(workspaceRoot, proposal, candidate.failureContext, {
|
|
637
|
-
detectCi: deps.detectCi,
|
|
638
|
-
postComment: deps.postComment
|
|
639
|
-
});
|
|
640
|
-
if (postResult.outcome === "no-ci") {
|
|
641
|
-
pail.notice("Not running in a recognised CI provider; skipping PR comment. Patch was applied + validated locally.");
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
if (postResult.outcome === "posted") {
|
|
645
|
-
const identifier = postResult.surface === "annotation" ? `build #${String(postResult.ciContext.buildNumber ?? "?")}` : `${postResult.surface} #${String(postResult.ciContext.prNumber)}`;
|
|
646
|
-
pail.success(`Posted fix proposal to ${identifier} via ${postResult.method ?? "unknown"}.`);
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
if (postResult.outcome === "no-pr") {
|
|
650
|
-
pail.notice(`No ${postResult.surface} number detected (push-event run); skipping comment.`);
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
const noun = postResult.surface === "annotation" ? "annotation" : `${postResult.surface} comment`;
|
|
654
|
-
pail.error(`Failed to post ${noun}: ${postResult.error ?? "unknown error"}`);
|
|
655
|
-
};
|
|
656
|
-
const aiHeal = async (toolbox) => {
|
|
657
|
-
await heal(toolbox);
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
const heal$1 = /*#__PURE__*/Object.defineProperty({
|
|
661
|
-
__proto__: null,
|
|
662
|
-
aiHeal,
|
|
663
|
-
findHealCandidate,
|
|
664
|
-
pickFenceForTesting: pickFence,
|
|
665
|
-
postHealComment,
|
|
666
|
-
proposeAndApply,
|
|
667
|
-
renderCommentBodyForTesting: renderCommentBody,
|
|
668
|
-
renderProposalDiffForTesting: renderProposalDiff,
|
|
669
|
-
runHealForTesting: heal,
|
|
670
|
-
validateAppliedFix
|
|
671
|
-
}, Symbol.toStringTag, { value: 'Module' });
|
|
672
|
-
|
|
673
|
-
export { postPrComment as a, detectCiContext as d, findHealCandidate as f, heal$1 as h, proposeAndApply as p, validateAppliedFix as v };
|
|
1
|
+
var U=Object.defineProperty;var I=(t,e)=>U(t,"name",{value:e,configurable:!0});import{createRequire as A}from"node:module";import{bold as O,dim as v,red as G,green as j,yellow as F}from"@visulima/colorize";import{relative as D}from"@visulima/path";import{readLastRunSummary as K}from"@visulima/task-runner";import{r as H,a as M,b as V,c as J}from"../packem_shared/ai-fix-nn4zOE95.js";import{p as u}from"./bin.js";import{r as z}from"../packem_shared/run-summary-utils-BfBvjzhY.js";const L=A(import.meta.url),b=typeof globalThis<"u"&&typeof globalThis.process<"u"?globalThis.process:process,T=I(t=>{if(typeof b<"u"&&b.versions&&b.versions.node){const[e,o]=b.versions.node.split(".").map(Number);if(e>22||e===22&&o>=3||e===20&&o>=16)return b.getBuiltinModule(t)}return L(t)},"__cjs_getBuiltinModule"),{spawn:k}=T("node:child_process"),{readFile:q}=T("node:fs/promises");var Q=Object.defineProperty,g=I((t,e)=>Q(t,"name",{value:e,configurable:!0}),"i");const W=g(t=>{if(!t)return;const e=/^refs\/pull\/(\d+)\//.exec(t);return e?Number.parseInt(e[1],10):void 0},"parsePrNumberFromGithubRef"),Y=g(async t=>{if(!t)return{prNumber:void 0,sha:void 0};try{const e=await q(t,"utf8"),o=JSON.parse(e),r=o.pull_request?.number??o.issue?.number??o.number,i=o.pull_request?.head?.sha;return{prNumber:r,sha:i}}catch{return{prNumber:void 0,sha:void 0}}},"readPrNumberFromGithubEvent"),Z=g(async t=>{const e=W(t.GITHUB_REF),{prNumber:o,sha:r}=e===void 0?await Y(t.GITHUB_EVENT_PATH):{prNumber:e,sha:void 0};return{apiBaseUrl:void 0,buildId:void 0,buildNumber:void 0,prNumber:e??o,provider:"github-actions",repo:t.GITHUB_REPOSITORY,sha:r??t.GITHUB_SHA,token:t.GITHUB_TOKEN}},"detectGithubActions"),X=g(t=>{const e=t.CI_MERGE_REQUEST_IID,o=e!==void 0&&e!==""?Number.parseInt(e,10):void 0,r=t.CI_API_V4_URL,i=t.GITLAB_TOKEN??t.CI_TOKEN;return{apiBaseUrl:r,buildId:void 0,buildNumber:void 0,prNumber:Number.isFinite(o)?o:void 0,provider:"gitlab-ci",repo:t.CI_PROJECT_ID??t.CI_PROJECT_PATH,sha:t.CI_COMMIT_SHA,token:i}},"detectGitlabCi"),ee=g(t=>{const e=t.BUILDKITE_PULL_REQUEST,o=e!==void 0&&e!==""&&e!=="false"?Number.parseInt(e,10):void 0,r=t.BUILDKITE_BUILD_NUMBER,i=r!==void 0&&r!==""?Number.parseInt(r,10):void 0,n=t.BUILDKITE_ORGANIZATION_SLUG,a=t.BUILDKITE_PIPELINE_SLUG,s=n!==void 0&&n!==""&&a!==void 0&&a!==""?`${n}/${a}`:void 0;return{apiBaseUrl:(t.BUILDKITE_API_BASE_URL??"https://api.buildkite.com").replace(/\/+$/,""),buildId:t.BUILDKITE_BUILD_ID,buildNumber:Number.isFinite(i)?i:void 0,prNumber:Number.isFinite(o)?o:void 0,provider:"buildkite",repo:s,sha:t.BUILDKITE_COMMIT,token:t.BUILDKITE_API_TOKEN}},"detectBuildkite"),te=g(async(t=process.env)=>t.GITHUB_ACTIONS==="true"?await Z(t):t.GITLAB_CI==="true"?X(t):t.BUILDKITE==="true"?ee(t):{apiBaseUrl:void 0,buildId:void 0,buildNumber:void 0,prNumber:void 0,provider:"unknown",repo:void 0,sha:void 0,token:void 0},"detectCiContext");var oe=Object.defineProperty,h=I((t,e)=>oe(t,"name",{value:e,configurable:!0}),"p");const re=h((t,e,o,r)=>new Promise(i=>{const n=["pr","comment",String(e),"--body-file","-"];r&&n.push("--repo",r);const a=k(t,n,{stdio:["pipe","ignore","pipe"]});let s="";a.stderr?.setEncoding("utf8"),a.stderr?.on("data",d=>{s+=d}),a.once("error",()=>{i({exitCode:127,stderr:s})}),a.once("close",d=>{i({exitCode:d??-1,stderr:s})}),a.stdin?.on("error",()=>{}),a.stdin?.end(o)}),"runGhComment"),ie=h(async(t,e,o,r,i)=>{const n=`https://api.github.com/repos/${e}/issues/${String(o)}/comments`;try{const a=await t(n,{body:JSON.stringify({body:r}),headers:{Accept:"application/vnd.github+json",Authorization:`Bearer ${i}`,"Content-Type":"application/json","X-GitHub-Api-Version":"2022-11-28"},method:"POST"});if(!a.ok){const s=await a.text().catch(()=>"");return{error:`GitHub REST returned ${String(a.status)}: ${s.slice(0,500)}`,ok:!1}}return{ok:!0}}catch(a){return{error:a instanceof Error?a.message:String(a),ok:!1}}},"postViaGithubRest"),ne=h(async(t,e,o,r,i,n)=>{const a=encodeURIComponent(o),s=`${e.replace(/\/+$/,"")}/projects/${a}/merge_requests/${String(r)}/notes`;try{const d=await t(s,{body:JSON.stringify({body:i}),headers:{"Content-Type":"application/json","PRIVATE-TOKEN":n},method:"POST"});if(!d.ok){const p=await d.text().catch(()=>"");return{error:`GitLab REST returned ${String(d.status)}: ${p.slice(0,500)}`,ok:!1}}return{ok:!0}}catch(d){return{error:d instanceof Error?d.message:String(d),ok:!1}}},"postViaGitlabRest"),ae=h((t,e,o,r)=>new Promise(i=>{const n=k(t,["annotate","--style",o,"--context",r],{stdio:["pipe","ignore","pipe"]});let a="";n.stderr?.setEncoding("utf8"),n.stderr?.on("data",s=>{a+=s}),n.once("error",()=>{i({exitCode:127,stderr:a})}),n.once("close",s=>{i({exitCode:s??-1,stderr:a})}),n.stdin?.on("error",()=>{}),n.stdin?.end(e)}),"runBuildkiteAnnotate"),se=h(async(t,e,o,r,i,n,a,s)=>{const[d,p]=o.split("/",2);if(!d||!p)return{error:`Buildkite repo identifier \`${o}\` is not in {org}/{pipeline} form.`,ok:!1};const m=`${e}/v2/organizations/${encodeURIComponent(d)}/pipelines/${encodeURIComponent(p)}/builds/${String(r)}/annotations`;try{const c=await t(m,{body:JSON.stringify({body:i,context:a,style:n}),headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},method:"POST"});if(!c.ok){const l=await c.text().catch(()=>"");return{error:`Buildkite REST returned ${String(c.status)}: ${l.slice(0,500)}`,ok:!1}}return{ok:!0}}catch(c){return{error:c instanceof Error?c.message:String(c),ok:!1}}},"postViaBuildkiteRest"),de=h(async(t,e,o,r)=>{const i=e.buildId?`vis-ai-heal-${e.buildId}`:"vis-ai-heal",n="info",a=await ae(o,t,n,i);if(a.exitCode===0)return{method:"buildkite-cli",posted:!0};if(!e.apiBaseUrl||!e.repo||e.buildNumber===void 0||!e.token){const d=[];return e.repo||d.push("BUILDKITE_ORGANIZATION_SLUG / BUILDKITE_PIPELINE_SLUG"),e.buildNumber===void 0&&d.push("BUILDKITE_BUILD_NUMBER"),e.token||d.push("BUILDKITE_API_TOKEN (with `write_build_annotations` scope)"),{error:`buildkite-agent annotate exited ${String(a.exitCode)} (${a.stderr.trim().slice(0,200)}); cannot fall back to REST without ${d.join(", ")}`,method:"buildkite-cli",posted:!1}}const s=await se(r,e.apiBaseUrl,e.repo,e.buildNumber,t,n,i,e.token);return s.ok?{method:"rest",posted:!0}:{error:`buildkite-agent annotate exited ${String(a.exitCode)}; REST fallback also failed: ${s.error??"unknown"}`,method:"rest",posted:!1}},"postBuildkiteAnnotation"),pe=h(async(t,e,o,r)=>{if(e.prNumber===void 0)return{method:"skipped",posted:!1};const i=await re(o,e.prNumber,t,e.repo);if(i.exitCode===0)return{method:"gh-cli",posted:!0};if(!e.repo||!e.token)return{error:`gh exited ${String(i.exitCode)} (${i.stderr.trim().slice(0,200)}); cannot fall back to REST without GITHUB_REPOSITORY + GITHUB_TOKEN`,method:"gh-cli",posted:!1};const n=await ie(r,e.repo,e.prNumber,t,e.token);return n.ok?{method:"rest",posted:!0}:{error:`gh exited ${String(i.exitCode)}; REST fallback also failed: ${n.error??"unknown"}`,method:"rest",posted:!1}},"postGithubComment"),ce=h(async(t,e,o)=>{if(e.prNumber===void 0)return{method:"skipped",posted:!1};if(!e.apiBaseUrl||!e.repo)return{error:"GitLab CI context is missing CI_API_V4_URL or CI_PROJECT_ID; cannot post note.",method:"rest",posted:!1};if(!e.token)return{error:"GitLab CI context has no token. CI_JOB_TOKEN cannot post MR notes — set GITLAB_TOKEN to a personal/project access token with `api` scope.",method:"rest",posted:!1};const r=await ne(o,e.apiBaseUrl,e.repo,e.prNumber,t,e.token);return r.ok?{method:"rest",posted:!0}:{error:r.error,method:"rest",posted:!1}},"postGitlabComment"),ue=h(async t=>{const{body:e,buildkiteAgentBin:o="buildkite-agent",context:r,fetchImpl:i=globalThis.fetch,ghBin:n="gh"}=t;return r.provider==="github-actions"?await pe(e,r,n,i):r.provider==="gitlab-ci"?await ce(e,r,i):r.provider==="buildkite"?await de(e,r,o,i):{method:"skipped",posted:!1}},"postPrComment");var le=Object.defineProperty,f=I((t,e)=>le(t,"name",{value:e,configurable:!0}),"l");const C=f(t=>{let e=0,o=0;for(const r of t)r.status==="applied"?e+=1:o+=1;return{applied:e,failed:o}},"summarizeApply"),_=f((t,e,o)=>{const r=H(t,e,o),i=D(t,r);return i===""||i.startsWith("..")?r:i},"formatDisplayPath"),fe=f((t,e,o,r)=>new Promise(i=>{const n=process.argv[1];if(!n){i({exitCode:-1,stderr:"Cannot locate vis bin (process.argv[1] missing).",stdout:""});return}const a=[n,"run",o,"--projects",e,"--no-cache","--summarize","--fail-fast"],s=k(process.execPath,a,{cwd:t,env:{...process.env,NO_COLOR:"1"},stdio:["ignore","pipe","pipe"]});let d="",p="";const m=setTimeout(()=>{s.kill("SIGTERM"),setTimeout(()=>s.kill("SIGKILL"),2e3).unref()},r);s.stdout?.setEncoding("utf8"),s.stdout?.on("data",c=>{d+=c}),s.stderr?.setEncoding("utf8"),s.stderr?.on("data",c=>{p+=c}),s.once("error",c=>{clearTimeout(m),i({exitCode:-1,stderr:c.message,stdout:d})}),s.once("close",c=>{clearTimeout(m),i({exitCode:c??-1,stderr:p,stdout:d})})}),"validateFixByRerun"),me=6e4,y=f(t=>{let e=0;const o=t.match(/`{3,}/g);if(o)for(const r of o)e=Math.max(e,r.length);return"`".repeat(Math.max(3,e+1))},"pickFence"),$=f((t,e,o)=>{if(t.patches.length===0)return"_No patches proposed._";const r=[];for(const[i,n]of t.patches.entries()){const a=_(e,o,n.file),s=[`**[${String(i+1)}] \`${a}\`**`];n.reason&&s.push(`_${n.reason}_`);const d=y(`${n.oldString}
|
|
2
|
+
${n.newString}`);s.push(`${d}diff`);for(const p of n.oldString.split(`
|
|
3
|
+
`))s.push(`- ${p}`);for(const p of n.newString.split(`
|
|
4
|
+
`))s.push(`+ ${p}`);s.push(d),r.push(s.join(`
|
|
5
|
+
`))}return r.join(`
|
|
6
|
+
|
|
7
|
+
`)},"renderProposalDiff"),x=f((t,e,o,r)=>{const i=["## vis ai heal — proposed fix","",`Failing task: \`${e.taskId}\` (provider: \`${t.provider}\`, confidence: \`${t.confidence}\`)`];r&&i.push(`Run anchored at \`${r.slice(0,7)}\`.`),i.push(""),i.push("### Root cause"),i.push(t.explanation||"_(no explanation)_"),i.push("");const n=["### Proposed patch",$(t,o,e.cwd),""],a=["### Validation",`Re-ran \`${e.taskId}\` after applying the patch on the CI runner — task **passed**.`,"","### Apply locally","```sh",`vis ai fix ${e.taskId} --apply`,"```","","---","","_Auto-generated by `vis ai heal`. Auto-commit is on the roadmap; for now the patch lives in this comment and is yours to accept or reject._"],s=[...i,...n,...a].join(`
|
|
8
|
+
`);if(Buffer.byteLength(s,"utf8")<=me)return s;const d=["### Proposed patch",`_Patch set is too large for a comment (${String(t.patches.length)} files). Run \`vis ai fix ${e.taskId} --apply\` locally to inspect and apply it._`,""];return[...i,...d,...a].join(`
|
|
9
|
+
`)},"renderCommentBody"),he=f((t,e,o)=>{const r=[O(`Proposal (${t.provider}, confidence: ${t.confidence})`),t.explanation||v("<no explanation>")];for(const[i,n]of t.patches.entries()){r.push(""),r.push(`[${String(i+1)}] ${_(e,o,n.file)}`),n.reason&&r.push(v(` reason: ${n.reason}`));for(const a of n.oldString.split(`
|
|
10
|
+
`))r.push(G(` - ${a}`));for(const a of n.newString.split(`
|
|
11
|
+
`))r.push(j(` + ${a}`))}return r.join(`
|
|
12
|
+
`)},"renderProposalForLog"),ge=f(async(t,e)=>{const o=e===void 0?await K(t):await z(t,e);if(o){for(const r of o.tasks)if(r.exitCode!==void 0&&r.exitCode!==0)return{project:r.target?.project,runId:o.id,target:r.target?.target,taskId:r.taskId}}},"findFirstFailedTask"),Ie=1800*1e3,w=f(async(t,e)=>{const o=await ge(t,e);if(!o)return{outcome:"no-failed-task"};if(!o.project||!o.target)return{failedTask:o,outcome:"missing-metadata"};const r=await M(t,o.taskId,{runId:o.runId});return r?{failedTask:{project:o.project,runId:o.runId,target:o.target,taskId:o.taskId},failureContext:r,outcome:"ready"}:{failedTask:{project:o.project,runId:o.runId,target:o.target,taskId:o.taskId},outcome:"no-failure-context"}},"findHealCandidate"),N=f(async(t,e)=>{const{logger:o,options:r,visConfig:i,workspaceRoot:n}=t,a=n??process.cwd(),s=r.dryRun===!0,d=i?.ai,p=await V(e.failureContext,o,{cache:r.noCache!==!0,config:d});if(!p)return{outcome:"no-proposal"};if(p.cannotFix)return{detail:p.cannotFix,outcome:"cannot-fix",proposal:p};if(p.patches.length===0)return{outcome:"empty-patches",proposal:p};if(s)return{outcome:"dry-run",proposal:p};const m=await J(a,e.failureContext.cwd,p);return C(m).applied===0?{applyResults:m,outcome:"no-patches-applied",proposal:p}:{applyResults:m,outcome:"applied",proposal:p}},"proposeAndApply"),E=f(async(t,e,o={})=>{const r=t.workspaceRoot??process.cwd(),i=t.options.validationTimeout===void 0?Ie:t.options.validationTimeout*1e3;return await(o.validate??((n,a)=>fe(r,n,a,i)))(e.failedTask.project,e.failedTask.target)},"validateAppliedFix"),S=f(async(t,e,o,r={})=>{const i=await(r.detectCi??te)(),n=i.provider==="gitlab-ci"?"MR":i.provider==="buildkite"?"annotation":"PR";if(i.provider==="unknown")return{ciContext:i,outcome:"no-ci",surface:n};const a=x(e,o,t,i.sha),s=await(r.postComment??(async(d,p)=>ue({body:d,context:p})))(a,i);return s.posted?{ciContext:i,method:s.method,outcome:"posted",surface:n}:s.method==="skipped"?{ciContext:i,outcome:"no-pr",surface:n}:{ciContext:i,error:s.error,method:s.method,outcome:"post-failed",surface:n}},"postHealComment"),R=f(async(t,e={})=>{const{logger:o,workspaceRoot:r}=t,i=r??process.cwd(),n=await w(i,t.options.run);if(n.outcome==="no-failed-task"){u.info("No failed tasks found in the latest run summary. Nothing to heal.");return}if(n.outcome==="missing-metadata"){u.error(`Failed task ${n.failedTask.taskId} is missing project/target metadata in the run summary; cannot validate a fix.`),process.exitCode=1;return}if(n.outcome==="no-failure-context"){u.error(`No failure log or run summary found for ${n.failedTask.taskId}.`),process.exitCode=1;return}const a={failedTask:n.failedTask,failureContext:n.failureContext};u.info(`Healing ${a.failedTask.taskId} (run ${a.failedTask.runId??"unknown"})`),a.failureContext.terminalOutputCaptured||u.warn(`No captured terminal output for ${a.failedTask.taskId}; the AI proposal will be weaker without it.`);const s=await N(t,a);if(s.outcome==="no-proposal"){u.error("AI fix proposal failed or no provider available."),process.exitCode=1;return}if(s.outcome==="cannot-fix"){u.warn(`AI declined to fix: ${s.detail??"(no reason)"}`);return}if(s.outcome==="empty-patches"){u.warn("AI returned an empty patch set.");return}const d=s.proposal;if(o.info(he(d,i,a.failureContext.cwd)),s.outcome==="dry-run"){u.info("Dry run: skipping apply, validation, and PR comment.");return}const p=s.applyResults??[],m=C(p);if(u.info(`Applied ${String(m.applied)}/${String(p.length)} patches.`),s.outcome==="no-patches-applied"){u.error("No patches could be applied (all failed validation)."),process.exitCode=1;return}u.info(`Re-running ${a.failedTask.taskId} to validate the fix...`);const c=await E(t,a,{validate:e.validate});if(c.exitCode!==0){u.error(`Validation failed (exit ${String(c.exitCode)}). Patch is not posted.`),c.stderr.trim().length>0&&(o.info(F("--- validation stderr (tail) ---")),o.info(c.stderr.split(`
|
|
13
|
+
`).slice(-20).join(`
|
|
14
|
+
`))),process.exitCode=1;return}u.success("Validation passed.");const l=await S(i,d,a.failureContext,{detectCi:e.detectCi,postComment:e.postComment});if(l.outcome==="no-ci"){u.notice("Not running in a recognised CI provider; skipping PR comment. Patch was applied + validated locally.");return}if(l.outcome==="posted"){const P=l.surface==="annotation"?`build #${String(l.ciContext.buildNumber??"?")}`:`${l.surface} #${String(l.ciContext.prNumber)}`;u.success(`Posted fix proposal to ${P} via ${l.method??"unknown"}.`);return}if(l.outcome==="no-pr"){u.notice(`No ${l.surface} number detected (push-event run); skipping comment.`);return}const B=l.surface==="annotation"?"annotation":`${l.surface} comment`;u.error(`Failed to post ${B}: ${l.error??"unknown error"}`)},"heal"),be=f(async t=>{await R(t)},"aiHeal"),we=Object.defineProperty({__proto__:null,aiHeal:be,findHealCandidate:w,pickFenceForTesting:y,postHealComment:S,proposeAndApply:N,renderCommentBodyForTesting:x,renderProposalDiffForTesting:$,runHealForTesting:R,validateAppliedFix:E},Symbol.toStringTag,{value:"Module"});export{E as I,w as P,N as R,te as d,we as h,ue as p};
|