@usepipr/runtime 0.1.3 → 0.2.0
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/{commands-BC2fTZsi.mjs → commands-BajlHf13.mjs} +1244 -1210
- package/dist/commands-BajlHf13.mjs.map +1 -0
- package/dist/{commands-C7OX2Jnd.d.mts → commands-Zzy2vNDo.d.mts} +49 -127
- package/dist/commands-Zzy2vNDo.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/internal/testing.d.mts +2 -0
- package/dist/internal/testing.mjs +2 -0
- package/dist/pi/runtime-tools-extension.d.mts.map +1 -1
- package/dist/pi/runtime-tools-extension.mjs.map +1 -1
- package/package.json +7 -12
- package/dist/commands-BC2fTZsi.mjs.map +0 -1
- package/dist/commands-C7OX2Jnd.d.mts.map +0 -1
- package/dist/contract-BsL98-bI.mjs +0 -74
- package/dist/contract-BsL98-bI.mjs.map +0 -1
- package/dist/contract-D-zqnrkd.d.mts +0 -10
- package/dist/contract-D-zqnrkd.d.mts.map +0 -1
- package/dist/e2e/action-fixture.d.mts +0 -2
- package/dist/e2e/action-fixture.mjs +0 -2
- package/dist/e2e/pi-contract.d.mts +0 -2
- package/dist/e2e/pi-contract.mjs +0 -2
|
@@ -1,134 +1,55 @@
|
|
|
1
|
-
import { i as piReadOnlyToolNames, r as piProviderProfileSchema, t as parsePiProviderInvocation } from "./contract-BsL98-bI.mjs";
|
|
2
1
|
import { c as resolveReadAtRefRequest, l as unavailableReadAtRefResult, n as boundedLineSlice, r as parseManifestPath, u as createDiffRangeIndex } from "./runtime-tools-core-CW1xenzy.mjs";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
import { chmod, cp, lstat, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { z } from "zod";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { parseReviewResult, reviewFindingSchema, reviewResultSchema, reviewSchemaExample } from "@usepipr/sdk";
|
|
7
|
+
import { buildPiprPlan, commandPatternParts, embeddedSdkDeclaration, isBuiltinReadOnlyTool, isCommandCaptureToken, isCommandRestCaptureToken, isOptionalCommandPatternPart, isPiprConfigFactory, readSdkDeclarationSourceWithChunk, renderPromptValue, reviewOutputSchemaId, tokenizeCommandPattern, unsupportedCommandRestCaptureError } from "@usepipr/sdk/internal";
|
|
9
8
|
import os from "node:os";
|
|
10
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
-
import { Buffer as Buffer$1 } from "node:buffer";
|
|
12
10
|
import { compact, isPlainObject, uniq, uniqBy } from "lodash-es";
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
14
|
-
import pino from "pino";
|
|
15
11
|
import picomatch from "picomatch";
|
|
12
|
+
import { Buffer as Buffer$1 } from "node:buffer";
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
16
14
|
import { Octokit } from "@octokit/rest";
|
|
17
15
|
import { existsSync, mkdirSync } from "node:fs";
|
|
18
|
-
//#region src/
|
|
19
|
-
const
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (isOptionalPart(part)) {
|
|
38
|
-
const nextIndex = parseOptionalPatternPart(part.slice(1, -1), lineTokens, index, captures);
|
|
39
|
-
if (nextIndex !== void 0) index = nextIndex;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (isRestCaptureToken(part)) {
|
|
43
|
-
const value = lineTokens.slice(index).join(" ");
|
|
44
|
-
if (!value) return {
|
|
45
|
-
ok: false,
|
|
46
|
-
error: `Expected '${part}'`
|
|
47
|
-
};
|
|
48
|
-
captures[part.slice(1, -4)] = value;
|
|
49
|
-
index = lineTokens.length;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const nextIndex = parsePatternToken(part, lineTokens, index, captures);
|
|
53
|
-
if (nextIndex === void 0) return {
|
|
54
|
-
ok: false,
|
|
55
|
-
error: `Expected '${part}'`
|
|
56
|
-
};
|
|
57
|
-
index = nextIndex;
|
|
58
|
-
}
|
|
59
|
-
if (index !== lineTokens.length) return {
|
|
60
|
-
ok: false,
|
|
61
|
-
error: `Unexpected argument '${lineTokens[index]}'`
|
|
62
|
-
};
|
|
63
|
-
return {
|
|
64
|
-
ok: true,
|
|
65
|
-
value: captures
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function commandPatternPrefixMatches(pattern, line) {
|
|
69
|
-
const patternTokens = patternPartsFor(pattern);
|
|
70
|
-
if (unsupportedRestCaptureError(patternTokens)) return false;
|
|
71
|
-
const lineTokens = tokenize(line);
|
|
72
|
-
let index = 0;
|
|
73
|
-
for (const token of patternTokens) {
|
|
74
|
-
if (isCaptureToken(token) || isOptionalPart(token)) return true;
|
|
75
|
-
if (lineTokens[index] !== token) return false;
|
|
76
|
-
index += 1;
|
|
77
|
-
}
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
function parseOptionalPatternPart(pattern, lineTokens, startIndex, captures) {
|
|
81
|
-
const patternTokens = tokenize(pattern);
|
|
82
|
-
if (patternTokens.length === 0 || lineTokens[startIndex] !== patternTokens[0]) return;
|
|
83
|
-
const snapshot = { ...captures };
|
|
84
|
-
let index = startIndex;
|
|
85
|
-
for (const token of patternTokens) {
|
|
86
|
-
const nextIndex = parsePatternToken(token, lineTokens, index, captures);
|
|
87
|
-
if (nextIndex === void 0) {
|
|
88
|
-
for (const key of Object.keys(captures)) delete captures[key];
|
|
89
|
-
Object.assign(captures, snapshot);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
index = nextIndex;
|
|
93
|
-
}
|
|
94
|
-
return index;
|
|
95
|
-
}
|
|
96
|
-
function parsePatternToken(patternToken, lineTokens, index, captures) {
|
|
97
|
-
if (isCaptureToken(patternToken)) {
|
|
98
|
-
const value = lineTokens[index];
|
|
99
|
-
if (!value) return;
|
|
100
|
-
captures[patternToken.slice(1, -1)] = value;
|
|
101
|
-
return index + 1;
|
|
102
|
-
}
|
|
103
|
-
return lineTokens[index] === patternToken ? index + 1 : void 0;
|
|
16
|
+
//#region src/config/config-deps.ts
|
|
17
|
+
const runtimeProvidedPackages = new Set(["@usepipr/sdk", "@types/bun"]);
|
|
18
|
+
async function installConfigDependencies(configDir, options = {}) {
|
|
19
|
+
const packageJsonPath = path.join(configDir, "package.json");
|
|
20
|
+
if (!await fileExists$1(packageJsonPath)) return;
|
|
21
|
+
if (shouldSkipConfigInstall(JSON.parse(await Bun.file(packageJsonPath).text()))) return;
|
|
22
|
+
const bunLockPath = path.join(configDir, "bun.lock");
|
|
23
|
+
if (options.frozen && !await fileExists$1(bunLockPath)) throw new Error(`${configDir}: bun.lock is required when .pipr/package.json declares dependencies. Run \`bun install\` in .pipr/ and commit bun.lock.`);
|
|
24
|
+
await assertBunAvailable();
|
|
25
|
+
const args = ["install", "--ignore-scripts"];
|
|
26
|
+
if (options.frozen) args.push("--frozen-lockfile");
|
|
27
|
+
const proc = Bun.spawn(["bun", ...args], {
|
|
28
|
+
cwd: configDir,
|
|
29
|
+
env: process.env,
|
|
30
|
+
stdout: "pipe",
|
|
31
|
+
stderr: "pipe"
|
|
32
|
+
});
|
|
33
|
+
const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
|
|
34
|
+
if (exitCode !== 0) throw new Error(`${configDir}: bun install failed (exit ${exitCode}).` + (stderr.trim().length > 0 ? `\n${stderr.trim()}` : ""));
|
|
104
35
|
}
|
|
105
|
-
function
|
|
106
|
-
|
|
36
|
+
function shouldSkipConfigInstall(manifest) {
|
|
37
|
+
const packages = [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})];
|
|
38
|
+
return packages.length === 0 || packages.every((name) => runtimeProvidedPackages.has(name));
|
|
107
39
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
40
|
+
async function assertBunAvailable() {
|
|
41
|
+
try {
|
|
42
|
+
if (await Bun.spawn(["bun", "--version"], {
|
|
43
|
+
env: process.env,
|
|
44
|
+
stdout: "pipe",
|
|
45
|
+
stderr: "pipe"
|
|
46
|
+
}).exited !== 0) throw new Error("bun is required on PATH to install .pipr/package.json dependencies. Install Bun from https://bun.sh");
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error("bun is required on PATH to install .pipr/package.json dependencies. Install Bun from https://bun.sh");
|
|
116
49
|
}
|
|
117
50
|
}
|
|
118
|
-
function
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
function isOptionalPart(value) {
|
|
122
|
-
return value.startsWith("[") && value.endsWith("]");
|
|
123
|
-
}
|
|
124
|
-
function isCaptureToken(value) {
|
|
125
|
-
return /^<[a-z0-9-]+(\.\.\.)?>$/.test(value);
|
|
126
|
-
}
|
|
127
|
-
function isRestCaptureToken(value) {
|
|
128
|
-
return /^<[a-z0-9-]+\.\.\.>$/.test(value);
|
|
129
|
-
}
|
|
130
|
-
function tokenize(value) {
|
|
131
|
-
return value.trim().split(/\s+/).filter(Boolean);
|
|
51
|
+
async function fileExists$1(filePath) {
|
|
52
|
+
return await Bun.file(filePath).exists();
|
|
132
53
|
}
|
|
133
54
|
//#endregion
|
|
134
55
|
//#region src/config/paths.ts
|
|
@@ -153,46 +74,86 @@ function toGitPath(filePath) {
|
|
|
153
74
|
return filePath === "." ? "." : filePath.split(path.sep).join("/");
|
|
154
75
|
}
|
|
155
76
|
//#endregion
|
|
156
|
-
//#region src/
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
77
|
+
//#region src/pi/contract.ts
|
|
78
|
+
const piThinkingLevels = [
|
|
79
|
+
"off",
|
|
80
|
+
"minimal",
|
|
81
|
+
"low",
|
|
82
|
+
"medium",
|
|
83
|
+
"high",
|
|
84
|
+
"xhigh"
|
|
85
|
+
];
|
|
86
|
+
const piBuiltinToolNames = [
|
|
87
|
+
"read",
|
|
88
|
+
"bash",
|
|
89
|
+
"edit",
|
|
90
|
+
"write",
|
|
91
|
+
"grep",
|
|
92
|
+
"find",
|
|
93
|
+
"ls"
|
|
94
|
+
];
|
|
95
|
+
const piReadOnlyToolNames = [
|
|
96
|
+
"read",
|
|
97
|
+
"grep",
|
|
98
|
+
"find",
|
|
99
|
+
"ls"
|
|
100
|
+
];
|
|
101
|
+
const piRequiredCliFlags = [
|
|
102
|
+
"--provider",
|
|
103
|
+
"--model",
|
|
104
|
+
"--system-prompt",
|
|
105
|
+
"--mode",
|
|
106
|
+
"--print",
|
|
107
|
+
"--no-session",
|
|
108
|
+
"--session-dir",
|
|
109
|
+
"--tools",
|
|
110
|
+
"--extension",
|
|
111
|
+
"--no-context-files",
|
|
112
|
+
"--no-approve",
|
|
113
|
+
"--no-extensions",
|
|
114
|
+
"--no-skills",
|
|
115
|
+
"--no-prompt-templates",
|
|
116
|
+
"--no-themes",
|
|
117
|
+
"--thinking"
|
|
118
|
+
];
|
|
119
|
+
const nonEmptyStringSchema$1 = z.string().min(1);
|
|
120
|
+
const piProviderIdSchema = z.string().regex(/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/);
|
|
121
|
+
const piApiKeyEnvNameSchema = z.string().regex(/^[A-Z_][A-Z0-9_]*$/);
|
|
122
|
+
const piThinkingLevelSchema = z.enum(piThinkingLevels);
|
|
123
|
+
const piReadOnlyToolNamesSchema = z.tuple([
|
|
124
|
+
z.literal("read"),
|
|
125
|
+
z.literal("grep"),
|
|
126
|
+
z.literal("find"),
|
|
127
|
+
z.literal("ls")
|
|
128
|
+
]);
|
|
129
|
+
const piProviderProfileSchema = z.strictObject({
|
|
130
|
+
id: piProviderIdSchema,
|
|
131
|
+
provider: nonEmptyStringSchema$1,
|
|
132
|
+
model: nonEmptyStringSchema$1,
|
|
133
|
+
apiKeyEnv: piApiKeyEnvNameSchema,
|
|
134
|
+
thinking: piThinkingLevelSchema.optional()
|
|
135
|
+
});
|
|
136
|
+
const piProviderInvocationSchema = z.strictObject({
|
|
137
|
+
provider: nonEmptyStringSchema$1,
|
|
138
|
+
model: nonEmptyStringSchema$1,
|
|
139
|
+
apiKeyEnv: piApiKeyEnvNameSchema,
|
|
140
|
+
thinking: piThinkingLevelSchema,
|
|
141
|
+
tools: piReadOnlyToolNamesSchema
|
|
142
|
+
});
|
|
143
|
+
function parsePiProviderInvocation(value) {
|
|
144
|
+
return piProviderInvocationSchema.parse(value);
|
|
162
145
|
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/review/contract.ts
|
|
148
|
+
const reviewResultSchemaId = reviewOutputSchemaId;
|
|
149
|
+
z.toJSONSchema(reviewResultSchema);
|
|
163
150
|
function reviewSchemaExample$1() {
|
|
164
|
-
return
|
|
151
|
+
return parseReviewResult(reviewSchemaExample());
|
|
165
152
|
}
|
|
166
153
|
//#endregion
|
|
167
154
|
//#region src/types.ts
|
|
168
155
|
const nonEmptyStringSchema = z.string().min(1);
|
|
169
156
|
const providerConfigSchema = piProviderProfileSchema;
|
|
170
|
-
const pathGlobPatternSchema = z.string().min(1).superRefine((pattern, context) => {
|
|
171
|
-
if (pattern.includes("\0")) context.addIssue({
|
|
172
|
-
code: "custom",
|
|
173
|
-
message: "must not contain NUL bytes"
|
|
174
|
-
});
|
|
175
|
-
if (pattern.includes("\\")) context.addIssue({
|
|
176
|
-
code: "custom",
|
|
177
|
-
message: "must use POSIX '/' separators"
|
|
178
|
-
});
|
|
179
|
-
if (pattern.startsWith("/") || /^[A-Za-z]:\//.test(pattern)) context.addIssue({
|
|
180
|
-
code: "custom",
|
|
181
|
-
message: "must be repo-relative"
|
|
182
|
-
});
|
|
183
|
-
if (pattern.startsWith("!")) context.addIssue({
|
|
184
|
-
code: "custom",
|
|
185
|
-
message: "must use paths.exclude instead of negation"
|
|
186
|
-
});
|
|
187
|
-
if (pattern.split("/").includes("..")) context.addIssue({
|
|
188
|
-
code: "custom",
|
|
189
|
-
message: "must not contain '..' segments"
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
z.strictObject({
|
|
193
|
-
include: z.array(pathGlobPatternSchema).min(1).optional(),
|
|
194
|
-
exclude: z.array(pathGlobPatternSchema).min(1).optional()
|
|
195
|
-
});
|
|
196
157
|
const diffManifestLimitsConfigSchema = z.strictObject({
|
|
197
158
|
fullMaxBytes: z.number().int().positive().optional(),
|
|
198
159
|
fullMaxEstimatedTokens: z.number().int().positive().optional(),
|
|
@@ -330,7 +291,7 @@ const droppedFindingSchema = z.strictObject({
|
|
|
330
291
|
reason: nonEmptyStringSchema
|
|
331
292
|
});
|
|
332
293
|
const validatedReviewSchema = z.strictObject({
|
|
333
|
-
review:
|
|
294
|
+
review: reviewResultSchema,
|
|
334
295
|
validFindings: z.array(reviewFindingSchema),
|
|
335
296
|
droppedFindings: z.array(droppedFindingSchema)
|
|
336
297
|
});
|
|
@@ -368,64 +329,64 @@ function embeddedSdkAssets() {
|
|
|
368
329
|
};
|
|
369
330
|
}
|
|
370
331
|
//#endregion
|
|
371
|
-
//#region src/config/
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
"moduleResolution": "Bundler"
|
|
379
|
-
},
|
|
380
|
-
"include": ["./**/*.ts"]
|
|
332
|
+
//#region src/config/sdk-module.ts
|
|
333
|
+
function resolvedSdkModulePath() {
|
|
334
|
+
try {
|
|
335
|
+
return fileURLToPath(import.meta.resolve("@usepipr/sdk"));
|
|
336
|
+
} catch {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
381
339
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const files = [];
|
|
385
|
-
if (options.tsconfig !== false) files.push({
|
|
386
|
-
relativePath: path.join(relativeConfigDir, "tsconfig.json"),
|
|
387
|
-
contents: starterTsconfig
|
|
388
|
-
});
|
|
389
|
-
files.push({
|
|
390
|
-
relativePath: path.join(relativeConfigDir, "types", "pipr-sdk.d.ts"),
|
|
391
|
-
contents: await sdkDeclaration()
|
|
392
|
-
});
|
|
393
|
-
return files;
|
|
340
|
+
function resolvedSdkPackageRoot() {
|
|
341
|
+
return sdkPackageRootFromResolvedModule(resolvedSdkModulePath());
|
|
394
342
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
343
|
+
function sdkPackageRootFromResolvedModule(modulePath) {
|
|
344
|
+
if (!modulePath) return;
|
|
345
|
+
const moduleDir = path.dirname(modulePath);
|
|
346
|
+
return path.basename(moduleDir) === "src" || path.basename(moduleDir) === "dist" ? path.dirname(moduleDir) : moduleDir;
|
|
347
|
+
}
|
|
348
|
+
function sdkModuleStubSource(modulePath, embeddedModule) {
|
|
349
|
+
if (modulePath) return `export * from ${JSON.stringify(pathToFileURL(modulePath).href)};\n`;
|
|
350
|
+
if (embeddedModule) return embeddedModule;
|
|
351
|
+
throw new Error("Unable to locate @usepipr/sdk runtime module");
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/config/sdk-stub.ts
|
|
355
|
+
const sdkDeclarationModules = [{
|
|
356
|
+
moduleName: "@usepipr/sdk",
|
|
357
|
+
fileName: "index.d.mts"
|
|
358
|
+
}];
|
|
359
|
+
async function installTypedSdkStub(configDir) {
|
|
360
|
+
const sdkRoot = path.join(configDir, "node_modules", "@usepipr", "sdk");
|
|
361
|
+
await rm(sdkRoot, {
|
|
362
|
+
recursive: true,
|
|
363
|
+
force: true
|
|
364
|
+
});
|
|
365
|
+
await mkdir(sdkRoot, { recursive: true });
|
|
366
|
+
await Bun.write(path.join(sdkRoot, "package.json"), JSON.stringify({
|
|
367
|
+
type: "module",
|
|
368
|
+
types: "./index.d.ts",
|
|
369
|
+
exports: { ".": {
|
|
370
|
+
types: "./index.d.ts",
|
|
371
|
+
default: "./index.mjs"
|
|
372
|
+
} }
|
|
373
|
+
}));
|
|
374
|
+
await Bun.write(path.join(sdkRoot, "index.mjs"), sdkModuleStubSource(resolvedSdkModulePath(), embeddedSdkAssets().module));
|
|
375
|
+
await Bun.write(path.join(sdkRoot, "index.d.ts"), await sdkStubDeclaration());
|
|
401
376
|
}
|
|
402
|
-
async function
|
|
377
|
+
async function sdkStubDeclaration() {
|
|
403
378
|
const embedded = embeddedSdkAssets().declaration;
|
|
404
379
|
if (embedded?.includes("declare module \"@usepipr/sdk\"")) {
|
|
405
380
|
assertStandaloneSdkDeclaration(embedded);
|
|
406
|
-
return embedded
|
|
381
|
+
return embedded.endsWith("\n") ? embedded : `${embedded}\n`;
|
|
407
382
|
}
|
|
408
383
|
const declaration = embeddedSdkDeclaration(await rawSdkDeclarations());
|
|
409
384
|
assertStandaloneSdkDeclaration(declaration);
|
|
410
|
-
return declaration
|
|
385
|
+
return declaration.endsWith("\n") ? declaration : `${declaration}\n`;
|
|
411
386
|
}
|
|
412
387
|
function assertStandaloneSdkDeclaration(declaration) {
|
|
413
388
|
if (declaration.includes("from \"zod\"") || declaration.includes("z.ZodType")) throw new Error("generated SDK declaration must be standalone and must not import zod");
|
|
414
389
|
}
|
|
415
|
-
const sdkDeclarationModules = [
|
|
416
|
-
{
|
|
417
|
-
moduleName: "@usepipr/sdk",
|
|
418
|
-
fileName: "index.d.mts"
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
moduleName: "@usepipr/sdk/review",
|
|
422
|
-
fileName: "review.d.mts"
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
moduleName: "@usepipr/sdk/tools",
|
|
426
|
-
fileName: "tools.d.mts"
|
|
427
|
-
}
|
|
428
|
-
];
|
|
429
390
|
async function rawSdkDeclarations() {
|
|
430
391
|
const declarations = await Promise.all(sdkDeclarationModules.map(async (module) => {
|
|
431
392
|
const declarationPath = await sdkDeclarationPath(module.fileName);
|
|
@@ -440,13 +401,27 @@ async function rawSdkDeclarations() {
|
|
|
440
401
|
moduleName: sdkDeclarationModules[0].moduleName,
|
|
441
402
|
source: embedded
|
|
442
403
|
}];
|
|
443
|
-
throw new Error("Unable to locate @usepipr/sdk declaration file. Build @usepipr/sdk before
|
|
404
|
+
throw new Error("Unable to locate @usepipr/sdk declaration file. Build @usepipr/sdk before loading config.");
|
|
444
405
|
}
|
|
445
406
|
async function sdkDeclarationPath(fileName) {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
407
|
+
const sdkRoot = resolvedSdkPackageRoot();
|
|
408
|
+
if (!sdkRoot) return;
|
|
409
|
+
const candidate = path.join(sdkRoot, "dist", fileName);
|
|
410
|
+
return await Bun.file(candidate).exists() ? candidate : void 0;
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/config/starter-tsconfig.ts
|
|
414
|
+
const starterTsconfig = `{
|
|
415
|
+
"compilerOptions": {
|
|
416
|
+
"strict": true,
|
|
417
|
+
"noEmit": true,
|
|
418
|
+
"target": "ES2022",
|
|
419
|
+
"module": "ESNext",
|
|
420
|
+
"moduleResolution": "Bundler"
|
|
421
|
+
},
|
|
422
|
+
"include": ["./**/*.ts"]
|
|
449
423
|
}
|
|
424
|
+
`;
|
|
450
425
|
//#endregion
|
|
451
426
|
//#region src/config/ts-loader.ts
|
|
452
427
|
async function loadTypescriptConfig(options) {
|
|
@@ -457,13 +432,8 @@ async function loadTypescriptConfig(options) {
|
|
|
457
432
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pipr-config-"));
|
|
458
433
|
try {
|
|
459
434
|
const tempConfigDir = path.join(tempRoot, relativeConfigDir);
|
|
460
|
-
await
|
|
461
|
-
|
|
462
|
-
errorOnExist: false,
|
|
463
|
-
force: true,
|
|
464
|
-
filter: (source) => !isIgnoredConfigCopyPath(source, projectDir)
|
|
465
|
-
});
|
|
466
|
-
await installSdkStub(tempConfigDir);
|
|
435
|
+
await copyConfigDirectory(projectDir, tempConfigDir);
|
|
436
|
+
await prepareConfigDirectory(tempConfigDir, { frozen: true });
|
|
467
437
|
const factory = (await import(`${pathToFileURL(path.join(tempConfigDir, "config.ts")).href}?pipr=${Date.now()}`)).default;
|
|
468
438
|
if (!isPiprConfigFactory(factory)) throw new Error(`${sourceConfigPath}: default export must be created by definePipr()`);
|
|
469
439
|
return {
|
|
@@ -478,27 +448,15 @@ async function loadTypescriptConfig(options) {
|
|
|
478
448
|
});
|
|
479
449
|
}
|
|
480
450
|
}
|
|
481
|
-
async function
|
|
482
|
-
|
|
483
|
-
await
|
|
484
|
-
await Bun.write(path.join(sdkRoot, "package.json"), JSON.stringify({
|
|
485
|
-
type: "module",
|
|
486
|
-
exports: {
|
|
487
|
-
".": "./index.mjs",
|
|
488
|
-
"./review": "./review.mjs",
|
|
489
|
-
"./tools": "./tools.mjs"
|
|
490
|
-
}
|
|
491
|
-
}));
|
|
492
|
-
await Bun.write(path.join(sdkRoot, "index.mjs"), await sdkStubSource());
|
|
493
|
-
await Bun.write(path.join(sdkRoot, "review.mjs"), "export * from \"./index.mjs\";\n");
|
|
494
|
-
await Bun.write(path.join(sdkRoot, "tools.mjs"), "export * from \"./index.mjs\";\n");
|
|
451
|
+
async function prepareConfigDirectory(configDir, options = {}) {
|
|
452
|
+
await installConfigDependencies(configDir, options);
|
|
453
|
+
await installTypedSdkStub(configDir);
|
|
495
454
|
}
|
|
496
455
|
async function typecheckTypescriptConfig(rootDir, relativeConfigDir) {
|
|
497
456
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pipr-config-check-"));
|
|
498
457
|
try {
|
|
499
458
|
const tempProjectDir = path.join(tempRoot, "project");
|
|
500
459
|
const tempConfigDir = path.join(tempProjectDir, relativeConfigDir);
|
|
501
|
-
const configTypesPath = path.join(relativeConfigDir, "types");
|
|
502
460
|
await cp(rootDir, tempProjectDir, {
|
|
503
461
|
recursive: true,
|
|
504
462
|
errorOnExist: false,
|
|
@@ -506,11 +464,15 @@ async function typecheckTypescriptConfig(rootDir, relativeConfigDir) {
|
|
|
506
464
|
filter: (source) => {
|
|
507
465
|
const relative = path.relative(rootDir, source);
|
|
508
466
|
const first = relative.split(path.sep)[0] ?? "";
|
|
509
|
-
return !ignoredTypecheckRootEntries.has(first) && relative !== "bun.lock"
|
|
467
|
+
return !ignoredTypecheckRootEntries.has(first) && relative !== "bun.lock";
|
|
510
468
|
}
|
|
511
469
|
});
|
|
470
|
+
await prepareConfigDirectory(tempConfigDir, { frozen: true });
|
|
512
471
|
const tsconfigPath = path.join(tempConfigDir, "tsconfig.json");
|
|
513
|
-
|
|
472
|
+
if (!await fileExists(tsconfigPath)) {
|
|
473
|
+
await mkdir(tempConfigDir, { recursive: true });
|
|
474
|
+
await Bun.write(tsconfigPath, starterTsconfig);
|
|
475
|
+
}
|
|
514
476
|
await typecheckTypescriptConfigWithApi(tempConfigDir, tsconfigPath);
|
|
515
477
|
} finally {
|
|
516
478
|
await rm(tempRoot, {
|
|
@@ -524,28 +486,16 @@ async function typecheckTypescriptConfigWithApi(configDir, tsconfigPath) {
|
|
|
524
486
|
const config = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
525
487
|
if (config.error) throw new Error(formatTypeScriptDiagnostics(ts, [config.error], configDir));
|
|
526
488
|
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, configDir);
|
|
527
|
-
const typeRoot = path.join(configDir, "types");
|
|
528
489
|
const bundledTypeRoots = [];
|
|
529
|
-
try {
|
|
490
|
+
if (!await fileExists(path.join(configDir, "node_modules", "@types", "bun", "package.json"))) try {
|
|
530
491
|
const require = createRequire(import.meta.url);
|
|
531
492
|
bundledTypeRoots.push(path.dirname(path.dirname(require.resolve("@types/bun/package.json"))));
|
|
532
493
|
} catch {}
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
return relative.startsWith("..") || path.isAbsolute(relative);
|
|
536
|
-
})];
|
|
537
|
-
const program = ts.createProgram(fileNames, {
|
|
494
|
+
const configPath = path.join(configDir, "config.ts");
|
|
495
|
+
const program = ts.createProgram([configPath, ...parsed.fileNames], {
|
|
538
496
|
...parsed.options,
|
|
539
|
-
typeRoots: [...new Set([
|
|
540
|
-
|
|
541
|
-
...bundledTypeRoots,
|
|
542
|
-
...parsed.options.typeRoots ?? []
|
|
543
|
-
])],
|
|
544
|
-
types: [...new Set([
|
|
545
|
-
...parsed.options.types ?? [],
|
|
546
|
-
"pipr-sdk",
|
|
547
|
-
...bundledTypeRoots.length ? ["bun"] : []
|
|
548
|
-
])]
|
|
497
|
+
typeRoots: [...new Set([...parsed.options.typeRoots ?? [], ...bundledTypeRoots])],
|
|
498
|
+
types: [...new Set([...parsed.options.types ?? [], ...bundledTypeRoots.length ? ["bun"] : []])]
|
|
549
499
|
});
|
|
550
500
|
const diagnostics = [...parsed.errors, ...ts.getPreEmitDiagnostics(program)];
|
|
551
501
|
if (diagnostics.length > 0) throw new Error(`TypeScript config check failed for ${path.join(configDir, "config.ts")}:\n` + formatTypeScriptDiagnostics(ts, diagnostics, configDir));
|
|
@@ -557,26 +507,17 @@ function formatTypeScriptDiagnostics(ts, diagnostics, configDir) {
|
|
|
557
507
|
getNewLine: () => "\n"
|
|
558
508
|
});
|
|
559
509
|
}
|
|
560
|
-
async function
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
async function sdkSourcePath() {
|
|
568
|
-
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
569
|
-
const candidates = [
|
|
570
|
-
path.resolve(moduleDir, "../../../sdk/src/index.ts"),
|
|
571
|
-
path.resolve(moduleDir, "../../sdk/src/index.ts"),
|
|
572
|
-
path.resolve(moduleDir, "../../../sdk/dist/index.mjs"),
|
|
573
|
-
path.resolve(moduleDir, "../../sdk/dist/index.mjs")
|
|
574
|
-
];
|
|
575
|
-
for (const candidate of candidates) if (await fileExists(candidate)) return candidate;
|
|
510
|
+
async function copyConfigDirectory(sourceDir, targetDir) {
|
|
511
|
+
await cp(sourceDir, targetDir, {
|
|
512
|
+
recursive: true,
|
|
513
|
+
errorOnExist: false,
|
|
514
|
+
force: true,
|
|
515
|
+
filter: (source) => !isIgnoredConfigCopyPath(source, sourceDir)
|
|
516
|
+
});
|
|
576
517
|
}
|
|
577
518
|
function isIgnoredConfigCopyPath(source, configDir) {
|
|
578
519
|
const relative = path.relative(configDir, source);
|
|
579
|
-
return relative === "node_modules" || relative.startsWith(`node_modules${path.sep}`)
|
|
520
|
+
return relative === "node_modules" || relative.startsWith(`node_modules${path.sep}`);
|
|
580
521
|
}
|
|
581
522
|
const ignoredTypecheckRootEntries = new Set([
|
|
582
523
|
".fallow",
|
|
@@ -747,6 +688,8 @@ export default definePipr((pipr) => {
|
|
|
747
688
|
options: { thinking: "medium" },
|
|
748
689
|
});
|
|
749
690
|
|
|
691
|
+
pipr.config({ publication: { maxInlineComments: 8 } });
|
|
692
|
+
|
|
750
693
|
const reviewer = pipr.reviewer({
|
|
751
694
|
name: "bug-hunter",
|
|
752
695
|
model: primary,
|
|
@@ -765,7 +708,6 @@ export default definePipr((pipr) => {
|
|
|
765
708
|
paths: {
|
|
766
709
|
exclude: ["docs/**", "**/*.md"],
|
|
767
710
|
},
|
|
768
|
-
inlineComments: { max: 8 },
|
|
769
711
|
timeout: "7m",
|
|
770
712
|
entrypoints: {
|
|
771
713
|
changeRequest: ["opened", "updated", "reopened", "ready"],
|
|
@@ -913,6 +855,8 @@ export default definePipr((pipr) => {
|
|
|
913
855
|
options: { thinking: "high" },
|
|
914
856
|
});
|
|
915
857
|
|
|
858
|
+
pipr.config({ publication: { maxInlineComments: 5 } });
|
|
859
|
+
|
|
916
860
|
pipr.review({
|
|
917
861
|
id: "review",
|
|
918
862
|
model,
|
|
@@ -921,7 +865,6 @@ export default definePipr((pipr) => {
|
|
|
921
865
|
maintainability, and test coverage.
|
|
922
866
|
Return only actionable findings that target valid diff ranges.
|
|
923
867
|
\`,
|
|
924
|
-
inlineComments: { max: 5 },
|
|
925
868
|
timeout: "5m",
|
|
926
869
|
comment: (result, context) => ({
|
|
927
870
|
main:
|
|
@@ -1547,6 +1490,8 @@ export default definePipr((pipr) => {
|
|
|
1547
1490
|
options: { thinking: "medium" },
|
|
1548
1491
|
});
|
|
1549
1492
|
|
|
1493
|
+
pipr.config({ publication: { maxInlineComments: 0 } });
|
|
1494
|
+
|
|
1550
1495
|
pipr.review({
|
|
1551
1496
|
id: "pr-briefing",
|
|
1552
1497
|
model,
|
|
@@ -1555,7 +1500,6 @@ export default definePipr((pipr) => {
|
|
|
1555
1500
|
classify the PR type, explain review risk, and include a concise file walkthrough.
|
|
1556
1501
|
Return no inline findings unless there is a concrete blocker.
|
|
1557
1502
|
\`,
|
|
1558
|
-
inlineComments: false,
|
|
1559
1503
|
comment: (result, context) => [
|
|
1560
1504
|
"## PR Briefing",
|
|
1561
1505
|
"",
|
|
@@ -1646,18 +1590,6 @@ export default definePipr((pipr) => {
|
|
|
1646
1590
|
options: { thinking: "high" },
|
|
1647
1591
|
});
|
|
1648
1592
|
|
|
1649
|
-
pipr.checks({
|
|
1650
|
-
aggregate: { enabled: true, name: "pipr quality gate" },
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
pipr.limits({
|
|
1654
|
-
timeoutSeconds: 420,
|
|
1655
|
-
diffManifest: {
|
|
1656
|
-
fullMaxEstimatedTokens: 32000,
|
|
1657
|
-
condensedMaxEstimatedTokens: 64000,
|
|
1658
|
-
},
|
|
1659
|
-
});
|
|
1660
|
-
|
|
1661
1593
|
pipr.config({
|
|
1662
1594
|
publication: {
|
|
1663
1595
|
maxInlineComments: 6,
|
|
@@ -1669,6 +1601,16 @@ export default definePipr((pipr) => {
|
|
|
1669
1601
|
userReplies: { enabled: true, allowedActors: "write" },
|
|
1670
1602
|
},
|
|
1671
1603
|
},
|
|
1604
|
+
checks: {
|
|
1605
|
+
aggregate: { enabled: true, name: "pipr quality gate" },
|
|
1606
|
+
},
|
|
1607
|
+
limits: {
|
|
1608
|
+
timeoutSeconds: 420,
|
|
1609
|
+
diffManifest: {
|
|
1610
|
+
fullMaxEstimatedTokens: 32000,
|
|
1611
|
+
condensedMaxEstimatedTokens: 64000,
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1672
1614
|
});
|
|
1673
1615
|
|
|
1674
1616
|
pipr.review({
|
|
@@ -1680,7 +1622,6 @@ export default definePipr((pipr) => {
|
|
|
1680
1622
|
If no blocking issue exists, state that no blocking issue exists.
|
|
1681
1623
|
\`,
|
|
1682
1624
|
check: { enabled: true, name: "quality gate", required: true },
|
|
1683
|
-
inlineComments: { max: 6 },
|
|
1684
1625
|
comment: (result) => ({
|
|
1685
1626
|
main: \`## Quality Gate\\n\\n\${result.summary.body}\`,
|
|
1686
1627
|
inlineFindings: result.inlineFindings,
|
|
@@ -1860,7 +1801,9 @@ function isOfficialInitRecipeId(recipe) {
|
|
|
1860
1801
|
//#endregion
|
|
1861
1802
|
//#region src/config/init.ts
|
|
1862
1803
|
const supportedOfficialInitAdapters = ["github"];
|
|
1863
|
-
const defaultWorkflowActionRef = "somus/pipr@v0.
|
|
1804
|
+
const defaultWorkflowActionRef = "somus/pipr@v0.2.0";
|
|
1805
|
+
const defaultSdkVersion = "0.2.0";
|
|
1806
|
+
const defaultTypesBunVersion = "1.3.14";
|
|
1864
1807
|
function resolveOfficialInitAdapters(adapters) {
|
|
1865
1808
|
if (adapters === void 0) return [...supportedOfficialInitAdapters];
|
|
1866
1809
|
if (adapters.length === 0) return [];
|
|
@@ -1880,25 +1823,38 @@ function unsupportedAdapterError(adapter) {
|
|
|
1880
1823
|
return /* @__PURE__ */ new Error(`Unsupported pipr init adapter '${adapter}'. Supported adapters: ${supportedOfficialInitAdapters.join(", ")}; use 'none' to skip adapter files.`);
|
|
1881
1824
|
}
|
|
1882
1825
|
async function initOfficialMinimalProject(options) {
|
|
1883
|
-
const { configDir, relativeConfigDir } = resolveContainedConfigDir(options);
|
|
1826
|
+
const { configDir, relativeConfigDir, projectDir } = resolveContainedConfigDir(options);
|
|
1884
1827
|
const adapters = resolveOfficialInitAdapters(options.adapters);
|
|
1885
1828
|
const rootDir = path.resolve(options.rootDir);
|
|
1886
|
-
const
|
|
1887
|
-
const targets = (
|
|
1829
|
+
const minimal = options.minimal === true;
|
|
1830
|
+
const targets = (await starterFiles(relativeConfigDir, adapters, options.recipe, minimal)).map((file) => ({
|
|
1888
1831
|
...file,
|
|
1889
1832
|
absolutePath: path.join(rootDir, file.relativePath)
|
|
1890
1833
|
}));
|
|
1891
1834
|
await assertSafeTargetAncestors(targets, rootDir);
|
|
1892
1835
|
const existing = await findExistingTargets(targets);
|
|
1893
|
-
if (existing.length > 0 && !options.force) {
|
|
1894
|
-
if (typeSupport === "only") return {
|
|
1895
|
-
configDir,
|
|
1896
|
-
...await writeTargets(targets, existing, { skipExisting: true })
|
|
1897
|
-
};
|
|
1898
|
-
throw new Error(`Project already contains pipr files: ${existing.join(", ")}. Use --force to replace existing .pipr files.`);
|
|
1899
|
-
}
|
|
1836
|
+
if (existing.length > 0 && !options.force) throw new Error(`Project already contains pipr files: ${existing.join(", ")}. Use --force to replace existing .pipr files.`);
|
|
1900
1837
|
const result = await writeTargets(targets, existing, { skipExisting: false });
|
|
1901
|
-
if (
|
|
1838
|
+
if (!minimal) {
|
|
1839
|
+
await assertBunAvailable();
|
|
1840
|
+
const install = Bun.spawn([
|
|
1841
|
+
"bun",
|
|
1842
|
+
"install",
|
|
1843
|
+
"--ignore-scripts"
|
|
1844
|
+
], {
|
|
1845
|
+
cwd: projectDir,
|
|
1846
|
+
env: process.env,
|
|
1847
|
+
stdout: "pipe",
|
|
1848
|
+
stderr: "pipe"
|
|
1849
|
+
});
|
|
1850
|
+
const [exitCode, stderr] = await Promise.all([install.exited, new Response(install.stderr).text()]);
|
|
1851
|
+
if (exitCode !== 0) throw new Error(`${configDir}: bun install failed (exit ${exitCode}).` + (stderr.trim().length > 0 ? `\n${stderr.trim()}` : ""));
|
|
1852
|
+
if (await Bun.file(path.join(projectDir, "bun.lock")).exists()) {
|
|
1853
|
+
const lockRelative = path.join(relativeConfigDir, "bun.lock");
|
|
1854
|
+
if (!existing.includes(lockRelative) && !result.created.includes(lockRelative)) result.created.push(lockRelative);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
await loadRuntimeProject({
|
|
1902
1858
|
rootDir: options.rootDir,
|
|
1903
1859
|
configDir
|
|
1904
1860
|
});
|
|
@@ -1907,7 +1863,7 @@ async function initOfficialMinimalProject(options) {
|
|
|
1907
1863
|
...result
|
|
1908
1864
|
};
|
|
1909
1865
|
}
|
|
1910
|
-
async function starterFiles(relativeConfigDir, adapters, recipe,
|
|
1866
|
+
async function starterFiles(relativeConfigDir, adapters, recipe, minimal = false) {
|
|
1911
1867
|
const files = [{
|
|
1912
1868
|
relativePath: path.join(relativeConfigDir, "config.ts"),
|
|
1913
1869
|
contents: officialInitRecipeConfigTs(recipe)
|
|
@@ -1915,13 +1871,29 @@ async function starterFiles(relativeConfigDir, adapters, recipe, includeTypeSupp
|
|
|
1915
1871
|
relativePath: path.join(relativeConfigDir, file.relativePath),
|
|
1916
1872
|
contents: file.contents
|
|
1917
1873
|
}))];
|
|
1918
|
-
if (
|
|
1874
|
+
if (!minimal) files.push({
|
|
1875
|
+
relativePath: path.join(relativeConfigDir, "package.json"),
|
|
1876
|
+
contents: starterPackageJson()
|
|
1877
|
+
}, {
|
|
1878
|
+
relativePath: path.join(relativeConfigDir, "tsconfig.json"),
|
|
1879
|
+
contents: starterTsconfig
|
|
1880
|
+
}, {
|
|
1881
|
+
relativePath: path.join(relativeConfigDir, ".gitignore"),
|
|
1882
|
+
contents: "node_modules\n"
|
|
1883
|
+
});
|
|
1919
1884
|
if (adapters.includes("github")) files.push({
|
|
1920
1885
|
relativePath: path.join(".github", "workflows", "pipr.yml"),
|
|
1921
|
-
contents: starterWorkflow(relativeConfigDir.split(path.sep).join("/"), recipe)
|
|
1886
|
+
contents: starterWorkflow(relativeConfigDir.split(path.sep).join("/"), recipe, minimal)
|
|
1922
1887
|
});
|
|
1923
1888
|
return files;
|
|
1924
1889
|
}
|
|
1890
|
+
function starterPackageJson() {
|
|
1891
|
+
return `${JSON.stringify({
|
|
1892
|
+
private: true,
|
|
1893
|
+
dependencies: { "@usepipr/sdk": process.env.PIPR_INTERNAL_INIT_SDK_VERSION ?? defaultSdkVersion },
|
|
1894
|
+
devDependencies: { "@types/bun": defaultTypesBunVersion }
|
|
1895
|
+
}, null, 2)}\n`;
|
|
1896
|
+
}
|
|
1925
1897
|
async function writeTargets(targets, existing, options) {
|
|
1926
1898
|
const created = [];
|
|
1927
1899
|
const overwritten = [];
|
|
@@ -1975,7 +1947,7 @@ async function maybeLstat(filePath) {
|
|
|
1975
1947
|
return;
|
|
1976
1948
|
}
|
|
1977
1949
|
}
|
|
1978
|
-
function starterWorkflow(relativeConfigDir, recipe) {
|
|
1950
|
+
function starterWorkflow(relativeConfigDir, recipe, minimal = false) {
|
|
1979
1951
|
const lines = [
|
|
1980
1952
|
"name: pipr",
|
|
1981
1953
|
"",
|
|
@@ -1998,20 +1970,25 @@ function starterWorkflow(relativeConfigDir, recipe) {
|
|
|
1998
1970
|
" steps:",
|
|
1999
1971
|
" - uses: actions/checkout@v6",
|
|
2000
1972
|
" with:",
|
|
2001
|
-
" fetch-depth: 0"
|
|
2002
|
-
` - uses: ${defaultWorkflowActionRef}`,
|
|
2003
|
-
" env:",
|
|
2004
|
-
` DEEPSEEK_API_KEY: $${[
|
|
2005
|
-
"{{ ",
|
|
2006
|
-
"secrets.DEEPSEEK_API_KEY",
|
|
2007
|
-
" }}"
|
|
2008
|
-
].join("")}`,
|
|
2009
|
-
` GITHUB_TOKEN: $${[
|
|
2010
|
-
"{{ ",
|
|
2011
|
-
"github.token",
|
|
2012
|
-
" }}"
|
|
2013
|
-
].join("")}`
|
|
1973
|
+
" fetch-depth: 0"
|
|
2014
1974
|
];
|
|
1975
|
+
if (!minimal) lines.push(" - uses: actions/cache@v4", " with:", " path: /home/runner/work/_temp/_github_home/.bun/install/cache", ` key: pipr-bun-${[
|
|
1976
|
+
"{{ ",
|
|
1977
|
+
`hashFiles('${relativeConfigDir}/bun.lock')`,
|
|
1978
|
+
" }}"
|
|
1979
|
+
].join("")}`);
|
|
1980
|
+
lines.push(` - uses: ${defaultWorkflowActionRef}`);
|
|
1981
|
+
lines.push(" env:");
|
|
1982
|
+
lines.push(` DEEPSEEK_API_KEY: $${[
|
|
1983
|
+
"{{ ",
|
|
1984
|
+
"secrets.DEEPSEEK_API_KEY",
|
|
1985
|
+
" }}"
|
|
1986
|
+
].join("")}`);
|
|
1987
|
+
lines.push(` GITHUB_TOKEN: $${[
|
|
1988
|
+
"{{ ",
|
|
1989
|
+
"github.token",
|
|
1990
|
+
" }}"
|
|
1991
|
+
].join("")}`);
|
|
2015
1992
|
for (const secret of officialInitRecipeWorkflowEnvSecrets(recipe)) lines.push(` ${secret.env}: $${[
|
|
2016
1993
|
"{{ ",
|
|
2017
1994
|
`secrets.${secret.secret}`,
|
|
@@ -2386,44 +2363,452 @@ function createLocalChangeRequestEvent(options) {
|
|
|
2386
2363
|
});
|
|
2387
2364
|
}
|
|
2388
2365
|
//#endregion
|
|
2389
|
-
//#region src/
|
|
2390
|
-
const
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
const baseRoot = path.join(toolRoot, "base");
|
|
2394
|
-
await mkdir(baseRoot, { recursive: true });
|
|
2395
|
-
const baseRanges = await materializeBaseRangeSnapshots({
|
|
2396
|
-
baseRoot,
|
|
2397
|
-
manifest: options.request.manifest,
|
|
2398
|
-
sourceWorkspace: options.sourceWorkspace,
|
|
2399
|
-
maxBytes: options.request.toolResponseMaxBytes
|
|
2400
|
-
});
|
|
2401
|
-
const data = {
|
|
2402
|
-
manifest: options.request.manifest,
|
|
2403
|
-
toolResponseMaxBytes: options.request.toolResponseMaxBytes,
|
|
2404
|
-
baseRanges
|
|
2405
|
-
};
|
|
2406
|
-
const dataPath = path.join(toolRoot, "data.json");
|
|
2407
|
-
await Bun.write(dataPath, JSON.stringify(data));
|
|
2408
|
-
return {
|
|
2409
|
-
extensionPath: await piRuntimeToolsExtensionPath(),
|
|
2410
|
-
dataPath,
|
|
2411
|
-
toolNames: piRuntimeReadToolNames
|
|
2412
|
-
};
|
|
2366
|
+
//#region src/commands/grammar.ts
|
|
2367
|
+
const piprCommandPrefix = "@pipr";
|
|
2368
|
+
function firstNonEmptyLine(value) {
|
|
2369
|
+
return value.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
|
|
2413
2370
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
const candidates = [
|
|
2417
|
-
path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
|
|
2418
|
-
path.join(moduleDir, "runtime-tools-extension.mjs"),
|
|
2419
|
-
path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
|
|
2420
|
-
path.join(moduleDir, "runtime-tools-extension.ts")
|
|
2421
|
-
];
|
|
2422
|
-
for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
|
|
2423
|
-
throw new Error("Unable to locate pipr runtime tools extension");
|
|
2371
|
+
function isPiprCommandLine(line) {
|
|
2372
|
+
return line === piprCommandPrefix || line.startsWith(`${piprCommandPrefix} `);
|
|
2424
2373
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2374
|
+
function parseCommandPattern(pattern, line) {
|
|
2375
|
+
const patternParts = commandPatternParts(pattern);
|
|
2376
|
+
const validationError = unsupportedCommandRestCaptureError(pattern);
|
|
2377
|
+
if (validationError) return {
|
|
2378
|
+
ok: false,
|
|
2379
|
+
error: validationError
|
|
2380
|
+
};
|
|
2381
|
+
const lineTokens = tokenizeCommandPattern(line);
|
|
2382
|
+
const captures = {};
|
|
2383
|
+
let index = 0;
|
|
2384
|
+
for (const part of patternParts) {
|
|
2385
|
+
if (isOptionalCommandPatternPart(part)) {
|
|
2386
|
+
const nextIndex = parseOptionalPatternPart(part.slice(1, -1), lineTokens, index, captures);
|
|
2387
|
+
if (nextIndex !== void 0) index = nextIndex;
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2390
|
+
if (isCommandRestCaptureToken(part)) {
|
|
2391
|
+
const value = lineTokens.slice(index).join(" ");
|
|
2392
|
+
if (!value) return {
|
|
2393
|
+
ok: false,
|
|
2394
|
+
error: `Expected '${part}'`
|
|
2395
|
+
};
|
|
2396
|
+
captures[part.slice(1, -4)] = value;
|
|
2397
|
+
index = lineTokens.length;
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
const nextIndex = parsePatternToken(part, lineTokens, index, captures);
|
|
2401
|
+
if (nextIndex === void 0) return {
|
|
2402
|
+
ok: false,
|
|
2403
|
+
error: `Expected '${part}'`
|
|
2404
|
+
};
|
|
2405
|
+
index = nextIndex;
|
|
2406
|
+
}
|
|
2407
|
+
if (index !== lineTokens.length) return {
|
|
2408
|
+
ok: false,
|
|
2409
|
+
error: `Unexpected argument '${lineTokens[index]}'`
|
|
2410
|
+
};
|
|
2411
|
+
return {
|
|
2412
|
+
ok: true,
|
|
2413
|
+
value: captures
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
function commandPatternPrefixMatches(pattern, line) {
|
|
2417
|
+
const patternTokens = commandPatternParts(pattern);
|
|
2418
|
+
if (unsupportedCommandRestCaptureError(pattern)) return false;
|
|
2419
|
+
const lineTokens = tokenizeCommandPattern(line);
|
|
2420
|
+
let index = 0;
|
|
2421
|
+
for (const token of patternTokens) {
|
|
2422
|
+
if (isCommandCaptureToken(token) || isOptionalCommandPatternPart(token)) return true;
|
|
2423
|
+
if (lineTokens[index] !== token) return false;
|
|
2424
|
+
index += 1;
|
|
2425
|
+
}
|
|
2426
|
+
return true;
|
|
2427
|
+
}
|
|
2428
|
+
function parseOptionalPatternPart(pattern, lineTokens, startIndex, captures) {
|
|
2429
|
+
const patternTokens = tokenizeCommandPattern(pattern);
|
|
2430
|
+
if (patternTokens.length === 0 || lineTokens[startIndex] !== patternTokens[0]) return;
|
|
2431
|
+
const snapshot = { ...captures };
|
|
2432
|
+
let index = startIndex;
|
|
2433
|
+
for (const token of patternTokens) {
|
|
2434
|
+
const nextIndex = parsePatternToken(token, lineTokens, index, captures);
|
|
2435
|
+
if (nextIndex === void 0) {
|
|
2436
|
+
for (const key of Object.keys(captures)) delete captures[key];
|
|
2437
|
+
Object.assign(captures, snapshot);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
index = nextIndex;
|
|
2441
|
+
}
|
|
2442
|
+
return index;
|
|
2443
|
+
}
|
|
2444
|
+
function parsePatternToken(patternToken, lineTokens, index, captures) {
|
|
2445
|
+
if (isCommandCaptureToken(patternToken)) {
|
|
2446
|
+
const value = lineTokens[index];
|
|
2447
|
+
if (!value) return;
|
|
2448
|
+
captures[patternToken.slice(1, -1)] = value;
|
|
2449
|
+
return index + 1;
|
|
2450
|
+
}
|
|
2451
|
+
return lineTokens[index] === patternToken ? index + 1 : void 0;
|
|
2452
|
+
}
|
|
2453
|
+
//#endregion
|
|
2454
|
+
//#region src/action/entry-dispatch.ts
|
|
2455
|
+
const permissionOrder = [
|
|
2456
|
+
"read",
|
|
2457
|
+
"triage",
|
|
2458
|
+
"write",
|
|
2459
|
+
"maintain",
|
|
2460
|
+
"admin"
|
|
2461
|
+
];
|
|
2462
|
+
const changeRequestActions = [
|
|
2463
|
+
"opened",
|
|
2464
|
+
"updated",
|
|
2465
|
+
"reopened",
|
|
2466
|
+
"ready",
|
|
2467
|
+
"closed"
|
|
2468
|
+
];
|
|
2469
|
+
function dispatchRuntimeEntry(options) {
|
|
2470
|
+
if (options.kind === "change-request") return {
|
|
2471
|
+
kind: "change-request",
|
|
2472
|
+
tasks: selectRuntimeTasks({
|
|
2473
|
+
plan: options.plan,
|
|
2474
|
+
event: options.event,
|
|
2475
|
+
taskName: options.taskName
|
|
2476
|
+
}),
|
|
2477
|
+
taskName: options.taskName
|
|
2478
|
+
};
|
|
2479
|
+
return resolvePlanCommand(options.plan, options.line);
|
|
2480
|
+
}
|
|
2481
|
+
function selectRuntimeTasks(options) {
|
|
2482
|
+
if (options.taskName) return options.plan.tasks.filter((task) => task.name === options.taskName);
|
|
2483
|
+
return selectChangeRequestTasks(options.plan, options.event);
|
|
2484
|
+
}
|
|
2485
|
+
function selectLocalReviewTasks(plan) {
|
|
2486
|
+
return uniqBy(plan.changeRequestTriggers.map((trigger) => trigger.task), (task) => task.name).filter((task) => task.local !== false);
|
|
2487
|
+
}
|
|
2488
|
+
function selectChangeRequestTasks(plan, event) {
|
|
2489
|
+
if (!changeRequestActions.includes(event.action)) return [];
|
|
2490
|
+
const action = event.action;
|
|
2491
|
+
return uniqBy(plan.changeRequestTriggers.filter((trigger) => trigger.actions.includes(action)).map((trigger) => trigger.task), (task) => task.name);
|
|
2492
|
+
}
|
|
2493
|
+
function selectPlanCommand(plan, line) {
|
|
2494
|
+
let firstInvalid;
|
|
2495
|
+
for (const command of plan.commands) {
|
|
2496
|
+
const parsed = parseCommandPattern(command.pattern, line);
|
|
2497
|
+
if (!parsed.ok) {
|
|
2498
|
+
if (commandPatternPrefixMatches(command.pattern, line) && !firstInvalid) firstInvalid = {
|
|
2499
|
+
kind: "invalid",
|
|
2500
|
+
command,
|
|
2501
|
+
error: parsed.error
|
|
2502
|
+
};
|
|
2503
|
+
continue;
|
|
2504
|
+
}
|
|
2505
|
+
return {
|
|
2506
|
+
kind: "matched",
|
|
2507
|
+
command,
|
|
2508
|
+
commandName: command.pattern.replace(/^@pipr\s+/, "").split(/\s+/)[0] ?? command.pattern,
|
|
2509
|
+
line,
|
|
2510
|
+
arguments: parsed.value
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
return firstInvalid;
|
|
2514
|
+
}
|
|
2515
|
+
function resolvePlanCommand(plan, line) {
|
|
2516
|
+
if (!line) return {
|
|
2517
|
+
kind: "ignored",
|
|
2518
|
+
reason: "comment did not contain a command line"
|
|
2519
|
+
};
|
|
2520
|
+
if (!isPiprCommandLine(line)) return {
|
|
2521
|
+
kind: "ignored",
|
|
2522
|
+
reason: "comment did not target pipr"
|
|
2523
|
+
};
|
|
2524
|
+
const selected = selectPlanCommand(plan, line);
|
|
2525
|
+
if (selected?.kind === "matched") return {
|
|
2526
|
+
kind: "matched",
|
|
2527
|
+
invocation: {
|
|
2528
|
+
taskName: selected.command.task.name,
|
|
2529
|
+
commandName: selected.commandName,
|
|
2530
|
+
requiredPermission: selected.command.permission,
|
|
2531
|
+
line: selected.line,
|
|
2532
|
+
pattern: selected.command.pattern,
|
|
2533
|
+
arguments: selected.arguments
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
if (selected?.kind === "invalid") return {
|
|
2537
|
+
kind: "invalid",
|
|
2538
|
+
reason: selected.error,
|
|
2539
|
+
requiredPermission: selected.command.permission,
|
|
2540
|
+
body: renderPlanCommandHelp(plan, selected.error)
|
|
2541
|
+
};
|
|
2542
|
+
return {
|
|
2543
|
+
kind: "help",
|
|
2544
|
+
reason: `unknown pipr command '${line}'`,
|
|
2545
|
+
requiredPermission: "read",
|
|
2546
|
+
body: renderPlanCommandHelp(plan, `Unknown command: ${line}`)
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
function parsePlanCommandInputs(plan, invocation) {
|
|
2550
|
+
const matchingCommand = plan.commands.find((candidate) => candidate.task.name === invocation.taskName && candidate.pattern === invocation.pattern) ?? plan.commands.find((candidate) => candidate.task.name === invocation.taskName);
|
|
2551
|
+
if (!matchingCommand) return {
|
|
2552
|
+
kind: "invalid",
|
|
2553
|
+
reason: `No command registered for task '${invocation.taskName}'`,
|
|
2554
|
+
requiredPermission: invocation.requiredPermission,
|
|
2555
|
+
body: renderPlanCommandHelp(plan)
|
|
2556
|
+
};
|
|
2557
|
+
try {
|
|
2558
|
+
return {
|
|
2559
|
+
kind: "matched",
|
|
2560
|
+
invocation: {
|
|
2561
|
+
...invocation,
|
|
2562
|
+
inputs: matchingCommand.parse ? matchingCommand.parse(invocation.arguments) : invocation.arguments
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2567
|
+
return {
|
|
2568
|
+
kind: "invalid",
|
|
2569
|
+
reason,
|
|
2570
|
+
requiredPermission: invocation.requiredPermission,
|
|
2571
|
+
body: renderPlanCommandHelp(plan, reason)
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
function hasRequiredRepositoryPermission(actual, required) {
|
|
2576
|
+
if (actual === "none") return false;
|
|
2577
|
+
return permissionOrder.indexOf(actual) >= permissionOrder.indexOf(required);
|
|
2578
|
+
}
|
|
2579
|
+
function permissionDeniedHelp(plan, required) {
|
|
2580
|
+
return renderPlanCommandHelp(plan, `Permission denied: requires ${required}.`);
|
|
2581
|
+
}
|
|
2582
|
+
function renderPlanCommandHelp(plan, reason) {
|
|
2583
|
+
const lines = ["# pipr commands", ""];
|
|
2584
|
+
if (reason) lines.push(reason, "");
|
|
2585
|
+
for (const command of plan.commands) lines.push(`- ${command.pattern} (${command.permission})`);
|
|
2586
|
+
return lines.join("\n");
|
|
2587
|
+
}
|
|
2588
|
+
//#endregion
|
|
2589
|
+
//#region src/diff/path-filter.ts
|
|
2590
|
+
const matcherOptions = {
|
|
2591
|
+
dot: true,
|
|
2592
|
+
nonegate: true,
|
|
2593
|
+
windows: false
|
|
2594
|
+
};
|
|
2595
|
+
const compiledFilters = /* @__PURE__ */ new WeakMap();
|
|
2596
|
+
function filterDiffManifestByPaths(manifest, filter) {
|
|
2597
|
+
if (!filter) return manifest;
|
|
2598
|
+
return {
|
|
2599
|
+
...manifest,
|
|
2600
|
+
files: manifest.files.filter((file) => diffFileMatchesPathFilter(file, filter))
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
function diffFileMatchesPathFilter(file, filter) {
|
|
2604
|
+
if (!filter) return true;
|
|
2605
|
+
const paths = [...new Set([file.path, file.previousPath].filter((item) => item !== void 0))].map((item) => item.replaceAll("\\", "/").replace(/^\.\/+/, ""));
|
|
2606
|
+
const compiled = readCompiledFilter(filter);
|
|
2607
|
+
const include = compiled.include;
|
|
2608
|
+
const exclude = compiled.exclude;
|
|
2609
|
+
const included = include ? paths.some((filePath) => include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : true;
|
|
2610
|
+
const excluded = exclude ? paths.some((filePath) => exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : false;
|
|
2611
|
+
return included && !excluded;
|
|
2612
|
+
}
|
|
2613
|
+
function pathMatchesFilter(filePath, filter) {
|
|
2614
|
+
if (!filter) return true;
|
|
2615
|
+
const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
2616
|
+
const compiled = readCompiledFilter(filter);
|
|
2617
|
+
if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
|
|
2618
|
+
return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
|
|
2619
|
+
}
|
|
2620
|
+
function readCompiledFilter(filter) {
|
|
2621
|
+
const existing = compiledFilters.get(filter);
|
|
2622
|
+
if (existing) return existing;
|
|
2623
|
+
const compiled = {
|
|
2624
|
+
include: filter.include?.map(compilePattern),
|
|
2625
|
+
exclude: filter.exclude?.map(compilePattern)
|
|
2626
|
+
};
|
|
2627
|
+
compiledFilters.set(filter, compiled);
|
|
2628
|
+
return compiled;
|
|
2629
|
+
}
|
|
2630
|
+
function compilePattern(pattern) {
|
|
2631
|
+
return {
|
|
2632
|
+
matches: picomatch(pattern, matcherOptions),
|
|
2633
|
+
matchBasename: !pattern.includes("/")
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
//#endregion
|
|
2637
|
+
//#region src/diff/manifest-projection.ts
|
|
2638
|
+
const defaultDiffManifestPromptLimits = {
|
|
2639
|
+
fullMaxBytes: 128 * 1024,
|
|
2640
|
+
fullMaxEstimatedTokens: 32e3,
|
|
2641
|
+
condensedMaxBytes: 256 * 1024,
|
|
2642
|
+
condensedMaxEstimatedTokens: 64e3,
|
|
2643
|
+
toolResponseMaxBytes: 64 * 1024
|
|
2644
|
+
};
|
|
2645
|
+
function projectDiffManifest(manifest, options) {
|
|
2646
|
+
if (!manifestOptionsHaveEffect(options)) return manifest;
|
|
2647
|
+
const manifestOptions = options ?? {};
|
|
2648
|
+
const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
|
|
2649
|
+
return parseDiffManifest({
|
|
2650
|
+
...scopedManifest,
|
|
2651
|
+
files: scopedManifest.files.map((file) => ({
|
|
2652
|
+
...withoutCompressedFileFields(file, manifestOptions.compressed === true),
|
|
2653
|
+
commentableRanges: file.commentableRanges.map((range) => ({
|
|
2654
|
+
...rangeFieldsForOptions(range, manifestOptions),
|
|
2655
|
+
...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
|
|
2656
|
+
}))
|
|
2657
|
+
}))
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
function cloneDiffManifest(manifest) {
|
|
2661
|
+
return parseDiffManifest(structuredClone(manifest));
|
|
2662
|
+
}
|
|
2663
|
+
function prepareDiffManifestPrompt(manifest, config) {
|
|
2664
|
+
const limits = resolveDiffManifestPromptLimits(config);
|
|
2665
|
+
const full = measureDiffManifestPrompt(manifest);
|
|
2666
|
+
if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
|
|
2667
|
+
mode: "full",
|
|
2668
|
+
manifest,
|
|
2669
|
+
metrics: {
|
|
2670
|
+
full,
|
|
2671
|
+
selected: full
|
|
2672
|
+
},
|
|
2673
|
+
limits
|
|
2674
|
+
};
|
|
2675
|
+
const condensedManifest = condenseDiffManifest(manifest);
|
|
2676
|
+
const condensed = measureDiffManifestPrompt(condensedManifest);
|
|
2677
|
+
if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
|
|
2678
|
+
"Diff Manifest payload exceeds condensed limit before Pi execution",
|
|
2679
|
+
`selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
|
|
2680
|
+
`limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
|
|
2681
|
+
].join("; "));
|
|
2682
|
+
return {
|
|
2683
|
+
mode: "condensed",
|
|
2684
|
+
manifest: condensedManifest,
|
|
2685
|
+
metrics: {
|
|
2686
|
+
full,
|
|
2687
|
+
selected: condensed
|
|
2688
|
+
},
|
|
2689
|
+
limits
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
function condenseDiffManifest(manifest) {
|
|
2693
|
+
return {
|
|
2694
|
+
baseSha: manifest.baseSha,
|
|
2695
|
+
headSha: manifest.headSha,
|
|
2696
|
+
mergeBaseSha: manifest.mergeBaseSha,
|
|
2697
|
+
files: manifest.files.map(condenseDiffManifestFile)
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
function measureDiffManifestPrompt(manifest) {
|
|
2701
|
+
const json = JSON.stringify(manifest, null, 2);
|
|
2702
|
+
const bytes = Buffer.byteLength(json, "utf8");
|
|
2703
|
+
return {
|
|
2704
|
+
bytes,
|
|
2705
|
+
estimatedTokens: Math.ceil(bytes / 4)
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
function resolveDiffManifestPromptLimits(config) {
|
|
2709
|
+
return {
|
|
2710
|
+
...defaultDiffManifestPromptLimits,
|
|
2711
|
+
...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
function manifestOptionsHaveEffect(options) {
|
|
2715
|
+
return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
|
|
2716
|
+
}
|
|
2717
|
+
function withoutCompressedFileFields(file, compressed) {
|
|
2718
|
+
if (!compressed) return file;
|
|
2719
|
+
const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
|
|
2720
|
+
return rest;
|
|
2721
|
+
}
|
|
2722
|
+
function withoutCompressedRangeFields(range, compressed) {
|
|
2723
|
+
if (!compressed) return range;
|
|
2724
|
+
const { summary: _summary, ...rest } = range;
|
|
2725
|
+
return rest;
|
|
2726
|
+
}
|
|
2727
|
+
function rangeFieldsForOptions(range, options) {
|
|
2728
|
+
const fields = withoutCompressedRangeFields(range, options.compressed === true);
|
|
2729
|
+
if (options.includePreviews === false) {
|
|
2730
|
+
const { preview: _preview, ...rest } = fields;
|
|
2731
|
+
return rest;
|
|
2732
|
+
}
|
|
2733
|
+
return fields;
|
|
2734
|
+
}
|
|
2735
|
+
function truncatePreview(preview, maxLines) {
|
|
2736
|
+
if (preview === void 0 || maxLines === void 0) return preview;
|
|
2737
|
+
return preview.split("\n").slice(0, maxLines).join("\n");
|
|
2738
|
+
}
|
|
2739
|
+
function condenseDiffManifestFile(file) {
|
|
2740
|
+
return {
|
|
2741
|
+
path: file.path,
|
|
2742
|
+
previousPath: file.previousPath,
|
|
2743
|
+
status: file.status,
|
|
2744
|
+
language: file.language,
|
|
2745
|
+
additions: file.additions,
|
|
2746
|
+
deletions: file.deletions,
|
|
2747
|
+
hunks: file.hunks.map((hunk) => ({
|
|
2748
|
+
hunkIndex: hunk.hunkIndex,
|
|
2749
|
+
header: hunk.header,
|
|
2750
|
+
oldStart: hunk.oldStart,
|
|
2751
|
+
oldLines: hunk.oldLines,
|
|
2752
|
+
newStart: hunk.newStart,
|
|
2753
|
+
newLines: hunk.newLines,
|
|
2754
|
+
contentHash: hunk.contentHash
|
|
2755
|
+
})),
|
|
2756
|
+
commentableRanges: file.commentableRanges.map((range) => ({
|
|
2757
|
+
id: range.id,
|
|
2758
|
+
path: range.path,
|
|
2759
|
+
side: range.side,
|
|
2760
|
+
startLine: range.startLine,
|
|
2761
|
+
endLine: range.endLine,
|
|
2762
|
+
kind: range.kind,
|
|
2763
|
+
hunkIndex: range.hunkIndex,
|
|
2764
|
+
hunkHeader: range.hunkHeader,
|
|
2765
|
+
hunkContentHash: range.hunkContentHash
|
|
2766
|
+
})),
|
|
2767
|
+
excludedReason: file.excludedReason
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
|
|
2771
|
+
return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
|
|
2772
|
+
}
|
|
2773
|
+
//#endregion
|
|
2774
|
+
//#region src/pi/runtime-tools.ts
|
|
2775
|
+
const piRuntimeReadToolNames = ["pipr_read_diff", "pipr_read_at_ref"];
|
|
2776
|
+
async function preparePiRuntimeReadTools(options) {
|
|
2777
|
+
const toolRoot = path.join(options.root, "runtime-tools");
|
|
2778
|
+
const baseRoot = path.join(toolRoot, "base");
|
|
2779
|
+
await mkdir(baseRoot, { recursive: true });
|
|
2780
|
+
const baseRanges = await materializeBaseRangeSnapshots({
|
|
2781
|
+
baseRoot,
|
|
2782
|
+
manifest: options.request.manifest,
|
|
2783
|
+
sourceWorkspace: options.sourceWorkspace,
|
|
2784
|
+
maxBytes: options.request.toolResponseMaxBytes
|
|
2785
|
+
});
|
|
2786
|
+
const data = {
|
|
2787
|
+
manifest: options.request.manifest,
|
|
2788
|
+
toolResponseMaxBytes: options.request.toolResponseMaxBytes,
|
|
2789
|
+
baseRanges
|
|
2790
|
+
};
|
|
2791
|
+
const dataPath = path.join(toolRoot, "data.json");
|
|
2792
|
+
await Bun.write(dataPath, JSON.stringify(data));
|
|
2793
|
+
return {
|
|
2794
|
+
extensionPath: await piRuntimeToolsExtensionPath(),
|
|
2795
|
+
dataPath,
|
|
2796
|
+
toolNames: piRuntimeReadToolNames
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
async function piRuntimeToolsExtensionPath() {
|
|
2800
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
2801
|
+
const candidates = [
|
|
2802
|
+
path.join(moduleDir, "pi", "runtime-tools-extension.mjs"),
|
|
2803
|
+
path.join(moduleDir, "runtime-tools-extension.mjs"),
|
|
2804
|
+
path.join(moduleDir, "..", "..", "dist", "pi", "runtime-tools-extension.mjs"),
|
|
2805
|
+
path.join(moduleDir, "runtime-tools-extension.ts")
|
|
2806
|
+
];
|
|
2807
|
+
for (const candidate of candidates) if (await pathExists(candidate)) return candidate;
|
|
2808
|
+
throw new Error("Unable to locate pipr runtime tools extension");
|
|
2809
|
+
}
|
|
2810
|
+
async function pathExists(filePath) {
|
|
2811
|
+
return await Bun.file(filePath).exists();
|
|
2427
2812
|
}
|
|
2428
2813
|
async function materializeBaseRangeSnapshots(options) {
|
|
2429
2814
|
const ranges = {};
|
|
@@ -2853,51 +3238,31 @@ function createRuntimeActionLog(options) {
|
|
|
2853
3238
|
for (const [key, value] of Object.entries(options.env ?? process.env)) if (sensitiveEnvNamePattern.test(key)) addSecret(secrets, value);
|
|
2854
3239
|
const debugEnabled = (options.env ?? process.env).ACTIONS_STEP_DEBUG === "true" || (options.env ?? process.env).PIPR_LOG_LEVEL === "debug";
|
|
2855
3240
|
const sink = options.logSink ?? noopActionLogSink;
|
|
2856
|
-
const writer = structuredLogWriter(sink, secrets);
|
|
2857
|
-
const logger = pino({
|
|
2858
|
-
base: void 0,
|
|
2859
|
-
level: debugEnabled ? "debug" : "info",
|
|
2860
|
-
timestamp: false,
|
|
2861
|
-
messageKey: "event",
|
|
2862
|
-
customLevels: { notice: 35 },
|
|
2863
|
-
formatters: { level(label) {
|
|
2864
|
-
return { level: label };
|
|
2865
|
-
} }
|
|
2866
|
-
}, writer.stream);
|
|
2867
3241
|
return {
|
|
2868
3242
|
debugEnabled,
|
|
2869
3243
|
writesToSink: options.logSink !== void 0,
|
|
2870
3244
|
info(event, fields) {
|
|
2871
|
-
|
|
2872
|
-
logger.info(compactFields(fields, secrets), redact(event, secrets));
|
|
3245
|
+
emitRecord(sink, secrets, "info", event, fields);
|
|
2873
3246
|
},
|
|
2874
3247
|
notice(event, fields) {
|
|
2875
|
-
|
|
2876
|
-
logger.notice(compactFields(fields, secrets), redact(event, secrets));
|
|
3248
|
+
emitRecord(sink, secrets, "notice", event, fields);
|
|
2877
3249
|
},
|
|
2878
3250
|
warning(event, fields) {
|
|
2879
|
-
|
|
2880
|
-
logger.warn(compactFields(fields, secrets), redact(event, secrets));
|
|
3251
|
+
emitRecord(sink, secrets, "warning", event, fields);
|
|
2881
3252
|
},
|
|
2882
3253
|
error(event, fields) {
|
|
2883
|
-
|
|
2884
|
-
logger.error(compactFields(fields, secrets), redact(event, secrets));
|
|
3254
|
+
emitRecord(sink, secrets, "error", event, fields);
|
|
2885
3255
|
},
|
|
2886
3256
|
debug(event, fields) {
|
|
2887
|
-
if (debugEnabled)
|
|
2888
|
-
writer.level = "debug";
|
|
2889
|
-
logger.debug(compactFields(fields, secrets), redact(event, secrets));
|
|
2890
|
-
}
|
|
3257
|
+
if (debugEnabled) emitRecord(sink, secrets, "debug", event, fields);
|
|
2891
3258
|
},
|
|
2892
3259
|
text(level, event, text) {
|
|
2893
3260
|
if (level === "debug" && !debugEnabled) return;
|
|
2894
|
-
|
|
2895
|
-
sinkForLevel(sink, level)(message);
|
|
3261
|
+
emitRecord(sink, secrets, level, event, void 0, redact(text, secrets));
|
|
2896
3262
|
},
|
|
2897
3263
|
textSnippet(level, event, text, snippetOptions) {
|
|
2898
3264
|
if (level === "debug" && !debugEnabled) return;
|
|
2899
|
-
|
|
2900
|
-
sinkForLevel(sink, level)(message);
|
|
3265
|
+
emitRecord(sink, secrets, level, event, void 0, formatTextSnippet(text, secrets, snippetOptions));
|
|
2901
3266
|
},
|
|
2902
3267
|
formatTextSnippet(text, snippetOptions) {
|
|
2903
3268
|
return formatTextSnippet(text, secrets, snippetOptions);
|
|
@@ -2941,93 +3306,21 @@ function compactFields(fields, secrets) {
|
|
|
2941
3306
|
function formatTextSnippet(text, secrets, options) {
|
|
2942
3307
|
return boundedLogSnippet(redact(text, secrets), options);
|
|
2943
3308
|
}
|
|
2944
|
-
function
|
|
2945
|
-
|
|
2946
|
-
level
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
};
|
|
2951
|
-
return writer;
|
|
2952
|
-
}
|
|
2953
|
-
function structuredLine(logger, writer, level, event) {
|
|
2954
|
-
let line = "";
|
|
2955
|
-
const previousStream = writer.stream.write;
|
|
2956
|
-
writer.stream.write = (value) => {
|
|
2957
|
-
line = value.trimEnd();
|
|
2958
|
-
};
|
|
2959
|
-
writer.level = level;
|
|
2960
|
-
logger[logMethod(level)]({}, event);
|
|
2961
|
-
writer.stream.write = previousStream;
|
|
2962
|
-
return line;
|
|
2963
|
-
}
|
|
2964
|
-
function logMethod(level) {
|
|
2965
|
-
if (level === "warning") return "warn";
|
|
2966
|
-
return level;
|
|
2967
|
-
}
|
|
2968
|
-
function sinkForLevel(sink, level) {
|
|
2969
|
-
if (level === "warning") return sink.warning.bind(sink);
|
|
2970
|
-
return sink[level].bind(sink);
|
|
3309
|
+
function emitRecord(sink, secrets, level, event, fields, text) {
|
|
3310
|
+
sink.log({
|
|
3311
|
+
level,
|
|
3312
|
+
event: redact(event, secrets),
|
|
3313
|
+
fields: compactFields(fields, secrets),
|
|
3314
|
+
text
|
|
3315
|
+
});
|
|
2971
3316
|
}
|
|
2972
3317
|
const noopActionLogSink = {
|
|
2973
|
-
|
|
2974
|
-
notice() {},
|
|
2975
|
-
warning() {},
|
|
2976
|
-
error() {},
|
|
2977
|
-
debug() {},
|
|
3318
|
+
log() {},
|
|
2978
3319
|
async group(_name, run) {
|
|
2979
3320
|
return await run();
|
|
2980
3321
|
}
|
|
2981
3322
|
};
|
|
2982
3323
|
//#endregion
|
|
2983
|
-
//#region src/diff/path-filter.ts
|
|
2984
|
-
const matcherOptions = {
|
|
2985
|
-
dot: true,
|
|
2986
|
-
nonegate: true,
|
|
2987
|
-
windows: false
|
|
2988
|
-
};
|
|
2989
|
-
const compiledFilters = /* @__PURE__ */ new WeakMap();
|
|
2990
|
-
function filterDiffManifestByPaths(manifest, filter) {
|
|
2991
|
-
if (!filter) return manifest;
|
|
2992
|
-
return {
|
|
2993
|
-
...manifest,
|
|
2994
|
-
files: manifest.files.filter((file) => diffFileMatchesPathFilter(file, filter))
|
|
2995
|
-
};
|
|
2996
|
-
}
|
|
2997
|
-
function diffFileMatchesPathFilter(file, filter) {
|
|
2998
|
-
if (!filter) return true;
|
|
2999
|
-
const paths = [...new Set([file.path, file.previousPath].filter((item) => item !== void 0))].map((item) => item.replaceAll("\\", "/").replace(/^\.\/+/, ""));
|
|
3000
|
-
const compiled = readCompiledFilter(filter);
|
|
3001
|
-
const include = compiled.include;
|
|
3002
|
-
const exclude = compiled.exclude;
|
|
3003
|
-
const included = include ? paths.some((filePath) => include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : true;
|
|
3004
|
-
const excluded = exclude ? paths.some((filePath) => exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(filePath) : filePath))) : false;
|
|
3005
|
-
return included && !excluded;
|
|
3006
|
-
}
|
|
3007
|
-
function pathMatchesFilter(filePath, filter) {
|
|
3008
|
-
if (!filter) return true;
|
|
3009
|
-
const normalizedPath = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
3010
|
-
const compiled = readCompiledFilter(filter);
|
|
3011
|
-
if (!(compiled.include ? compiled.include.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true)) return false;
|
|
3012
|
-
return compiled.exclude ? !compiled.exclude.some((pattern) => pattern.matches(pattern.matchBasename ? path.posix.basename(normalizedPath) : normalizedPath)) : true;
|
|
3013
|
-
}
|
|
3014
|
-
function readCompiledFilter(filter) {
|
|
3015
|
-
const existing = compiledFilters.get(filter);
|
|
3016
|
-
if (existing) return existing;
|
|
3017
|
-
const compiled = {
|
|
3018
|
-
include: filter.include?.map(compilePattern),
|
|
3019
|
-
exclude: filter.exclude?.map(compilePattern)
|
|
3020
|
-
};
|
|
3021
|
-
compiledFilters.set(filter, compiled);
|
|
3022
|
-
return compiled;
|
|
3023
|
-
}
|
|
3024
|
-
function compilePattern(pattern) {
|
|
3025
|
-
return {
|
|
3026
|
-
matches: picomatch(pattern, matcherOptions),
|
|
3027
|
-
matchBasename: !pattern.includes("/")
|
|
3028
|
-
};
|
|
3029
|
-
}
|
|
3030
|
-
//#endregion
|
|
3031
3324
|
//#region src/review/range-validation.ts
|
|
3032
3325
|
function assertFindingMatchesRange(finding, range) {
|
|
3033
3326
|
const reason = findingRangeMismatchReason(finding, range);
|
|
@@ -3043,7 +3336,7 @@ function findingRangeMismatchReason(finding, range) {
|
|
|
3043
3336
|
}
|
|
3044
3337
|
//#endregion
|
|
3045
3338
|
//#region src/review/review.ts
|
|
3046
|
-
function
|
|
3339
|
+
function validateReviewResult(review, manifest, options) {
|
|
3047
3340
|
if (options.expectedHeadSha && manifest.headSha !== options.expectedHeadSha) throw new Error(`Diff Manifest head SHA '${manifest.headSha}' does not match expected head SHA '${options.expectedHeadSha}'`);
|
|
3048
3341
|
const ranges = createDiffRangeIndex(manifest);
|
|
3049
3342
|
const seenFingerprints = /* @__PURE__ */ new Set();
|
|
@@ -3111,7 +3404,7 @@ function findingFingerprint(finding) {
|
|
|
3111
3404
|
//#endregion
|
|
3112
3405
|
//#region src/review/agent/agent-prompt.ts
|
|
3113
3406
|
async function renderAgentPrompt(options) {
|
|
3114
|
-
const prompt = await options.agent
|
|
3407
|
+
const prompt = await renderAgentDefinitionPrompt(options.agent, options.input, options.agentRunContext.prompt);
|
|
3115
3408
|
const toolMode = options.toolMode ?? "read-only";
|
|
3116
3409
|
return compact([
|
|
3117
3410
|
promptSection("Role", "You are pipr's read-only change request agent."),
|
|
@@ -3126,6 +3419,9 @@ async function renderAgentPrompt(options) {
|
|
|
3126
3419
|
promptSection("Prompt", renderPromptValue(prompt))
|
|
3127
3420
|
]).join("\n\n");
|
|
3128
3421
|
}
|
|
3422
|
+
function renderAgentDefinitionPrompt(agent, input, context) {
|
|
3423
|
+
return agent.definition.prompt(input, { ...context });
|
|
3424
|
+
}
|
|
3129
3425
|
function promptSection(title, body) {
|
|
3130
3426
|
if (!body?.trim()) return;
|
|
3131
3427
|
return `${title}:\n${body}`;
|
|
@@ -3142,12 +3438,12 @@ function outputPrompt(schema) {
|
|
|
3142
3438
|
return compact([
|
|
3143
3439
|
`Schema ID: ${schema.id}.`,
|
|
3144
3440
|
schema.jsonSchema ? `JSON Schema:\n${JSON.stringify(schema.jsonSchema, null, 2)}` : void 0,
|
|
3145
|
-
schema.id ===
|
|
3146
|
-
schema.id ===
|
|
3441
|
+
schema.id === reviewResultSchemaId ? `Example:\n${JSON.stringify(reviewSchemaExample$1(), null, 2)}` : void 0,
|
|
3442
|
+
schema.id === reviewResultSchemaId ? "`suggestedFix` is exact replacement code for the selected range. Do not include Markdown fences, prose, or labels in `suggestedFix`." : void 0,
|
|
3147
3443
|
"Return exactly one JSON value matching the schema.",
|
|
3148
3444
|
"The first non-whitespace character must be { or [ and the last non-whitespace character must be } or ].",
|
|
3149
3445
|
"Do not include Markdown, code fences, prose, explanations, or leading/trailing text.",
|
|
3150
|
-
schema.id ===
|
|
3446
|
+
schema.id === reviewResultSchemaId ? "For inlineFindings, use only fields shown in the schema and only exact Diff Manifest commentable ranges. If no exact range applies, omit the finding." : void 0
|
|
3151
3447
|
]).join("\n\n");
|
|
3152
3448
|
}
|
|
3153
3449
|
function pathScopePrompt(paths) {
|
|
@@ -3184,143 +3480,6 @@ function customToolPrompt(agentTools) {
|
|
|
3184
3480
|
return ["Custom plugin tools:", ...agentTools.customTools.map((tool) => `${tool.name}: ${tool.description ?? "No description."}`)].join("\n");
|
|
3185
3481
|
}
|
|
3186
3482
|
//#endregion
|
|
3187
|
-
//#region src/diff/manifest-projection.ts
|
|
3188
|
-
const defaultDiffManifestPromptLimits = {
|
|
3189
|
-
fullMaxBytes: 128 * 1024,
|
|
3190
|
-
fullMaxEstimatedTokens: 32e3,
|
|
3191
|
-
condensedMaxBytes: 256 * 1024,
|
|
3192
|
-
condensedMaxEstimatedTokens: 64e3,
|
|
3193
|
-
toolResponseMaxBytes: 64 * 1024
|
|
3194
|
-
};
|
|
3195
|
-
function projectDiffManifest(manifest, options) {
|
|
3196
|
-
if (!manifestOptionsHaveEffect(options)) return manifest;
|
|
3197
|
-
const manifestOptions = options ?? {};
|
|
3198
|
-
const scopedManifest = filterDiffManifestByPaths(manifest, manifestOptions.paths);
|
|
3199
|
-
return parseDiffManifest({
|
|
3200
|
-
...scopedManifest,
|
|
3201
|
-
files: scopedManifest.files.map((file) => ({
|
|
3202
|
-
...withoutCompressedFileFields(file, manifestOptions.compressed === true),
|
|
3203
|
-
commentableRanges: file.commentableRanges.map((range) => ({
|
|
3204
|
-
...rangeFieldsForOptions(range, manifestOptions),
|
|
3205
|
-
...manifestOptions.includePreviews === false ? {} : { preview: truncatePreview(range.preview, manifestOptions.maxPreviewLines) }
|
|
3206
|
-
}))
|
|
3207
|
-
}))
|
|
3208
|
-
});
|
|
3209
|
-
}
|
|
3210
|
-
function cloneDiffManifest(manifest) {
|
|
3211
|
-
return parseDiffManifest(structuredClone(manifest));
|
|
3212
|
-
}
|
|
3213
|
-
function prepareDiffManifestPrompt(manifest, config) {
|
|
3214
|
-
const limits = resolveDiffManifestPromptLimits(config);
|
|
3215
|
-
const full = measureDiffManifestPrompt(manifest);
|
|
3216
|
-
if (fitsLimit(full, limits.fullMaxBytes, limits.fullMaxEstimatedTokens)) return {
|
|
3217
|
-
mode: "full",
|
|
3218
|
-
manifest,
|
|
3219
|
-
metrics: {
|
|
3220
|
-
full,
|
|
3221
|
-
selected: full
|
|
3222
|
-
},
|
|
3223
|
-
limits
|
|
3224
|
-
};
|
|
3225
|
-
const condensedManifest = condenseDiffManifest(manifest);
|
|
3226
|
-
const condensed = measureDiffManifestPrompt(condensedManifest);
|
|
3227
|
-
if (!fitsLimit(condensed, limits.condensedMaxBytes, limits.condensedMaxEstimatedTokens)) throw new Error([
|
|
3228
|
-
"Diff Manifest payload exceeds condensed limit before Pi execution",
|
|
3229
|
-
`selected=${condensed.bytes} bytes/${condensed.estimatedTokens} estimated tokens`,
|
|
3230
|
-
`limit=${limits.condensedMaxBytes} bytes/${limits.condensedMaxEstimatedTokens} estimated tokens`
|
|
3231
|
-
].join("; "));
|
|
3232
|
-
return {
|
|
3233
|
-
mode: "condensed",
|
|
3234
|
-
manifest: condensedManifest,
|
|
3235
|
-
metrics: {
|
|
3236
|
-
full,
|
|
3237
|
-
selected: condensed
|
|
3238
|
-
},
|
|
3239
|
-
limits
|
|
3240
|
-
};
|
|
3241
|
-
}
|
|
3242
|
-
function condenseDiffManifest(manifest) {
|
|
3243
|
-
return {
|
|
3244
|
-
baseSha: manifest.baseSha,
|
|
3245
|
-
headSha: manifest.headSha,
|
|
3246
|
-
mergeBaseSha: manifest.mergeBaseSha,
|
|
3247
|
-
files: manifest.files.map(condenseDiffManifestFile)
|
|
3248
|
-
};
|
|
3249
|
-
}
|
|
3250
|
-
function measureDiffManifestPrompt(manifest) {
|
|
3251
|
-
const json = JSON.stringify(manifest, null, 2);
|
|
3252
|
-
const bytes = Buffer.byteLength(json, "utf8");
|
|
3253
|
-
return {
|
|
3254
|
-
bytes,
|
|
3255
|
-
estimatedTokens: Math.ceil(bytes / 4)
|
|
3256
|
-
};
|
|
3257
|
-
}
|
|
3258
|
-
function resolveDiffManifestPromptLimits(config) {
|
|
3259
|
-
return {
|
|
3260
|
-
...defaultDiffManifestPromptLimits,
|
|
3261
|
-
...Object.fromEntries(Object.entries(config ?? {}).filter((entry) => entry[1] !== void 0))
|
|
3262
|
-
};
|
|
3263
|
-
}
|
|
3264
|
-
function manifestOptionsHaveEffect(options) {
|
|
3265
|
-
return Boolean(options?.compressed || options?.includePreviews === false || options?.maxPreviewLines !== void 0 || options?.paths);
|
|
3266
|
-
}
|
|
3267
|
-
function withoutCompressedFileFields(file, compressed) {
|
|
3268
|
-
if (!compressed) return file;
|
|
3269
|
-
const { signals: _signals, changedSymbols: _changedSymbols, ...rest } = file;
|
|
3270
|
-
return rest;
|
|
3271
|
-
}
|
|
3272
|
-
function withoutCompressedRangeFields(range, compressed) {
|
|
3273
|
-
if (!compressed) return range;
|
|
3274
|
-
const { summary: _summary, ...rest } = range;
|
|
3275
|
-
return rest;
|
|
3276
|
-
}
|
|
3277
|
-
function rangeFieldsForOptions(range, options) {
|
|
3278
|
-
const fields = withoutCompressedRangeFields(range, options.compressed === true);
|
|
3279
|
-
if (options.includePreviews === false) {
|
|
3280
|
-
const { preview: _preview, ...rest } = fields;
|
|
3281
|
-
return rest;
|
|
3282
|
-
}
|
|
3283
|
-
return fields;
|
|
3284
|
-
}
|
|
3285
|
-
function truncatePreview(preview, maxLines) {
|
|
3286
|
-
if (preview === void 0 || maxLines === void 0) return preview;
|
|
3287
|
-
return preview.split("\n").slice(0, maxLines).join("\n");
|
|
3288
|
-
}
|
|
3289
|
-
function condenseDiffManifestFile(file) {
|
|
3290
|
-
return {
|
|
3291
|
-
path: file.path,
|
|
3292
|
-
previousPath: file.previousPath,
|
|
3293
|
-
status: file.status,
|
|
3294
|
-
language: file.language,
|
|
3295
|
-
additions: file.additions,
|
|
3296
|
-
deletions: file.deletions,
|
|
3297
|
-
hunks: file.hunks.map((hunk) => ({
|
|
3298
|
-
hunkIndex: hunk.hunkIndex,
|
|
3299
|
-
header: hunk.header,
|
|
3300
|
-
oldStart: hunk.oldStart,
|
|
3301
|
-
oldLines: hunk.oldLines,
|
|
3302
|
-
newStart: hunk.newStart,
|
|
3303
|
-
newLines: hunk.newLines,
|
|
3304
|
-
contentHash: hunk.contentHash
|
|
3305
|
-
})),
|
|
3306
|
-
commentableRanges: file.commentableRanges.map((range) => ({
|
|
3307
|
-
id: range.id,
|
|
3308
|
-
path: range.path,
|
|
3309
|
-
side: range.side,
|
|
3310
|
-
startLine: range.startLine,
|
|
3311
|
-
endLine: range.endLine,
|
|
3312
|
-
kind: range.kind,
|
|
3313
|
-
hunkIndex: range.hunkIndex,
|
|
3314
|
-
hunkHeader: range.hunkHeader,
|
|
3315
|
-
hunkContentHash: range.hunkContentHash
|
|
3316
|
-
})),
|
|
3317
|
-
excludedReason: file.excludedReason
|
|
3318
|
-
};
|
|
3319
|
-
}
|
|
3320
|
-
function fitsLimit(metrics, maxBytes, maxEstimatedTokens) {
|
|
3321
|
-
return metrics.bytes <= maxBytes && metrics.estimatedTokens <= maxEstimatedTokens;
|
|
3322
|
-
}
|
|
3323
|
-
//#endregion
|
|
3324
3483
|
//#region src/review/agent/diff-manifest-context.ts
|
|
3325
3484
|
function prepareDiffManifestContext(options) {
|
|
3326
3485
|
const manifest = readReservedInputManifest(options.input);
|
|
@@ -3652,9 +3811,9 @@ function parseAgentOutput(output, agent) {
|
|
|
3652
3811
|
let lastError = "";
|
|
3653
3812
|
for (const payload of jsonPayloadCandidates(output)) try {
|
|
3654
3813
|
const json = JSON.parse(payload);
|
|
3655
|
-
if (agent.definition.output.id ===
|
|
3814
|
+
if (agent.definition.output.id === reviewResultSchemaId) return {
|
|
3656
3815
|
ok: true,
|
|
3657
|
-
value:
|
|
3816
|
+
value: parseReviewResult(json),
|
|
3658
3817
|
repairAttempted: false
|
|
3659
3818
|
};
|
|
3660
3819
|
return {
|
|
@@ -3690,6 +3849,9 @@ function buildRepairPrompt(options) {
|
|
|
3690
3849
|
].join("\n\n");
|
|
3691
3850
|
}
|
|
3692
3851
|
//#endregion
|
|
3852
|
+
//#region package.json
|
|
3853
|
+
var version = "0.2.0";
|
|
3854
|
+
//#endregion
|
|
3693
3855
|
//#region src/review/prior-state.ts
|
|
3694
3856
|
const mainCommentMarker = "pipr:main-comment";
|
|
3695
3857
|
const inlineFindingMarkerPrefix = "pipr:finding";
|
|
@@ -3732,7 +3894,7 @@ function buildPriorReviewState(options) {
|
|
|
3732
3894
|
const prior = priorFindings.get(id);
|
|
3733
3895
|
if (prior) usedPriorIds.add(prior.id);
|
|
3734
3896
|
currentFindingIds.add(id);
|
|
3735
|
-
|
|
3897
|
+
const record = {
|
|
3736
3898
|
id,
|
|
3737
3899
|
status: "open",
|
|
3738
3900
|
path: finding.path,
|
|
@@ -3742,29 +3904,30 @@ function buildPriorReviewState(options) {
|
|
|
3742
3904
|
endLine: finding.endLine,
|
|
3743
3905
|
firstSeenHeadSha: prior?.firstSeenHeadSha ?? options.reviewedHeadSha,
|
|
3744
3906
|
lastSeenHeadSha: options.reviewedHeadSha,
|
|
3745
|
-
lastCommentedHeadSha: prior
|
|
3746
|
-
}
|
|
3907
|
+
...prior?.lastCommentedHeadSha ? { lastCommentedHeadSha: prior.lastCommentedHeadSha } : {}
|
|
3908
|
+
};
|
|
3909
|
+
nextFindings.set(id, record);
|
|
3747
3910
|
}
|
|
3748
3911
|
for (const prior of priorFindings.values()) {
|
|
3749
3912
|
if (nextFindings.has(prior.id)) continue;
|
|
3750
|
-
nextFindings.set(prior.id,
|
|
3913
|
+
nextFindings.set(prior.id, prior);
|
|
3751
3914
|
}
|
|
3752
|
-
return {
|
|
3915
|
+
return {
|
|
3753
3916
|
version: 1,
|
|
3754
3917
|
reviewedHeadSha: options.reviewedHeadSha,
|
|
3755
3918
|
selectedTasks: options.selectedTasks,
|
|
3756
3919
|
findings: cappedFindings([...nextFindings.values()], currentFindingIds)
|
|
3757
|
-
}
|
|
3920
|
+
};
|
|
3758
3921
|
}
|
|
3759
3922
|
function resolvePriorFindings(state, findingIds) {
|
|
3760
3923
|
const resolved = new Set(findingIds);
|
|
3761
|
-
return
|
|
3924
|
+
return {
|
|
3762
3925
|
...state,
|
|
3763
3926
|
findings: state.findings.map((finding) => ({
|
|
3764
3927
|
...finding,
|
|
3765
3928
|
status: resolved.has(finding.id) ? "resolved" : finding.status
|
|
3766
3929
|
}))
|
|
3767
|
-
}
|
|
3930
|
+
};
|
|
3768
3931
|
}
|
|
3769
3932
|
function priorReviewStateForSelectedTasks(state, selectedTasks) {
|
|
3770
3933
|
if (!state || state.selectedTasks.length !== selectedTasks.length || !state.selectedTasks.every((taskName, index) => taskName === selectedTasks[index])) return;
|
|
@@ -3818,26 +3981,26 @@ function renderVerifierResponseMarker(findingId, responseKey) {
|
|
|
3818
3981
|
return `<!-- ${verifierResponseMarkerPrefix} id=${findingId} key=${responseKey} -->`;
|
|
3819
3982
|
}
|
|
3820
3983
|
function extractInlineFindingMarkerRecords(commentBodies) {
|
|
3821
|
-
return commentBodies
|
|
3984
|
+
return extractMarkerRecords(commentBodies, inlineFindingMarkerPrefix);
|
|
3822
3985
|
}
|
|
3823
3986
|
function extractInlineFindingMarkers(commentBodies) {
|
|
3824
3987
|
return new Set(extractInlineFindingMarkerRecords(commentBodies).map((record) => record.marker));
|
|
3825
3988
|
}
|
|
3826
3989
|
function extractResolvedFindingMarkerRecords(commentBodies) {
|
|
3827
|
-
return commentBodies
|
|
3990
|
+
return extractMarkerRecords(commentBodies, resolvedFindingMarkerPrefix);
|
|
3828
3991
|
}
|
|
3829
3992
|
function applyResolvedFindingMarkers(state, commentBodies) {
|
|
3830
3993
|
const resolvedMarkers = new Set(extractResolvedFindingMarkerRecords(commentBodies).map((record) => `${record.id}:${record.head}`));
|
|
3831
|
-
return
|
|
3994
|
+
return {
|
|
3832
3995
|
...state,
|
|
3833
3996
|
findings: state.findings.map((finding) => ({
|
|
3834
3997
|
...finding,
|
|
3835
3998
|
status: finding.lastCommentedHeadSha && resolvedMarkers.has(`${finding.id}:${finding.lastCommentedHeadSha}`) ? "resolved" : finding.status
|
|
3836
3999
|
}))
|
|
3837
|
-
}
|
|
4000
|
+
};
|
|
3838
4001
|
}
|
|
3839
4002
|
function extractVerifierResponseMarkers(commentBodies) {
|
|
3840
|
-
return new Set(commentBodies
|
|
4003
|
+
return new Set(extractMarkerRecords(commentBodies, verifierResponseMarkerPrefix).map((record) => record.marker));
|
|
3841
4004
|
}
|
|
3842
4005
|
function isPiprThreadActionReplyBody(body) {
|
|
3843
4006
|
const parsed = parsePiprMarker(body ? firstNonEmptyLine(body) : void 0);
|
|
@@ -3849,13 +4012,17 @@ function applyInlineFindingMarkers(state, commentBodies) {
|
|
|
3849
4012
|
const [, , findingId, headSha] = marker.split(":");
|
|
3850
4013
|
if (findingId && headSha) markerById.set(findingId, headSha);
|
|
3851
4014
|
}
|
|
3852
|
-
return
|
|
4015
|
+
return {
|
|
3853
4016
|
...state,
|
|
3854
|
-
findings: state.findings.map((finding) =>
|
|
3855
|
-
|
|
3856
|
-
lastCommentedHeadSha:
|
|
3857
|
-
|
|
3858
|
-
|
|
4017
|
+
findings: state.findings.map((finding) => {
|
|
4018
|
+
const headSha = markerById.get(finding.id);
|
|
4019
|
+
const { lastCommentedHeadSha: _lastCommentedHeadSha, ...rest } = finding;
|
|
4020
|
+
return headSha ? {
|
|
4021
|
+
...rest,
|
|
4022
|
+
lastCommentedHeadSha: headSha
|
|
4023
|
+
} : rest;
|
|
4024
|
+
})
|
|
4025
|
+
};
|
|
3859
4026
|
}
|
|
3860
4027
|
function findingIdFor(finding, state) {
|
|
3861
4028
|
return (state ? matchFindingRecord(state, finding) : void 0)?.id ?? newFindingId(finding);
|
|
@@ -3904,8 +4071,11 @@ function parseFindingHeadMarker(comment, prefix) {
|
|
|
3904
4071
|
marker: prefix === inlineFindingMarkerPrefix ? inlineFindingMarker(id, head) : prefix === resolvedFindingMarkerPrefix ? `${resolvedFindingMarkerPrefix}:${id}:${head}` : `${verifierResponseMarkerPrefix}:${id}:${head}`
|
|
3905
4072
|
};
|
|
3906
4073
|
}
|
|
4074
|
+
function extractMarkerRecords(commentBodies, prefix) {
|
|
4075
|
+
return commentBodies.flatMap((body) => [parseFindingHeadMarker(firstNonEmptyLine(body), prefix)].filter((marker) => marker !== void 0));
|
|
4076
|
+
}
|
|
3907
4077
|
function encodeReviewState(state) {
|
|
3908
|
-
return Buffer$1.from(JSON.stringify(
|
|
4078
|
+
return Buffer$1.from(JSON.stringify(state)).toString("base64url");
|
|
3909
4079
|
}
|
|
3910
4080
|
function decodeReviewState(value) {
|
|
3911
4081
|
if (!value) return;
|
|
@@ -3939,143 +4109,8 @@ function hashParts(parts) {
|
|
|
3939
4109
|
return new Bun.CryptoHasher("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
|
|
3940
4110
|
}
|
|
3941
4111
|
//#endregion
|
|
3942
|
-
//#region src/action/entry-dispatch.ts
|
|
3943
|
-
const permissionOrder = [
|
|
3944
|
-
"read",
|
|
3945
|
-
"triage",
|
|
3946
|
-
"write",
|
|
3947
|
-
"maintain",
|
|
3948
|
-
"admin"
|
|
3949
|
-
];
|
|
3950
|
-
const changeRequestActions = [
|
|
3951
|
-
"opened",
|
|
3952
|
-
"updated",
|
|
3953
|
-
"reopened",
|
|
3954
|
-
"ready",
|
|
3955
|
-
"closed"
|
|
3956
|
-
];
|
|
3957
|
-
function dispatchRuntimeEntry(options) {
|
|
3958
|
-
if (options.kind === "change-request") return {
|
|
3959
|
-
kind: "change-request",
|
|
3960
|
-
tasks: selectRuntimeTasks({
|
|
3961
|
-
plan: options.plan,
|
|
3962
|
-
event: options.event,
|
|
3963
|
-
taskName: options.taskName
|
|
3964
|
-
}),
|
|
3965
|
-
taskName: options.taskName
|
|
3966
|
-
};
|
|
3967
|
-
return resolvePlanCommand(options.plan, options.line);
|
|
3968
|
-
}
|
|
3969
|
-
function selectRuntimeTasks(options) {
|
|
3970
|
-
if (options.taskName) return options.plan.tasks.filter((task) => task.name === options.taskName);
|
|
3971
|
-
return selectChangeRequestTasks(options.plan, options.event);
|
|
3972
|
-
}
|
|
3973
|
-
function selectLocalReviewTasks(plan) {
|
|
3974
|
-
return uniqBy(plan.changeRequestTriggers.map((trigger) => trigger.task), (task) => task.name).filter((task) => task.local !== false);
|
|
3975
|
-
}
|
|
3976
|
-
function selectChangeRequestTasks(plan, event) {
|
|
3977
|
-
if (!changeRequestActions.includes(event.action)) return [];
|
|
3978
|
-
const action = event.action;
|
|
3979
|
-
return uniqBy(plan.changeRequestTriggers.filter((trigger) => trigger.actions.includes(action)).map((trigger) => trigger.task), (task) => task.name);
|
|
3980
|
-
}
|
|
3981
|
-
function selectPlanCommand(plan, line) {
|
|
3982
|
-
let firstInvalid;
|
|
3983
|
-
for (const command of plan.commands) {
|
|
3984
|
-
const parsed = parseCommandPattern(command.pattern, line);
|
|
3985
|
-
if (!parsed.ok) {
|
|
3986
|
-
if (commandPatternPrefixMatches(command.pattern, line) && !firstInvalid) firstInvalid = {
|
|
3987
|
-
kind: "invalid",
|
|
3988
|
-
command,
|
|
3989
|
-
error: parsed.error
|
|
3990
|
-
};
|
|
3991
|
-
continue;
|
|
3992
|
-
}
|
|
3993
|
-
return {
|
|
3994
|
-
kind: "matched",
|
|
3995
|
-
command,
|
|
3996
|
-
commandName: command.pattern.replace(/^@pipr\s+/, "").split(/\s+/)[0] ?? command.pattern,
|
|
3997
|
-
line,
|
|
3998
|
-
arguments: parsed.value
|
|
3999
|
-
};
|
|
4000
|
-
}
|
|
4001
|
-
return firstInvalid;
|
|
4002
|
-
}
|
|
4003
|
-
function resolvePlanCommand(plan, line) {
|
|
4004
|
-
if (!line) return {
|
|
4005
|
-
kind: "ignored",
|
|
4006
|
-
reason: "comment did not contain a command line"
|
|
4007
|
-
};
|
|
4008
|
-
if (!isPiprCommandLine(line)) return {
|
|
4009
|
-
kind: "ignored",
|
|
4010
|
-
reason: "comment did not target pipr"
|
|
4011
|
-
};
|
|
4012
|
-
const selected = selectPlanCommand(plan, line);
|
|
4013
|
-
if (selected?.kind === "matched") return {
|
|
4014
|
-
kind: "matched",
|
|
4015
|
-
invocation: {
|
|
4016
|
-
taskName: selected.command.task.name,
|
|
4017
|
-
commandName: selected.commandName,
|
|
4018
|
-
requiredPermission: selected.command.permission,
|
|
4019
|
-
line: selected.line,
|
|
4020
|
-
pattern: selected.command.pattern,
|
|
4021
|
-
arguments: selected.arguments
|
|
4022
|
-
}
|
|
4023
|
-
};
|
|
4024
|
-
if (selected?.kind === "invalid") return {
|
|
4025
|
-
kind: "invalid",
|
|
4026
|
-
reason: selected.error,
|
|
4027
|
-
requiredPermission: selected.command.permission,
|
|
4028
|
-
body: renderPlanCommandHelp(plan, selected.error)
|
|
4029
|
-
};
|
|
4030
|
-
return {
|
|
4031
|
-
kind: "help",
|
|
4032
|
-
reason: `unknown pipr command '${line}'`,
|
|
4033
|
-
requiredPermission: "read",
|
|
4034
|
-
body: renderPlanCommandHelp(plan, `Unknown command: ${line}`)
|
|
4035
|
-
};
|
|
4036
|
-
}
|
|
4037
|
-
function parsePlanCommandInputs(plan, invocation) {
|
|
4038
|
-
const matchingCommand = plan.commands.find((candidate) => candidate.task.name === invocation.taskName && candidate.pattern === invocation.pattern) ?? plan.commands.find((candidate) => candidate.task.name === invocation.taskName);
|
|
4039
|
-
if (!matchingCommand) return {
|
|
4040
|
-
kind: "invalid",
|
|
4041
|
-
reason: `No command registered for task '${invocation.taskName}'`,
|
|
4042
|
-
requiredPermission: invocation.requiredPermission,
|
|
4043
|
-
body: renderPlanCommandHelp(plan)
|
|
4044
|
-
};
|
|
4045
|
-
try {
|
|
4046
|
-
return {
|
|
4047
|
-
kind: "matched",
|
|
4048
|
-
invocation: {
|
|
4049
|
-
...invocation,
|
|
4050
|
-
inputs: matchingCommand.parse ? matchingCommand.parse(invocation.arguments) : invocation.arguments
|
|
4051
|
-
}
|
|
4052
|
-
};
|
|
4053
|
-
} catch (error) {
|
|
4054
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
4055
|
-
return {
|
|
4056
|
-
kind: "invalid",
|
|
4057
|
-
reason,
|
|
4058
|
-
requiredPermission: invocation.requiredPermission,
|
|
4059
|
-
body: renderPlanCommandHelp(plan, reason)
|
|
4060
|
-
};
|
|
4061
|
-
}
|
|
4062
|
-
}
|
|
4063
|
-
function hasRequiredRepositoryPermission(actual, required) {
|
|
4064
|
-
if (actual === "none") return false;
|
|
4065
|
-
return permissionOrder.indexOf(actual) >= permissionOrder.indexOf(required);
|
|
4066
|
-
}
|
|
4067
|
-
function permissionDeniedHelp(plan, required) {
|
|
4068
|
-
return renderPlanCommandHelp(plan, `Permission denied: requires ${required}.`);
|
|
4069
|
-
}
|
|
4070
|
-
function renderPlanCommandHelp(plan, reason) {
|
|
4071
|
-
const lines = ["# pipr commands", ""];
|
|
4072
|
-
if (reason) lines.push(reason, "");
|
|
4073
|
-
for (const command of plan.commands) lines.push(`- ${command.pattern} (${command.permission})`);
|
|
4074
|
-
return lines.join("\n");
|
|
4075
|
-
}
|
|
4076
|
-
//#endregion
|
|
4077
4112
|
//#region src/review/comment.ts
|
|
4078
|
-
const runtimeVersion =
|
|
4113
|
+
const runtimeVersion = version;
|
|
4079
4114
|
const inlinePublicationItemSchema = z.strictObject({
|
|
4080
4115
|
finding: reviewFindingSchema,
|
|
4081
4116
|
range: commentableRangeSchema,
|
|
@@ -4146,7 +4181,7 @@ function buildPublicationPlan(options) {
|
|
|
4146
4181
|
findings: options.inlineItems.map((item) => item.finding),
|
|
4147
4182
|
reviewedHeadSha: options.metadata.reviewedHeadSha,
|
|
4148
4183
|
selectedTasks: options.metadata.selectedTasks
|
|
4149
|
-
})
|
|
4184
|
+
});
|
|
4150
4185
|
const cappedInlineItems = options.maxInlineComments === void 0 ? options.inlineItems : options.inlineItems.slice(0, options.maxInlineComments);
|
|
4151
4186
|
const metadata = publicationMetadataSchema.parse({
|
|
4152
4187
|
...options.metadata,
|
|
@@ -4227,7 +4262,7 @@ function buildCommentPublishingPlan(options) {
|
|
|
4227
4262
|
findings: options.validated.validFindings,
|
|
4228
4263
|
reviewedHeadSha: options.event.change.head.sha,
|
|
4229
4264
|
selectedTasks: options.metadata.selectedTasks
|
|
4230
|
-
})
|
|
4265
|
+
});
|
|
4231
4266
|
const inlineCommentDrafts = prepareInlinePublicationItems({
|
|
4232
4267
|
validated: options.validated,
|
|
4233
4268
|
manifest: options.manifest,
|
|
@@ -4469,7 +4504,7 @@ function boundedVerifierText(value) {
|
|
|
4469
4504
|
}
|
|
4470
4505
|
//#endregion
|
|
4471
4506
|
//#region src/review/task/task-output.ts
|
|
4472
|
-
const
|
|
4507
|
+
const agentInlineFindingsOutputSchema = z.custom((value) => z.looseObject({ inlineFindings: z.array(reviewFindingSchema) }).safeParse(value).success);
|
|
4473
4508
|
function createOutputState() {
|
|
4474
4509
|
return {
|
|
4475
4510
|
findings: [],
|
|
@@ -4491,16 +4526,19 @@ function mergeTaskOutputs(results) {
|
|
|
4491
4526
|
}
|
|
4492
4527
|
function mergeCommentContribution(merged, comment) {
|
|
4493
4528
|
if (!comment) return;
|
|
4494
|
-
|
|
4495
|
-
if (merged.commandResponse) throw new Error("ctx.comment(...) and ctx.command.reply(...) cannot both be called");
|
|
4529
|
+
assertOutputContributionAllowed(merged, "comment", comment.taskName, (existing, next) => `ctx.comment(...) may be called once per selected run; received comments from '${existing}' and '${next}'`);
|
|
4496
4530
|
merged.comment = comment;
|
|
4497
4531
|
}
|
|
4498
4532
|
function mergeCommandResponseContribution(merged, commandResponse) {
|
|
4499
4533
|
if (!commandResponse) return;
|
|
4500
|
-
|
|
4501
|
-
if (merged.comment) throw new Error("ctx.comment(...) and ctx.command.reply(...) cannot both be called");
|
|
4534
|
+
assertOutputContributionAllowed(merged, "commandResponse", commandResponse.taskName, (existing, next) => `ctx.command.reply(...) may be called once per selected run; received replies from '${existing}' and '${next}'`);
|
|
4502
4535
|
merged.commandResponse = commandResponse;
|
|
4503
4536
|
}
|
|
4537
|
+
function assertOutputContributionAllowed(state, kind, taskName, duplicateMessage) {
|
|
4538
|
+
const existing = kind === "comment" ? state.comment : state.commandResponse;
|
|
4539
|
+
if (existing) throw new Error(duplicateMessage(existing.taskName, taskName));
|
|
4540
|
+
if (kind === "comment" ? state.commandResponse : state.comment) throw new Error("ctx.comment(...) and ctx.command.reply(...) cannot both be called");
|
|
4541
|
+
}
|
|
4504
4542
|
function createCheckHandle(state) {
|
|
4505
4543
|
return {
|
|
4506
4544
|
pass(summary) {
|
|
@@ -4532,8 +4570,7 @@ function runtimeTaskCheckResult(taskName, check) {
|
|
|
4532
4570
|
};
|
|
4533
4571
|
}
|
|
4534
4572
|
function collectComment(state, value, taskName) {
|
|
4535
|
-
|
|
4536
|
-
if (state.comment) throw new Error(`ctx.comment(...) may be called once per selected run; '${taskName}' called it more than once`);
|
|
4573
|
+
assertOutputContributionAllowed(state, "comment", taskName, () => `ctx.comment(...) may be called once per selected run; '${taskName}' called it more than once`);
|
|
4537
4574
|
state.comment = {
|
|
4538
4575
|
taskName,
|
|
4539
4576
|
value
|
|
@@ -4542,8 +4579,7 @@ function collectComment(state, value, taskName) {
|
|
|
4542
4579
|
collectInlineFindings(state, value.inlineFindings);
|
|
4543
4580
|
}
|
|
4544
4581
|
function collectCommandResponse(state, value, taskName) {
|
|
4545
|
-
|
|
4546
|
-
if (state.commandResponse) throw new Error(`ctx.command.reply(...) may be called once per selected run; '${taskName}' called it more than once`);
|
|
4582
|
+
assertOutputContributionAllowed(state, "commandResponse", taskName, () => `ctx.command.reply(...) may be called once per selected run; '${taskName}' called it more than once`);
|
|
4547
4583
|
state.commandResponse = {
|
|
4548
4584
|
taskName,
|
|
4549
4585
|
value
|
|
@@ -4576,30 +4612,13 @@ function collectInlineFindings(state, findings) {
|
|
|
4576
4612
|
const arrayScope = state.findingScopes.get(findings);
|
|
4577
4613
|
state.findings.push(...findings.map((finding) => ({
|
|
4578
4614
|
finding,
|
|
4579
|
-
paths:
|
|
4615
|
+
paths: arrayScope
|
|
4580
4616
|
})));
|
|
4581
4617
|
}
|
|
4582
4618
|
function trackResultFindingScope(state, value, paths) {
|
|
4583
|
-
if (!hasInlineFindings(value)) return;
|
|
4584
4619
|
if (!paths) return;
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
if (!isReviewFindingLike(finding)) continue;
|
|
4588
|
-
markFindingScope(finding, paths);
|
|
4589
|
-
}
|
|
4590
|
-
}
|
|
4591
|
-
function markFindingScope(finding, paths) {
|
|
4592
|
-
Object.defineProperty(finding, findingScopeMarker, {
|
|
4593
|
-
value: paths,
|
|
4594
|
-
enumerable: true,
|
|
4595
|
-
configurable: true
|
|
4596
|
-
});
|
|
4597
|
-
}
|
|
4598
|
-
function hasInlineFindings(value) {
|
|
4599
|
-
return typeof value === "object" && value !== null && Array.isArray(value.inlineFindings);
|
|
4600
|
-
}
|
|
4601
|
-
function isReviewFindingLike(value) {
|
|
4602
|
-
return typeof value === "object" && value !== null && typeof value.body === "string" && typeof value.path === "string" && typeof value.rangeId === "string";
|
|
4620
|
+
const parsed = agentInlineFindingsOutputSchema.safeParse(value);
|
|
4621
|
+
if (parsed.success) state.findingScopes.set(parsed.data.inlineFindings, paths);
|
|
4603
4622
|
}
|
|
4604
4623
|
function collectedReview(output) {
|
|
4605
4624
|
return {
|
|
@@ -4731,7 +4750,7 @@ async function runTaskRuntime(options) {
|
|
|
4731
4750
|
});
|
|
4732
4751
|
if (commandResponse) return commandResponse;
|
|
4733
4752
|
assertReviewCommentOutput(output, options.commandInvocation !== void 0);
|
|
4734
|
-
const validated =
|
|
4753
|
+
const validated = validateReviewResult(collectedReview(output), diffManifest, {
|
|
4735
4754
|
expectedHeadSha: options.event.change.head.sha,
|
|
4736
4755
|
pathScopeForFinding: (_finding, index) => output.findings[index]?.paths
|
|
4737
4756
|
});
|
|
@@ -4883,7 +4902,7 @@ function createTaskContext(options) {
|
|
|
4883
4902
|
options.output.providerModels.push(...result.providerModels);
|
|
4884
4903
|
if (result.repairAttempted) options.output.repairAttempted = true;
|
|
4885
4904
|
trackResultFindingScope(options.output, result.value, runOptions?.paths);
|
|
4886
|
-
return result.value;
|
|
4905
|
+
return agentOutputForTaskContext(agent, result.value);
|
|
4887
4906
|
} },
|
|
4888
4907
|
review: { async prior() {
|
|
4889
4908
|
return priorReviewForTask(options.priorMainComment, options.priorReviewState);
|
|
@@ -4896,6 +4915,9 @@ function createTaskContext(options) {
|
|
|
4896
4915
|
};
|
|
4897
4916
|
return taskContext;
|
|
4898
4917
|
}
|
|
4918
|
+
function agentOutputForTaskContext(_agent, value) {
|
|
4919
|
+
return value;
|
|
4920
|
+
}
|
|
4899
4921
|
function resolveTaskSecret(secret, options) {
|
|
4900
4922
|
if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
|
|
4901
4923
|
const value = (options.env ?? process.env)[secret.name];
|
|
@@ -6243,105 +6265,31 @@ async function logPhase(log, name, run) {
|
|
|
6243
6265
|
throw error;
|
|
6244
6266
|
}
|
|
6245
6267
|
}
|
|
6246
|
-
function logEventContext(log, event) {
|
|
6247
|
-
log.notice("event", {
|
|
6248
|
-
platform: event.platform.id,
|
|
6249
|
-
eventName: event.eventName,
|
|
6250
|
-
action: event.action,
|
|
6251
|
-
rawAction: event.rawAction,
|
|
6252
|
-
repo: event.repository.slug,
|
|
6253
|
-
change: event.change.number,
|
|
6254
|
-
base: shortSha(event.change.base.sha),
|
|
6255
|
-
head: shortSha(event.change.head.sha),
|
|
6256
|
-
fork: event.change.isFork
|
|
6257
|
-
});
|
|
6258
|
-
}
|
|
6259
|
-
function logTrustedRuntime(log, runtime) {
|
|
6260
|
-
log.notice("trusted config", {
|
|
6261
|
-
source: runtime.settings.source,
|
|
6262
|
-
trustedConfigSha: shortSha(runtime.trustedConfigSha),
|
|
6263
|
-
trustedConfigHash: runtime.trustedConfigHash.slice(0, 12),
|
|
6264
|
-
providers: runtime.settings.config.providers.map((provider) => `${provider.id}:${provider.model}`).join(","),
|
|
6265
|
-
tasks: runtime.plan.tasks.length,
|
|
6266
|
-
commands: runtime.plan.commands.length
|
|
6267
|
-
});
|
|
6268
|
-
}
|
|
6269
|
-
function addProviderSecrets(log, config, env) {
|
|
6270
|
-
for (const provider of config.providers) log.addSecret((env ?? process.env)[provider.apiKeyEnv]);
|
|
6271
|
-
}
|
|
6272
|
-
//#endregion
|
|
6273
|
-
//#region src/action/git-project.ts
|
|
6274
|
-
async function loadRuntimeProjectFromGitCommit(options) {
|
|
6275
|
-
const configDir = resolveContainedConfigDir(options);
|
|
6276
|
-
const files = listConfigFilesAtCommit(options.rootDir, options.commitSha, configDir.gitPath);
|
|
6277
|
-
if (files.length === 0) throw new Error(`${configDir.configDir}/config.ts is required at base commit ${options.commitSha}`);
|
|
6278
|
-
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pipr-base-config-"));
|
|
6279
|
-
try {
|
|
6280
|
-
const hash = new Bun.CryptoHasher("sha256");
|
|
6281
|
-
for (const file of files) {
|
|
6282
|
-
assertRegularFile(file, options.commitSha);
|
|
6283
|
-
const relativePath = relativeGitPath(configDir.gitPath, file.path);
|
|
6284
|
-
const targetPath = path.join(tempRoot, configDir.relativeConfigDir, ...relativePath.split("/"));
|
|
6285
|
-
const contents = showFileAtCommit(options.rootDir, options.commitSha, file.path);
|
|
6286
|
-
hash.update(relativePath);
|
|
6287
|
-
hash.update("\0");
|
|
6288
|
-
hash.update(contents);
|
|
6289
|
-
hash.update("\0");
|
|
6290
|
-
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
6291
|
-
await Bun.write(targetPath, contents);
|
|
6292
|
-
}
|
|
6293
|
-
return {
|
|
6294
|
-
...await loadRuntimeProject({
|
|
6295
|
-
rootDir: tempRoot,
|
|
6296
|
-
configDir: configDir.relativeConfigDir,
|
|
6297
|
-
env: options.env,
|
|
6298
|
-
requireProviderEnv: false
|
|
6299
|
-
}),
|
|
6300
|
-
trustedConfigSha: options.commitSha,
|
|
6301
|
-
trustedConfigHash: hash.digest("hex")
|
|
6302
|
-
};
|
|
6303
|
-
} finally {
|
|
6304
|
-
await rm(tempRoot, {
|
|
6305
|
-
recursive: true,
|
|
6306
|
-
force: true
|
|
6307
|
-
});
|
|
6308
|
-
}
|
|
6309
|
-
}
|
|
6310
|
-
function listConfigFilesAtCommit(rootDir, commitSha, gitPath) {
|
|
6311
|
-
return runGit$1([
|
|
6312
|
-
"ls-tree",
|
|
6313
|
-
"-r",
|
|
6314
|
-
"-z",
|
|
6315
|
-
commitSha,
|
|
6316
|
-
"--",
|
|
6317
|
-
gitPath
|
|
6318
|
-
], rootDir).split("\0").filter(Boolean).map((entry) => parseGitTreeEntry(entry, commitSha));
|
|
6319
|
-
}
|
|
6320
|
-
function parseGitTreeEntry(entry, commitSha) {
|
|
6321
|
-
const separatorIndex = entry.indexOf(" ");
|
|
6322
|
-
if (separatorIndex === -1) throw new Error(`Could not parse git tree entry at ${commitSha}: ${entry}`);
|
|
6323
|
-
const metadata = entry.slice(0, separatorIndex);
|
|
6324
|
-
const filePath = entry.slice(separatorIndex + 1);
|
|
6325
|
-
const [mode = ""] = metadata.split(" ");
|
|
6326
|
-
if (!mode || !filePath) throw new Error(`Could not parse git tree entry at ${commitSha}: ${entry}`);
|
|
6327
|
-
return {
|
|
6328
|
-
mode,
|
|
6329
|
-
path: filePath
|
|
6330
|
-
};
|
|
6331
|
-
}
|
|
6332
|
-
function assertRegularFile(file, commitSha) {
|
|
6333
|
-
if (file.mode !== "100644" && file.mode !== "100755") throw new Error(`${file.path}: only regular config files are supported at ${commitSha}`);
|
|
6334
|
-
}
|
|
6335
|
-
function relativeGitPath(root, filePath) {
|
|
6336
|
-
const relative = root === "." ? filePath : path.posix.relative(root, filePath);
|
|
6337
|
-
assertRelativeGitPath(root, filePath, relative);
|
|
6338
|
-
return relative;
|
|
6339
|
-
}
|
|
6340
|
-
function assertRelativeGitPath(root, filePath, relative) {
|
|
6341
|
-
if (!relative || relative.startsWith("..") || path.posix.isAbsolute(relative)) throw new Error(`${filePath}: git path escaped configDir ${root}`);
|
|
6268
|
+
function logEventContext(log, event) {
|
|
6269
|
+
log.notice("event", {
|
|
6270
|
+
platform: event.platform.id,
|
|
6271
|
+
eventName: event.eventName,
|
|
6272
|
+
action: event.action,
|
|
6273
|
+
rawAction: event.rawAction,
|
|
6274
|
+
repo: event.repository.slug,
|
|
6275
|
+
change: event.change.number,
|
|
6276
|
+
base: shortSha(event.change.base.sha),
|
|
6277
|
+
head: shortSha(event.change.head.sha),
|
|
6278
|
+
fork: event.change.isFork
|
|
6279
|
+
});
|
|
6342
6280
|
}
|
|
6343
|
-
function
|
|
6344
|
-
|
|
6281
|
+
function logTrustedRuntime(log, runtime) {
|
|
6282
|
+
log.notice("trusted config", {
|
|
6283
|
+
source: runtime.settings.source,
|
|
6284
|
+
trustedConfigSha: shortSha(runtime.trustedConfigSha),
|
|
6285
|
+
trustedConfigHash: runtime.trustedConfigHash.slice(0, 12),
|
|
6286
|
+
providers: runtime.settings.config.providers.map((provider) => `${provider.id}:${provider.model}`).join(","),
|
|
6287
|
+
tasks: runtime.plan.tasks.length,
|
|
6288
|
+
commands: runtime.plan.commands.length
|
|
6289
|
+
});
|
|
6290
|
+
}
|
|
6291
|
+
function addProviderSecrets(log, config, env) {
|
|
6292
|
+
for (const provider of config.providers) log.addSecret((env ?? process.env)[provider.apiKeyEnv]);
|
|
6345
6293
|
}
|
|
6346
6294
|
//#endregion
|
|
6347
6295
|
//#region src/action/runtime-checks.ts
|
|
@@ -6601,146 +6549,286 @@ async function runTrustedReviewAndPublish(options) {
|
|
|
6601
6549
|
}
|
|
6602
6550
|
}
|
|
6603
6551
|
//#endregion
|
|
6604
|
-
//#region src/action/
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6552
|
+
//#region src/action/git-project.ts
|
|
6553
|
+
async function loadRuntimeProjectFromGitCommit(options) {
|
|
6554
|
+
const configDir = resolveContainedConfigDir(options);
|
|
6555
|
+
const files = listConfigFilesAtCommit(options.rootDir, options.commitSha, configDir.gitPath);
|
|
6556
|
+
if (files.length === 0) throw new Error(`${configDir.configDir}/config.ts is required at base commit ${options.commitSha}`);
|
|
6557
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pipr-base-config-"));
|
|
6558
|
+
try {
|
|
6559
|
+
const hash = new Bun.CryptoHasher("sha256");
|
|
6560
|
+
for (const file of files) {
|
|
6561
|
+
assertRegularFile(file, options.commitSha);
|
|
6562
|
+
const relativePath = relativeGitPath(configDir.gitPath, file.path);
|
|
6563
|
+
const targetPath = path.join(tempRoot, configDir.relativeConfigDir, ...relativePath.split("/"));
|
|
6564
|
+
const contents = showFileAtCommit(options.rootDir, options.commitSha, file.path);
|
|
6565
|
+
hash.update(relativePath);
|
|
6566
|
+
hash.update("\0");
|
|
6567
|
+
hash.update(contents);
|
|
6568
|
+
hash.update("\0");
|
|
6569
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
6570
|
+
await Bun.write(targetPath, contents);
|
|
6571
|
+
}
|
|
6572
|
+
return {
|
|
6573
|
+
...await loadRuntimeProject({
|
|
6574
|
+
rootDir: tempRoot,
|
|
6575
|
+
configDir: configDir.relativeConfigDir,
|
|
6576
|
+
env: options.env,
|
|
6577
|
+
requireProviderEnv: false
|
|
6578
|
+
}),
|
|
6579
|
+
trustedConfigSha: options.commitSha,
|
|
6580
|
+
trustedConfigHash: hash.digest("hex")
|
|
6581
|
+
};
|
|
6582
|
+
} finally {
|
|
6583
|
+
await rm(tempRoot, {
|
|
6584
|
+
recursive: true,
|
|
6585
|
+
force: true
|
|
6586
|
+
});
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
function listConfigFilesAtCommit(rootDir, commitSha, gitPath) {
|
|
6590
|
+
return runGit$1([
|
|
6591
|
+
"ls-tree",
|
|
6592
|
+
"-r",
|
|
6593
|
+
"-z",
|
|
6594
|
+
commitSha,
|
|
6595
|
+
"--",
|
|
6596
|
+
gitPath
|
|
6597
|
+
], rootDir).split("\0").filter(Boolean).map((entry) => parseGitTreeEntry(entry, commitSha)).filter((file) => !isIgnoredGitConfigPath(gitPath, file.path));
|
|
6598
|
+
}
|
|
6599
|
+
function isIgnoredGitConfigPath(gitPath, filePath) {
|
|
6600
|
+
const relative = gitPath === "." ? filePath : path.posix.relative(gitPath, filePath);
|
|
6601
|
+
return relative === "node_modules" || relative.startsWith("node_modules/");
|
|
6602
|
+
}
|
|
6603
|
+
function parseGitTreeEntry(entry, commitSha) {
|
|
6604
|
+
const separatorIndex = entry.indexOf(" ");
|
|
6605
|
+
if (separatorIndex === -1) throw new Error(`Could not parse git tree entry at ${commitSha}: ${entry}`);
|
|
6606
|
+
const metadata = entry.slice(0, separatorIndex);
|
|
6607
|
+
const filePath = entry.slice(separatorIndex + 1);
|
|
6608
|
+
const [mode = ""] = metadata.split(" ");
|
|
6609
|
+
if (!mode || !filePath) throw new Error(`Could not parse git tree entry at ${commitSha}: ${entry}`);
|
|
6610
|
+
return {
|
|
6611
|
+
mode,
|
|
6612
|
+
path: filePath
|
|
6613
|
+
};
|
|
6614
|
+
}
|
|
6615
|
+
function assertRegularFile(file, commitSha) {
|
|
6616
|
+
if (file.mode !== "100644" && file.mode !== "100755") throw new Error(`${file.path}: only regular config files are supported at ${commitSha}`);
|
|
6617
|
+
}
|
|
6618
|
+
function relativeGitPath(root, filePath) {
|
|
6619
|
+
const relative = root === "." ? filePath : path.posix.relative(root, filePath);
|
|
6620
|
+
assertRelativeGitPath(root, filePath, relative);
|
|
6621
|
+
return relative;
|
|
6622
|
+
}
|
|
6623
|
+
function assertRelativeGitPath(root, filePath, relative) {
|
|
6624
|
+
if (!relative || relative.startsWith("..") || path.posix.isAbsolute(relative)) throw new Error(`${filePath}: git path escaped configDir ${root}`);
|
|
6625
|
+
}
|
|
6626
|
+
function showFileAtCommit(rootDir, commitSha, filePath) {
|
|
6627
|
+
return runGit$1(["show", `${commitSha}:${filePath}`], rootDir);
|
|
6628
|
+
}
|
|
6629
|
+
//#endregion
|
|
6630
|
+
//#region src/action/trusted-runtime.ts
|
|
6631
|
+
async function loadTrustedRuntimeForEvent(options, event, log) {
|
|
6632
|
+
const trustedRuntime = await logPhase(log, "load trusted config", async () => loadRuntimeProjectFromGitCommit({
|
|
6608
6633
|
rootDir: options.rootDir,
|
|
6609
6634
|
configDir: options.configDir,
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6635
|
+
commitSha: event.change.base.sha,
|
|
6636
|
+
env: options.env
|
|
6637
|
+
}));
|
|
6638
|
+
logTrustedRuntime(log, trustedRuntime);
|
|
6639
|
+
return trustedRuntime;
|
|
6640
|
+
}
|
|
6641
|
+
async function prepareTrustedHeadCheckout(options, adapter, config, event, log) {
|
|
6642
|
+
addProviderSecrets(log, config, options.env);
|
|
6643
|
+
assertTrustedActionProviderEnv(options, config);
|
|
6644
|
+
await logPhase(log, "checkout head", async () => {
|
|
6645
|
+
adapter.workspace.ensureHeadCheckout({
|
|
6646
|
+
rootDir: options.rootDir,
|
|
6647
|
+
change: event
|
|
6648
|
+
});
|
|
6614
6649
|
});
|
|
6615
6650
|
}
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6651
|
+
//#endregion
|
|
6652
|
+
//#region src/action/command-entry.ts
|
|
6653
|
+
async function runIssueCommentActionCommand(options, adapter, log) {
|
|
6654
|
+
const prepared = await prepareIssueCommentCommand(options, adapter, log);
|
|
6655
|
+
if (prepared.kind === "ignored") {
|
|
6656
|
+
log.notice("action ignored", { reason: prepared.reason });
|
|
6657
|
+
return prepared;
|
|
6658
|
+
}
|
|
6659
|
+
return await dispatchIssueCommentCommand(options, adapter, prepared, log);
|
|
6622
6660
|
}
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6661
|
+
async function prepareIssueCommentCommand(options, adapter, log) {
|
|
6662
|
+
const comment = await logPhase(log, "parse issue comment", async () => adapter.events.resolveCommandComment({
|
|
6663
|
+
eventPath: options.eventPath,
|
|
6664
|
+
env: options.env ?? process.env,
|
|
6665
|
+
workspace: options.rootDir
|
|
6666
|
+
}));
|
|
6667
|
+
const runnable = runnableIssueCommentCommand(comment, options.dryRun);
|
|
6668
|
+
if (runnable.kind === "ignored") return runnable;
|
|
6669
|
+
const loaded = await logPhase(log, "load change request", async () => adapter.events.loadChangeRequest({
|
|
6670
|
+
repository: comment.repository,
|
|
6671
|
+
changeNumber: comment.changeNumber,
|
|
6672
|
+
workspace: comment.workspace,
|
|
6673
|
+
eventName: comment.eventName,
|
|
6674
|
+
action: comment.action,
|
|
6675
|
+
rawAction: comment.rawAction
|
|
6676
|
+
}));
|
|
6677
|
+
const event = parseChangeRequestEventContext({
|
|
6678
|
+
eventName: loaded.eventName ?? comment.eventName,
|
|
6679
|
+
action: loaded.action ?? comment.action,
|
|
6680
|
+
rawAction: loaded.rawAction ?? comment.rawAction,
|
|
6681
|
+
platform: { id: adapter.id },
|
|
6682
|
+
repository: loaded.repository,
|
|
6683
|
+
change: loaded.change,
|
|
6684
|
+
workspace: loaded.workspace ?? comment.workspace
|
|
6685
|
+
});
|
|
6686
|
+
logEventContext(log, event);
|
|
6687
|
+
const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
|
|
6688
|
+
const resolution = resolvePlanCommand(trustedRuntime.plan, runnable.line);
|
|
6689
|
+
if (resolution.kind === "ignored") return {
|
|
6690
|
+
kind: "ignored",
|
|
6691
|
+
reason: resolution.reason
|
|
6692
|
+
};
|
|
6693
|
+
return {
|
|
6694
|
+
kind: "prepared",
|
|
6695
|
+
comment,
|
|
6696
|
+
line: runnable.line,
|
|
6697
|
+
event,
|
|
6698
|
+
trustedRuntime,
|
|
6699
|
+
resolution
|
|
6700
|
+
};
|
|
6701
|
+
}
|
|
6702
|
+
function runnableIssueCommentCommand(comment, dryRun) {
|
|
6703
|
+
if (!comment.isChangeRequest) return {
|
|
6704
|
+
kind: "ignored",
|
|
6705
|
+
reason: "issue_comment did not target a pull request"
|
|
6706
|
+
};
|
|
6707
|
+
if (comment.action !== "created") return {
|
|
6708
|
+
kind: "ignored",
|
|
6709
|
+
reason: `issue_comment action '${comment.action}' is not supported`
|
|
6710
|
+
};
|
|
6711
|
+
const line = firstNonEmptyLine(comment.body);
|
|
6712
|
+
if (!line || !isPiprCommandLine(line)) return {
|
|
6713
|
+
kind: "ignored",
|
|
6714
|
+
reason: "issue_comment did not target pipr"
|
|
6715
|
+
};
|
|
6716
|
+
return dryRun ? {
|
|
6717
|
+
kind: "ignored",
|
|
6718
|
+
reason: "PIPR_DRY_RUN=1; command dispatch skipped"
|
|
6719
|
+
} : {
|
|
6720
|
+
kind: "runnable",
|
|
6721
|
+
line
|
|
6722
|
+
};
|
|
6723
|
+
}
|
|
6724
|
+
async function dispatchIssueCommentCommand(options, adapter, prepared, log) {
|
|
6725
|
+
const requiredPermission = prepared.resolution.kind === "matched" ? prepared.resolution.invocation.requiredPermission : prepared.resolution.requiredPermission;
|
|
6726
|
+
const permission = await logPhase(log, "check command permission", async () => adapter.permissions.getRepositoryPermission({
|
|
6727
|
+
repository: prepared.comment.repository,
|
|
6728
|
+
actor: prepared.comment.actor
|
|
6729
|
+
}));
|
|
6730
|
+
log.notice("command dispatch", {
|
|
6731
|
+
resolution: prepared.resolution.kind,
|
|
6732
|
+
requiredPermission,
|
|
6733
|
+
actualPermission: permission
|
|
6734
|
+
});
|
|
6735
|
+
if (!hasRequiredRepositoryPermission(permission, requiredPermission)) return {
|
|
6736
|
+
kind: "command-help",
|
|
6737
|
+
event: prepared.event,
|
|
6738
|
+
configSource: prepared.trustedRuntime.settings.source,
|
|
6739
|
+
body: permissionDeniedHelp(prepared.trustedRuntime.plan, requiredPermission),
|
|
6740
|
+
reason: `permission denied for '${prepared.line}'`
|
|
6741
|
+
};
|
|
6742
|
+
if (prepared.resolution.kind === "help" || prepared.resolution.kind === "invalid") return {
|
|
6743
|
+
kind: "command-help",
|
|
6744
|
+
event: prepared.event,
|
|
6745
|
+
configSource: prepared.trustedRuntime.settings.source,
|
|
6746
|
+
body: prepared.resolution.body,
|
|
6747
|
+
reason: prepared.resolution.reason
|
|
6748
|
+
};
|
|
6749
|
+
const parsedResolution = parsePlanCommandInputs(prepared.trustedRuntime.plan, prepared.resolution.invocation);
|
|
6750
|
+
if (parsedResolution.kind === "invalid") return {
|
|
6751
|
+
kind: "command-help",
|
|
6752
|
+
event: prepared.event,
|
|
6753
|
+
configSource: prepared.trustedRuntime.settings.source,
|
|
6754
|
+
body: parsedResolution.body,
|
|
6755
|
+
reason: parsedResolution.reason
|
|
6756
|
+
};
|
|
6757
|
+
if (parsedResolution.kind !== "matched") return {
|
|
6758
|
+
kind: "ignored",
|
|
6759
|
+
reason: "command dispatch did not resolve to a runnable task"
|
|
6760
|
+
};
|
|
6761
|
+
await prepareTrustedHeadCheckout(options, adapter, prepared.trustedRuntime.settings.config, prepared.event, log);
|
|
6762
|
+
const dispatch = dispatchRuntimeEntry({
|
|
6763
|
+
kind: "change-request",
|
|
6764
|
+
plan: prepared.trustedRuntime.plan,
|
|
6765
|
+
event: prepared.event,
|
|
6766
|
+
taskName: parsedResolution.invocation.taskName
|
|
6767
|
+
});
|
|
6768
|
+
return await issueCommentCommandResult({
|
|
6769
|
+
adapter,
|
|
6770
|
+
completed: await runTrustedReviewAndPublish({
|
|
6771
|
+
options,
|
|
6772
|
+
adapter,
|
|
6773
|
+
trustedRuntime: prepared.trustedRuntime,
|
|
6774
|
+
event: prepared.event,
|
|
6775
|
+
taskName: parsedResolution.invocation.taskName,
|
|
6776
|
+
taskInput: parsedResolution.invocation.inputs,
|
|
6777
|
+
selectedTasks: dispatch.kind === "change-request" ? dispatch.tasks : [],
|
|
6778
|
+
commandInvocation: {
|
|
6779
|
+
name: parsedResolution.invocation.commandName,
|
|
6780
|
+
line: parsedResolution.invocation.line,
|
|
6781
|
+
arguments: parsedResolution.invocation.arguments
|
|
6782
|
+
},
|
|
6783
|
+
log
|
|
6784
|
+
}),
|
|
6785
|
+
event: prepared.event,
|
|
6786
|
+
commandName: parsedResolution.invocation.commandName,
|
|
6787
|
+
sourceCommentId: prepared.comment.commentId,
|
|
6788
|
+
configSource: prepared.trustedRuntime.settings.source
|
|
6628
6789
|
});
|
|
6629
|
-
return inspectRuntimePlan(runtime.plan, runtime.settings.source);
|
|
6630
6790
|
}
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
GITHUB_EVENT_NAME: "pull_request"
|
|
6643
|
-
},
|
|
6644
|
-
workspace: options.rootDir
|
|
6791
|
+
async function issueCommentCommandResult(options) {
|
|
6792
|
+
if (options.completed.kind === "skipped") return {
|
|
6793
|
+
kind: "ignored",
|
|
6794
|
+
reason: options.completed.reason
|
|
6795
|
+
};
|
|
6796
|
+
if (options.completed.kind === "command-response") return await publishCommandResponseActionResult({
|
|
6797
|
+
adapter: options.adapter,
|
|
6798
|
+
completed: options.completed,
|
|
6799
|
+
event: options.event,
|
|
6800
|
+
sourceCommentId: options.sourceCommentId,
|
|
6801
|
+
configSource: options.configSource
|
|
6645
6802
|
});
|
|
6646
6803
|
return {
|
|
6647
|
-
|
|
6648
|
-
event
|
|
6804
|
+
kind: "review",
|
|
6805
|
+
event: options.event,
|
|
6806
|
+
command: options.commandName,
|
|
6807
|
+
configSource: options.configSource,
|
|
6808
|
+
review: options.completed.review,
|
|
6809
|
+
publication: options.completed.publication
|
|
6649
6810
|
};
|
|
6650
6811
|
}
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
configDir: options.configDir,
|
|
6660
|
-
base: options.baseSha.slice(0, 12),
|
|
6661
|
-
head: options.headSha?.slice(0, 12)
|
|
6662
|
-
});
|
|
6663
|
-
const runtime = await loadRuntimeProject({
|
|
6664
|
-
...options,
|
|
6665
|
-
requireProviderEnv: true
|
|
6666
|
-
});
|
|
6667
|
-
log?.notice("local config loaded", {
|
|
6668
|
-
source: runtime.settings.source,
|
|
6669
|
-
providers: runtime.settings.config.providers.map((provider) => `${provider.id}:${provider.model}`).join(","),
|
|
6670
|
-
tasks: runtime.plan.tasks.length,
|
|
6671
|
-
commands: runtime.plan.commands.length
|
|
6672
|
-
});
|
|
6673
|
-
const selectedTasks = selectLocalReviewTasks(runtime.plan);
|
|
6674
|
-
const includeWorkingTree = options.headSha === void 0;
|
|
6675
|
-
const headSha = options.headSha ?? runGit$1(["rev-parse", "HEAD"], options.rootDir).trim();
|
|
6676
|
-
const event = parseChangeRequestEventContext({ ...createLocalChangeRequestEvent({
|
|
6677
|
-
rootDir: options.rootDir,
|
|
6678
|
-
baseSha: options.baseSha,
|
|
6679
|
-
headSha
|
|
6680
|
-
}) });
|
|
6681
|
-
if (log) {
|
|
6682
|
-
logEventContext(log, event);
|
|
6683
|
-
log.notice("local dispatch", {
|
|
6684
|
-
selectedTasks: selectedTasks.map((task) => task.name),
|
|
6685
|
-
skippedLocalTasks: runtime.plan.tasks.filter((task) => task.local === false).map((task) => task.name),
|
|
6686
|
-
diffTarget: includeWorkingTree ? "working-tree" : "head-ref"
|
|
6687
|
-
});
|
|
6688
|
-
}
|
|
6689
|
-
const result = await runTaskRuntime({
|
|
6690
|
-
workspace: options.rootDir,
|
|
6691
|
-
config: runtime.settings.config,
|
|
6692
|
-
event,
|
|
6693
|
-
env: options.env,
|
|
6694
|
-
plan: runtime.plan,
|
|
6695
|
-
selectedTasks,
|
|
6696
|
-
emptyTasksReason: "No change-request tasks are configured for local review",
|
|
6697
|
-
piExecutable: options.piExecutable,
|
|
6698
|
-
diffManifestBuilder: includeWorkingTree ? (diffOptions) => buildDiffManifest({
|
|
6699
|
-
...diffOptions,
|
|
6700
|
-
includeWorkingTree: true
|
|
6701
|
-
}) : void 0,
|
|
6702
|
-
log,
|
|
6703
|
-
taskLog: options.taskLog
|
|
6704
|
-
});
|
|
6705
|
-
if (result.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
|
|
6706
|
-
log?.notice("local review complete", {
|
|
6707
|
-
kind: result.kind,
|
|
6708
|
-
taskChecks: result.taskChecks.length,
|
|
6709
|
-
validFindings: result.kind === "review" ? result.validated.validFindings.length : void 0,
|
|
6710
|
-
droppedFindings: result.kind === "review" ? result.validated.droppedFindings.length : void 0,
|
|
6711
|
-
inlineDrafts: result.kind === "review" ? result.inlineCommentDrafts.length : void 0
|
|
6712
|
-
});
|
|
6713
|
-
return result;
|
|
6714
|
-
}
|
|
6715
|
-
/** Runs the GitHub Action workflow for pull request and issue-comment events. */
|
|
6716
|
-
async function runActionCommand(options) {
|
|
6717
|
-
return await runActionCommandWithDependencies(options);
|
|
6718
|
-
}
|
|
6719
|
-
async function runActionCommandWithDependencies(options) {
|
|
6720
|
-
const log = createRuntimeActionLog({
|
|
6721
|
-
logSink: options.logSink,
|
|
6722
|
-
env: options.env
|
|
6723
|
-
});
|
|
6724
|
-
return await log.group("pipr action", async () => {
|
|
6725
|
-
const eventName = (options.env ?? process.env).GITHUB_EVENT_NAME ?? "pull_request";
|
|
6726
|
-
log.notice("action start", {
|
|
6727
|
-
eventName,
|
|
6728
|
-
dryRun: options.dryRun,
|
|
6729
|
-
root: options.rootDir,
|
|
6730
|
-
configDir: options.configDir
|
|
6731
|
-
});
|
|
6732
|
-
const adapter = createActionHostAdapter(options);
|
|
6733
|
-
await logPhase(log, "workspace", async () => {
|
|
6734
|
-
adapter.workspace.ensureWorkspaceSafeDirectory?.({
|
|
6735
|
-
rootDir: options.rootDir,
|
|
6736
|
-
env: options.env
|
|
6737
|
-
});
|
|
6738
|
-
});
|
|
6739
|
-
if (eventName === "issue_comment") return await runIssueCommentActionCommand(options, adapter, log);
|
|
6740
|
-
if (eventName === "pull_request_review_comment") return await runReviewCommentReplyActionCommand(options, adapter, log);
|
|
6741
|
-
return await runPullRequestActionCommand(options, adapter, log);
|
|
6812
|
+
async function publishCommandResponseActionResult(options) {
|
|
6813
|
+
const publishCommandResponse = options.adapter.publication?.publishCommandResponse;
|
|
6814
|
+
if (!publishCommandResponse) throw new Error("command response publication is not available for this code host");
|
|
6815
|
+
const publication = await publishCommandResponse({
|
|
6816
|
+
change: options.event,
|
|
6817
|
+
sourceCommentId: options.sourceCommentId,
|
|
6818
|
+
commandName: options.completed.response.commandName,
|
|
6819
|
+
body: options.completed.response.body
|
|
6742
6820
|
});
|
|
6821
|
+
return {
|
|
6822
|
+
kind: "command-response",
|
|
6823
|
+
event: options.event,
|
|
6824
|
+
command: options.completed.response.commandName,
|
|
6825
|
+
configSource: options.configSource,
|
|
6826
|
+
response: { body: options.completed.response.body },
|
|
6827
|
+
publication
|
|
6828
|
+
};
|
|
6743
6829
|
}
|
|
6830
|
+
//#endregion
|
|
6831
|
+
//#region src/action/pull-request-entry.ts
|
|
6744
6832
|
async function runPullRequestActionCommand(options, adapter, log) {
|
|
6745
6833
|
const event = await logPhase(log, "parse event", async () => adapter.events.parseEvent({
|
|
6746
6834
|
eventPath: options.eventPath,
|
|
@@ -6789,34 +6877,8 @@ async function runPullRequestActionCommand(options, adapter, log) {
|
|
|
6789
6877
|
publication: completed.publication
|
|
6790
6878
|
};
|
|
6791
6879
|
}
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
rootDir: options.rootDir,
|
|
6795
|
-
configDir: options.configDir,
|
|
6796
|
-
commitSha: event.change.base.sha,
|
|
6797
|
-
env: options.env
|
|
6798
|
-
}));
|
|
6799
|
-
logTrustedRuntime(log, trustedRuntime);
|
|
6800
|
-
return trustedRuntime;
|
|
6801
|
-
}
|
|
6802
|
-
async function prepareTrustedHeadCheckout(options, adapter, config, event, log) {
|
|
6803
|
-
addProviderSecrets(log, config, options.env);
|
|
6804
|
-
assertTrustedActionProviderEnv(options, config);
|
|
6805
|
-
await logPhase(log, "checkout head", async () => {
|
|
6806
|
-
adapter.workspace.ensureHeadCheckout({
|
|
6807
|
-
rootDir: options.rootDir,
|
|
6808
|
-
change: event
|
|
6809
|
-
});
|
|
6810
|
-
});
|
|
6811
|
-
}
|
|
6812
|
-
async function runIssueCommentActionCommand(options, adapter, log) {
|
|
6813
|
-
const prepared = await prepareIssueCommentCommand(options, adapter, log);
|
|
6814
|
-
if (prepared.kind === "ignored") {
|
|
6815
|
-
log.notice("action ignored", { reason: prepared.reason });
|
|
6816
|
-
return prepared;
|
|
6817
|
-
}
|
|
6818
|
-
return await dispatchIssueCommentCommand(options, adapter, prepared, log);
|
|
6819
|
-
}
|
|
6880
|
+
//#endregion
|
|
6881
|
+
//#region src/action/verifier-entry.ts
|
|
6820
6882
|
async function runReviewCommentReplyActionCommand(options, adapter, log) {
|
|
6821
6883
|
const capabilities = reviewCommentReplyDispatchCapabilities(options, adapter);
|
|
6822
6884
|
if (capabilities.kind === "ignored") {
|
|
@@ -6966,201 +7028,173 @@ async function runReviewCommentVerifier(options, adapter, prepared, log) {
|
|
|
6966
7028
|
function runnableReviewCommentReply(reply) {
|
|
6967
7029
|
if (reply.action !== "created") return {
|
|
6968
7030
|
kind: "ignored",
|
|
6969
|
-
reason: `review comment action '${reply.action}' is not supported`
|
|
6970
|
-
};
|
|
6971
|
-
if (!reply.parentCommentId) return {
|
|
6972
|
-
kind: "ignored",
|
|
6973
|
-
reason: "review comment was not a reply"
|
|
6974
|
-
};
|
|
6975
|
-
if (reply.actor === "github-actions[bot]") return {
|
|
6976
|
-
kind: "ignored",
|
|
6977
|
-
reason: "review comment reply was authored by pipr"
|
|
6978
|
-
};
|
|
6979
|
-
if (isPiprThreadActionReplyBody(reply.body)) return {
|
|
6980
|
-
kind: "ignored",
|
|
6981
|
-
reason: "review comment reply was authored by pipr"
|
|
6982
|
-
};
|
|
6983
|
-
return { kind: "runnable" };
|
|
6984
|
-
}
|
|
6985
|
-
async function verifierActorAllowed(adapter, event, reply, config) {
|
|
6986
|
-
const allowed = config.publication.autoResolve.userReplies.allowedActors;
|
|
6987
|
-
if (allowed === "any") return true;
|
|
6988
|
-
if (allowed === "author-or-write" && event.change.author?.login === reply.actor) return true;
|
|
6989
|
-
return hasRequiredRepositoryPermission(await adapter.permissions.getRepositoryPermission({
|
|
6990
|
-
repository: event.repository,
|
|
6991
|
-
actor: reply.actor
|
|
6992
|
-
}), "write");
|
|
6993
|
-
}
|
|
6994
|
-
async function prepareIssueCommentCommand(options, adapter, log) {
|
|
6995
|
-
const comment = await logPhase(log, "parse issue comment", async () => adapter.events.resolveCommandComment({
|
|
6996
|
-
eventPath: options.eventPath,
|
|
6997
|
-
env: options.env ?? process.env,
|
|
6998
|
-
workspace: options.rootDir
|
|
6999
|
-
}));
|
|
7000
|
-
const runnable = runnableIssueCommentCommand(comment, options.dryRun);
|
|
7001
|
-
if (runnable.kind === "ignored") return runnable;
|
|
7002
|
-
const loaded = await logPhase(log, "load change request", async () => adapter.events.loadChangeRequest({
|
|
7003
|
-
repository: comment.repository,
|
|
7004
|
-
changeNumber: comment.changeNumber,
|
|
7005
|
-
workspace: comment.workspace,
|
|
7006
|
-
eventName: comment.eventName,
|
|
7007
|
-
action: comment.action,
|
|
7008
|
-
rawAction: comment.rawAction
|
|
7009
|
-
}));
|
|
7010
|
-
const event = parseChangeRequestEventContext({
|
|
7011
|
-
eventName: loaded.eventName ?? comment.eventName,
|
|
7012
|
-
action: loaded.action ?? comment.action,
|
|
7013
|
-
rawAction: loaded.rawAction ?? comment.rawAction,
|
|
7014
|
-
platform: { id: adapter.id },
|
|
7015
|
-
repository: loaded.repository,
|
|
7016
|
-
change: loaded.change,
|
|
7017
|
-
workspace: loaded.workspace ?? comment.workspace
|
|
7018
|
-
});
|
|
7019
|
-
logEventContext(log, event);
|
|
7020
|
-
const trustedRuntime = await loadTrustedRuntimeForEvent(options, event, log);
|
|
7021
|
-
const resolution = resolvePlanCommand(trustedRuntime.plan, runnable.line);
|
|
7022
|
-
if (resolution.kind === "ignored") return {
|
|
7023
|
-
kind: "ignored",
|
|
7024
|
-
reason: resolution.reason
|
|
7025
|
-
};
|
|
7026
|
-
return {
|
|
7027
|
-
kind: "prepared",
|
|
7028
|
-
comment,
|
|
7029
|
-
line: runnable.line,
|
|
7030
|
-
event,
|
|
7031
|
-
trustedRuntime,
|
|
7032
|
-
resolution
|
|
7033
|
-
};
|
|
7034
|
-
}
|
|
7035
|
-
function runnableIssueCommentCommand(comment, dryRun) {
|
|
7036
|
-
if (!comment.isChangeRequest) return {
|
|
7037
|
-
kind: "ignored",
|
|
7038
|
-
reason: "issue_comment did not target a pull request"
|
|
7031
|
+
reason: `review comment action '${reply.action}' is not supported`
|
|
7039
7032
|
};
|
|
7040
|
-
if (
|
|
7033
|
+
if (!reply.parentCommentId) return {
|
|
7041
7034
|
kind: "ignored",
|
|
7042
|
-
reason:
|
|
7035
|
+
reason: "review comment was not a reply"
|
|
7043
7036
|
};
|
|
7044
|
-
|
|
7045
|
-
if (!line || !isPiprCommandLine(line)) return {
|
|
7037
|
+
if (reply.actor === "github-actions[bot]") return {
|
|
7046
7038
|
kind: "ignored",
|
|
7047
|
-
reason: "
|
|
7039
|
+
reason: "review comment reply was authored by pipr"
|
|
7048
7040
|
};
|
|
7049
|
-
|
|
7041
|
+
if (isPiprThreadActionReplyBody(reply.body)) return {
|
|
7050
7042
|
kind: "ignored",
|
|
7051
|
-
reason: "
|
|
7052
|
-
} : {
|
|
7053
|
-
kind: "runnable",
|
|
7054
|
-
line
|
|
7043
|
+
reason: "review comment reply was authored by pipr"
|
|
7055
7044
|
};
|
|
7045
|
+
return { kind: "runnable" };
|
|
7056
7046
|
}
|
|
7057
|
-
async function
|
|
7058
|
-
const
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
event: prepared.event,
|
|
7078
|
-
configSource: prepared.trustedRuntime.settings.source,
|
|
7079
|
-
body: prepared.resolution.body,
|
|
7080
|
-
reason: prepared.resolution.reason
|
|
7081
|
-
};
|
|
7082
|
-
const parsedResolution = parsePlanCommandInputs(prepared.trustedRuntime.plan, prepared.resolution.invocation);
|
|
7083
|
-
if (parsedResolution.kind === "invalid") return {
|
|
7084
|
-
kind: "command-help",
|
|
7085
|
-
event: prepared.event,
|
|
7086
|
-
configSource: prepared.trustedRuntime.settings.source,
|
|
7087
|
-
body: parsedResolution.body,
|
|
7088
|
-
reason: parsedResolution.reason
|
|
7089
|
-
};
|
|
7090
|
-
if (parsedResolution.kind !== "matched") return {
|
|
7091
|
-
kind: "ignored",
|
|
7092
|
-
reason: "command dispatch did not resolve to a runnable task"
|
|
7093
|
-
};
|
|
7094
|
-
await prepareTrustedHeadCheckout(options, adapter, prepared.trustedRuntime.settings.config, prepared.event, log);
|
|
7095
|
-
const dispatch = dispatchRuntimeEntry({
|
|
7096
|
-
kind: "change-request",
|
|
7097
|
-
plan: prepared.trustedRuntime.plan,
|
|
7098
|
-
event: prepared.event,
|
|
7099
|
-
taskName: parsedResolution.invocation.taskName
|
|
7047
|
+
async function verifierActorAllowed(adapter, event, reply, config) {
|
|
7048
|
+
const allowed = config.publication.autoResolve.userReplies.allowedActors;
|
|
7049
|
+
if (allowed === "any") return true;
|
|
7050
|
+
if (allowed === "author-or-write" && event.change.author?.login === reply.actor) return true;
|
|
7051
|
+
return hasRequiredRepositoryPermission(await adapter.permissions.getRepositoryPermission({
|
|
7052
|
+
repository: event.repository,
|
|
7053
|
+
actor: reply.actor
|
|
7054
|
+
}), "write");
|
|
7055
|
+
}
|
|
7056
|
+
//#endregion
|
|
7057
|
+
//#region src/action/commands.ts
|
|
7058
|
+
/** Initializes the official minimal `.pipr` project files. */
|
|
7059
|
+
async function runInitCommand(options) {
|
|
7060
|
+
return await initOfficialMinimalProject({
|
|
7061
|
+
rootDir: options.rootDir,
|
|
7062
|
+
configDir: options.configDir,
|
|
7063
|
+
force: options.force,
|
|
7064
|
+
adapters: options.adapters,
|
|
7065
|
+
recipe: options.recipe,
|
|
7066
|
+
minimal: options.minimal
|
|
7100
7067
|
});
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
arguments: parsedResolution.invocation.arguments
|
|
7115
|
-
},
|
|
7116
|
-
log
|
|
7117
|
-
}),
|
|
7118
|
-
event: prepared.event,
|
|
7119
|
-
commandName: parsedResolution.invocation.commandName,
|
|
7120
|
-
sourceCommentId: prepared.comment.commentId,
|
|
7121
|
-
configSource: prepared.trustedRuntime.settings.source
|
|
7068
|
+
}
|
|
7069
|
+
/** Loads and validates the runtime project configuration. */
|
|
7070
|
+
async function runValidateCommand(options) {
|
|
7071
|
+
return (await validateProject({
|
|
7072
|
+
...options,
|
|
7073
|
+
requireProviderEnv: options.requireProviderEnv ?? false
|
|
7074
|
+
})).settings;
|
|
7075
|
+
}
|
|
7076
|
+
/** Returns an inspectable summary of the configured runtime plan. */
|
|
7077
|
+
async function runInspectCommand(options) {
|
|
7078
|
+
const runtime = await loadRuntimeProject({
|
|
7079
|
+
...options,
|
|
7080
|
+
requireProviderEnv: false
|
|
7122
7081
|
});
|
|
7082
|
+
return inspectRuntimePlan(runtime.plan, runtime.settings.source);
|
|
7123
7083
|
}
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7084
|
+
/** Loads the runtime config and pull request event without running review publication. */
|
|
7085
|
+
async function runDryRunCommand(options) {
|
|
7086
|
+
const runtime = await loadRuntimeProject({
|
|
7087
|
+
...options,
|
|
7088
|
+
requireProviderEnv: false
|
|
7089
|
+
});
|
|
7090
|
+
const event = await createActionHostAdapter(options).events.parseEvent({
|
|
7091
|
+
eventPath: options.eventPath,
|
|
7092
|
+
env: {
|
|
7093
|
+
...options.env,
|
|
7094
|
+
GITHUB_WORKSPACE: options.rootDir,
|
|
7095
|
+
GITHUB_EVENT_NAME: "pull_request"
|
|
7096
|
+
},
|
|
7097
|
+
workspace: options.rootDir
|
|
7135
7098
|
});
|
|
7136
7099
|
return {
|
|
7137
|
-
|
|
7138
|
-
event
|
|
7139
|
-
command: options.commandName,
|
|
7140
|
-
configSource: options.configSource,
|
|
7141
|
-
review: options.completed.review,
|
|
7142
|
-
publication: options.completed.publication
|
|
7100
|
+
configSource: runtime.settings.source,
|
|
7101
|
+
event
|
|
7143
7102
|
};
|
|
7144
7103
|
}
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7104
|
+
/** Runs configured change-request tasks against local Git base and head revisions. */
|
|
7105
|
+
async function runLocalReviewCommand(options) {
|
|
7106
|
+
const log = options.logSink ? createRuntimeActionLog({
|
|
7107
|
+
logSink: options.logSink,
|
|
7108
|
+
env: options.env
|
|
7109
|
+
}) : void 0;
|
|
7110
|
+
log?.notice("local review start", {
|
|
7111
|
+
root: options.rootDir,
|
|
7112
|
+
configDir: options.configDir,
|
|
7113
|
+
base: options.baseSha.slice(0, 12),
|
|
7114
|
+
head: options.headSha?.slice(0, 12)
|
|
7115
|
+
});
|
|
7116
|
+
const runtime = await loadRuntimeProject({
|
|
7117
|
+
...options,
|
|
7118
|
+
requireProviderEnv: true
|
|
7119
|
+
});
|
|
7120
|
+
log?.notice("local config loaded", {
|
|
7121
|
+
source: runtime.settings.source,
|
|
7122
|
+
providers: runtime.settings.config.providers.map((provider) => `${provider.id}:${provider.model}`).join(","),
|
|
7123
|
+
tasks: runtime.plan.tasks.length,
|
|
7124
|
+
commands: runtime.plan.commands.length
|
|
7125
|
+
});
|
|
7126
|
+
const selectedTasks = selectLocalReviewTasks(runtime.plan);
|
|
7127
|
+
const includeWorkingTree = options.headSha === void 0;
|
|
7128
|
+
const headSha = options.headSha ?? runGit$1(["rev-parse", "HEAD"], options.rootDir).trim();
|
|
7129
|
+
const event = parseChangeRequestEventContext({ ...createLocalChangeRequestEvent({
|
|
7130
|
+
rootDir: options.rootDir,
|
|
7131
|
+
baseSha: options.baseSha,
|
|
7132
|
+
headSha
|
|
7133
|
+
}) });
|
|
7134
|
+
if (log) {
|
|
7135
|
+
logEventContext(log, event);
|
|
7136
|
+
log.notice("local dispatch", {
|
|
7137
|
+
selectedTasks: selectedTasks.map((task) => task.name),
|
|
7138
|
+
skippedLocalTasks: runtime.plan.tasks.filter((task) => task.local === false).map((task) => task.name),
|
|
7139
|
+
diffTarget: includeWorkingTree ? "working-tree" : "head-ref"
|
|
7140
|
+
});
|
|
7141
|
+
}
|
|
7142
|
+
const result = await runTaskRuntime({
|
|
7143
|
+
workspace: options.rootDir,
|
|
7144
|
+
config: runtime.settings.config,
|
|
7145
|
+
event,
|
|
7146
|
+
env: options.env,
|
|
7147
|
+
plan: runtime.plan,
|
|
7148
|
+
selectedTasks,
|
|
7149
|
+
emptyTasksReason: "No change-request tasks are configured for local review",
|
|
7150
|
+
piExecutable: options.piExecutable,
|
|
7151
|
+
diffManifestBuilder: includeWorkingTree ? (diffOptions) => buildDiffManifest({
|
|
7152
|
+
...diffOptions,
|
|
7153
|
+
includeWorkingTree: true
|
|
7154
|
+
}) : void 0,
|
|
7155
|
+
log,
|
|
7156
|
+
taskLog: options.taskLog
|
|
7157
|
+
});
|
|
7158
|
+
if (result.kind === "command-response") throw new Error("command response result is only supported for issue_comment commands");
|
|
7159
|
+
log?.notice("local review complete", {
|
|
7160
|
+
kind: result.kind,
|
|
7161
|
+
taskChecks: result.taskChecks.length,
|
|
7162
|
+
validFindings: result.kind === "review" ? result.validated.validFindings.length : void 0,
|
|
7163
|
+
droppedFindings: result.kind === "review" ? result.validated.droppedFindings.length : void 0,
|
|
7164
|
+
inlineDrafts: result.kind === "review" ? result.inlineCommentDrafts.length : void 0
|
|
7165
|
+
});
|
|
7166
|
+
return result;
|
|
7167
|
+
}
|
|
7168
|
+
/** Runs the GitHub Action workflow for pull request and issue-comment events. */
|
|
7169
|
+
async function runActionCommand(options) {
|
|
7170
|
+
return await runActionCommandWithDependencies(options);
|
|
7171
|
+
}
|
|
7172
|
+
async function runActionCommandWithDependencies(options) {
|
|
7173
|
+
const log = createRuntimeActionLog({
|
|
7174
|
+
logSink: options.logSink,
|
|
7175
|
+
env: options.env
|
|
7176
|
+
});
|
|
7177
|
+
return await log.group("pipr action", async () => {
|
|
7178
|
+
const eventName = (options.env ?? process.env).GITHUB_EVENT_NAME ?? "pull_request";
|
|
7179
|
+
log.notice("action start", {
|
|
7180
|
+
eventName,
|
|
7181
|
+
dryRun: options.dryRun,
|
|
7182
|
+
root: options.rootDir,
|
|
7183
|
+
configDir: options.configDir
|
|
7184
|
+
});
|
|
7185
|
+
const adapter = createActionHostAdapter(options);
|
|
7186
|
+
await logPhase(log, "workspace", async () => {
|
|
7187
|
+
adapter.workspace.ensureWorkspaceSafeDirectory?.({
|
|
7188
|
+
rootDir: options.rootDir,
|
|
7189
|
+
env: options.env
|
|
7190
|
+
});
|
|
7191
|
+
});
|
|
7192
|
+
if (eventName === "issue_comment") return await runIssueCommentActionCommand(options, adapter, log);
|
|
7193
|
+
if (eventName === "pull_request_review_comment") return await runReviewCommentReplyActionCommand(options, adapter, log);
|
|
7194
|
+
return await runPullRequestActionCommand(options, adapter, log);
|
|
7153
7195
|
});
|
|
7154
|
-
return {
|
|
7155
|
-
kind: "command-response",
|
|
7156
|
-
event: options.event,
|
|
7157
|
-
command: options.completed.response.commandName,
|
|
7158
|
-
configSource: options.configSource,
|
|
7159
|
-
response: { body: options.completed.response.body },
|
|
7160
|
-
publication
|
|
7161
|
-
};
|
|
7162
7196
|
}
|
|
7163
7197
|
//#endregion
|
|
7164
|
-
export { runInspectCommand as a, PublicationError as c, supportedOfficialInitRecipes as d, runInitCommand as i, supportedOfficialInitAdapters as l, runActionCommandWithDependencies as n, runLocalReviewCommand as o, runDryRunCommand as r, runValidateCommand as s, runActionCommand as t, listOfficialInitRecipes as u };
|
|
7198
|
+
export { runInspectCommand as a, PublicationError as c, supportedOfficialInitRecipes as d, piBuiltinToolNames as f, piThinkingLevels as h, runInitCommand as i, supportedOfficialInitAdapters as l, piRequiredCliFlags as m, runActionCommandWithDependencies as n, runLocalReviewCommand as o, piReadOnlyToolNames as p, runDryRunCommand as r, runValidateCommand as s, runActionCommand as t, listOfficialInitRecipes as u };
|
|
7165
7199
|
|
|
7166
|
-
//# sourceMappingURL=commands-
|
|
7200
|
+
//# sourceMappingURL=commands-BajlHf13.mjs.map
|