@difflab/pi 0.8.0 → 0.9.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/README.md +8 -10
- package/agents/diffpi-orchestrator.md +1 -1
- package/agents/diffpi-planner.md +2 -2
- package/agents/diffpi-reviewer.md +2 -2
- package/agents/diffpi-worker.md +6 -6
- package/dist/environment.d.ts +0 -1
- package/dist/environment.d.ts.map +1 -1
- package/dist/extensions/index.js +319 -1495
- package/dist/index.d.ts +5 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +140 -759
- package/dist/log.d.ts +6 -11
- package/dist/log.d.ts.map +1 -1
- package/dist/plan/verify.d.ts.map +1 -1
- package/dist/review/index.d.ts +2 -3
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/review-backend.d.ts +2 -3
- package/dist/review/review-backend.d.ts.map +1 -1
- package/dist/setup.d.ts +0 -2
- package/dist/setup.d.ts.map +1 -1
- package/dist/tools/index.js +319 -1495
- package/dist/tools/log.d.ts.map +1 -1
- package/dist/tools/review.d.ts +1 -7
- package/dist/tools/review.d.ts.map +1 -1
- package/dist/tools/setup.d.ts.map +1 -1
- package/package.json +1 -5
- package/skills/diffpi-setup/SKILL.md +3 -3
- package/skills/plan/SKILL.md +3 -4
- package/skills/plan/references/design-quality.md +1 -1
- package/skills/plan/references/workflows/finalize.md +2 -2
- package/skills/plan/references/workflows/go.md +7 -5
- package/skills/plan/references/workflows/help.md +1 -2
- package/skills/plan/references/workflows/init.md +2 -2
- package/skills/plan/references/workflows/inline-fallback.md +3 -3
- package/skills/plan/references/workflows/new.md +3 -3
- package/skills/plan/references/workflows/update.md +6 -4
- package/skills/plan/references/workflows/validate.md +5 -5
- package/skills/review/SKILL.md +5 -5
- package/skills/review/references/review-standards.md +9 -1
- package/skills/review/references/workflows/address.md +9 -15
- package/skills/review/references/workflows/auto.md +9 -17
- package/skills/review/references/workflows/complete.md +2 -2
- package/skills/review/references/workflows/edit.md +7 -5
- package/skills/review/references/workflows/help.md +12 -20
- package/skills/review/references/workflows/new.md +14 -13
- package/skills/review/references/workflows/publish.md +2 -2
- package/skills/review/references/workflows/status.md +1 -1
- package/skills/review/templates/REVIEW.md +39 -0
- package/templates/plan/PLAN.md +9 -1
- package/templates/review/draft-pr.md +3 -9
- package/dist/cli/plan.d.ts +0 -17
- package/dist/cli/plan.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -4
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2258
- package/dist/extensions/tuicrx.d.ts +0 -66
- package/dist/extensions/tuicrx.d.ts.map +0 -1
- package/dist/extensions/zedx.d.ts +0 -18
- package/dist/extensions/zedx.d.ts.map +0 -1
- package/dist/review/local-review-backend.d.ts +0 -3
- package/dist/review/local-review-backend.d.ts.map +0 -1
- package/dist/review/local-reviews.d.ts +0 -31
- package/dist/review/local-reviews.d.ts.map +0 -1
- package/dist/review/review-markdown.d.ts +0 -6
- package/dist/review/review-markdown.d.ts.map +0 -1
- package/skills/plan/references/workflows/annotate.md +0 -13
- package/skills/review/references/workflows/open.md +0 -11
package/dist/index.js
CHANGED
|
@@ -95,30 +95,13 @@ function normalizeModelReference(value) {
|
|
|
95
95
|
return value.toLowerCase().replace(/^~/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
96
96
|
}
|
|
97
97
|
// src/environment.ts
|
|
98
|
-
import {
|
|
99
|
-
import { basename, join as join5 } from "node:path";
|
|
100
|
-
|
|
101
|
-
// src/assets.ts
|
|
102
|
-
import { existsSync } from "node:fs";
|
|
103
|
-
import { dirname, join as join2 } from "node:path";
|
|
104
|
-
import { fileURLToPath } from "node:url";
|
|
105
|
-
function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
|
|
106
|
-
const moduleDir = dirname(fileURLToPath(moduleUrl));
|
|
107
|
-
const candidates = [join2(moduleDir, name), join2(moduleDir, "..", name), join2(moduleDir, "..", "..", name)];
|
|
108
|
-
return candidates.find((path) => existsSync(path)) ?? candidates[1];
|
|
109
|
-
}
|
|
110
|
-
function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
|
|
111
|
-
return resolveBundledAssetDir("agents", moduleUrl);
|
|
112
|
-
}
|
|
113
|
-
function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
|
|
114
|
-
return resolveBundledAssetDir("templates", moduleUrl);
|
|
115
|
-
}
|
|
98
|
+
import { basename } from "node:path";
|
|
116
99
|
|
|
117
100
|
// src/extensions/processx.ts
|
|
118
101
|
import { spawn } from "node:child_process";
|
|
119
102
|
import { constants } from "node:fs";
|
|
120
103
|
import { access } from "node:fs/promises";
|
|
121
|
-
import { delimiter, join as
|
|
104
|
+
import { delimiter, join as join2 } from "node:path";
|
|
122
105
|
async function findExecutable(name) {
|
|
123
106
|
if (name.includes("/")) {
|
|
124
107
|
try {
|
|
@@ -131,7 +114,7 @@ async function findExecutable(name) {
|
|
|
131
114
|
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
132
115
|
if (!directory)
|
|
133
116
|
continue;
|
|
134
|
-
const candidate =
|
|
117
|
+
const candidate = join2(directory, name);
|
|
135
118
|
try {
|
|
136
119
|
await access(candidate, constants.X_OK);
|
|
137
120
|
return candidate;
|
|
@@ -195,175 +178,6 @@ function appendBounded(current, next) {
|
|
|
195
178
|
return combined.length <= MAX_CAPTURED_OUTPUT_LENGTH ? combined : combined.slice(-MAX_CAPTURED_OUTPUT_LENGTH);
|
|
196
179
|
}
|
|
197
180
|
|
|
198
|
-
// src/extensions/zedx.ts
|
|
199
|
-
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
200
|
-
import { homedir as homedir2 } from "node:os";
|
|
201
|
-
import { dirname as dirname2, join as join4 } from "node:path";
|
|
202
|
-
var ZED_LOCAL_REVIEW_TASK_NAME = "diffpi: tuicr local review";
|
|
203
|
-
var ZED_REVIEW_TASK_NAME = ZED_LOCAL_REVIEW_TASK_NAME;
|
|
204
|
-
var ZED_PR_REVIEW_TASK_NAME = "diffpi: tuicr PR review";
|
|
205
|
-
var ZED_PLAN_ANNOTATE_TASK_NAME = "diffpi: annotate plan";
|
|
206
|
-
var LEGACY_ZED_REVIEW_TASK_NAME = "diffpi: tuicr review";
|
|
207
|
-
var REVIEW_KEYBINDING = "cmd-alt-r";
|
|
208
|
-
function zedTasksPath(homeDir = homedir2()) {
|
|
209
|
-
return join4(homeDir, ".config", "zed", "tasks.json");
|
|
210
|
-
}
|
|
211
|
-
function zedKeymapPath(homeDir = homedir2()) {
|
|
212
|
-
return join4(homeDir, ".config", "zed", "keymap.json");
|
|
213
|
-
}
|
|
214
|
-
async function ensureZedReviewTask(homeDir = homedir2(), _command = ["tuicr", "-w", "-r", "main..HEAD"]) {
|
|
215
|
-
const path = zedTasksPath(homeDir);
|
|
216
|
-
const currentText = await readOptional(path);
|
|
217
|
-
const tasks = parseJsonArray(currentText, path);
|
|
218
|
-
const migrated = tasks.filter((task) => task.label !== LEGACY_ZED_REVIEW_TASK_NAME);
|
|
219
|
-
const next = [...migrated];
|
|
220
|
-
for (const task of [localReviewTask(), prReviewTask()]) {
|
|
221
|
-
const index = next.findIndex((existing) => existing.label === task.label);
|
|
222
|
-
if (index >= 0)
|
|
223
|
-
next[index] = { ...next[index], ...task };
|
|
224
|
-
else
|
|
225
|
-
next.push(task);
|
|
226
|
-
}
|
|
227
|
-
const changed = JSON.stringify(tasks) !== JSON.stringify(next);
|
|
228
|
-
if (changed)
|
|
229
|
-
await writeJson(path, next);
|
|
230
|
-
return { path, changed, existed: currentText !== undefined };
|
|
231
|
-
}
|
|
232
|
-
async function ensureZedPlanTask(packageVersion, homeDir = homedir2()) {
|
|
233
|
-
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageVersion)) {
|
|
234
|
-
throw new Error(`Invalid @difflab/pi package version: ${packageVersion}.`);
|
|
235
|
-
}
|
|
236
|
-
const path = zedTasksPath(homeDir);
|
|
237
|
-
const currentText = await readOptional(path);
|
|
238
|
-
const tasks = parseJsonArray(currentText, path);
|
|
239
|
-
const task = {
|
|
240
|
-
label: ZED_PLAN_ANNOTATE_TASK_NAME,
|
|
241
|
-
command: "npx",
|
|
242
|
-
args: ["--yes", `@difflab/pi@${packageVersion}`, "plan", "annotate", "$ZED_FILE", "--cwd", "$ZED_WORKTREE_ROOT"],
|
|
243
|
-
cwd: "$ZED_WORKTREE_ROOT",
|
|
244
|
-
use_new_terminal: true,
|
|
245
|
-
reveal: "always",
|
|
246
|
-
reveal_target: "center"
|
|
247
|
-
};
|
|
248
|
-
const next = [...tasks];
|
|
249
|
-
const index = next.findIndex((candidate) => candidate.label === task.label);
|
|
250
|
-
if (index >= 0)
|
|
251
|
-
next[index] = { ...next[index], ...task };
|
|
252
|
-
else
|
|
253
|
-
next.push(task);
|
|
254
|
-
const changed = JSON.stringify(tasks) !== JSON.stringify(next);
|
|
255
|
-
if (changed)
|
|
256
|
-
await writeJson(path, next);
|
|
257
|
-
return { path, changed, existed: currentText !== undefined };
|
|
258
|
-
}
|
|
259
|
-
async function ensureZedReviewKeybinding(homeDir = homedir2()) {
|
|
260
|
-
const path = zedKeymapPath(homeDir);
|
|
261
|
-
const currentText = await readOptional(path);
|
|
262
|
-
const entries = parseJsonArray(currentText, path);
|
|
263
|
-
const migrated = entries.map(migrateReviewKeymapEntry);
|
|
264
|
-
const alreadyBound = migrated.some((entry) => Object.values(entry.bindings ?? {}).some((action) => Array.isArray(action) && action[0] === "task::Spawn" && bindsReviewTask(action[1])));
|
|
265
|
-
const next = alreadyBound ? migrated : [
|
|
266
|
-
...migrated,
|
|
267
|
-
{
|
|
268
|
-
context: "Workspace",
|
|
269
|
-
bindings: { [REVIEW_KEYBINDING]: ["task::Spawn", { task_name: ZED_LOCAL_REVIEW_TASK_NAME }] }
|
|
270
|
-
}
|
|
271
|
-
];
|
|
272
|
-
const changed = JSON.stringify(entries) !== JSON.stringify(next);
|
|
273
|
-
if (changed)
|
|
274
|
-
await writeJson(path, next);
|
|
275
|
-
return { path, changed, existed: currentText !== undefined };
|
|
276
|
-
}
|
|
277
|
-
function zedReviewTaskName(command) {
|
|
278
|
-
if (command.some((value, index) => value === "plan" && command[index + 1] === "annotate")) {
|
|
279
|
-
return ZED_PLAN_ANNOTATE_TASK_NAME;
|
|
280
|
-
}
|
|
281
|
-
return command[0] === "tuicr" && command[1] === "pr" ? ZED_PR_REVIEW_TASK_NAME : ZED_LOCAL_REVIEW_TASK_NAME;
|
|
282
|
-
}
|
|
283
|
-
var LOCAL_REVIEW_SCRIPT = `set -eu
|
|
284
|
-
remote=$(git remote get-url origin 2>/dev/null || true)
|
|
285
|
-
branch=$(git branch --show-current)
|
|
286
|
-
base=
|
|
287
|
-
case "$remote" in
|
|
288
|
-
*github.com*)
|
|
289
|
-
base=$(gh pr list --head "$branch" --state open --limit 100 --json baseRefName,createdAt --jq 'sort_by(.createdAt) | reverse | .[0].baseRefName // empty' 2>/dev/null || true)
|
|
290
|
-
[ -n "$base" ] || base=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null || true)
|
|
291
|
-
;;
|
|
292
|
-
*gitlab*)
|
|
293
|
-
mr=$(glab mr list --source-branch "$branch" --order created_at --sort desc --per-page 100 --output json 2>/dev/null || true)
|
|
294
|
-
base=$(printf '%s' "$mr" | node -e 'let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{try{let a=JSON.parse(s);process.stdout.write(a[0]?.target_branch||"")}catch{}})')
|
|
295
|
-
[ -n "$base" ] || base=$(glab repo view --output json 2>/dev/null | node -e 'let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(JSON.parse(s).default_branch||"")}catch{}})' || true)
|
|
296
|
-
;;
|
|
297
|
-
esac
|
|
298
|
-
[ -n "$base" ] || { echo 'diffpi: could not resolve a GitHub/GitLab review base' >&2; exit 1; }
|
|
299
|
-
exec tuicr -w -r "$base..HEAD"`;
|
|
300
|
-
var PR_REVIEW_SCRIPT = `set -eu
|
|
301
|
-
remote=$(git remote get-url origin 2>/dev/null || true)
|
|
302
|
-
branch=$(git branch --show-current)
|
|
303
|
-
case "$remote" in
|
|
304
|
-
*github.com*) number=$(gh pr list --head "$branch" --state open --limit 100 --json number,createdAt --jq 'sort_by(.createdAt) | reverse | .[0].number // empty' 2>/dev/null || true) ;;
|
|
305
|
-
*gitlab*) number=$(glab mr list --source-branch "$branch" --order created_at --sort desc --per-page 100 --output json 2>/dev/null | node -e 'let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{try{let a=JSON.parse(s);process.stdout.write(String(a[0]?.iid||a[0]?.number||""))}catch{}})' || true) ;;
|
|
306
|
-
*) number= ;;
|
|
307
|
-
esac
|
|
308
|
-
[ -n "$number" ] || { echo 'diffpi: could not resolve the current GitHub PR or GitLab MR' >&2; exit 1; }
|
|
309
|
-
exec tuicr pr "$number"`;
|
|
310
|
-
function staticReviewTask(label, script) {
|
|
311
|
-
return {
|
|
312
|
-
label,
|
|
313
|
-
command: "sh",
|
|
314
|
-
args: ["-lc", script],
|
|
315
|
-
cwd: "$ZED_WORKTREE_ROOT",
|
|
316
|
-
use_new_terminal: true,
|
|
317
|
-
reveal: "always",
|
|
318
|
-
reveal_target: "center"
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function localReviewTask() {
|
|
322
|
-
return staticReviewTask(ZED_LOCAL_REVIEW_TASK_NAME, LOCAL_REVIEW_SCRIPT);
|
|
323
|
-
}
|
|
324
|
-
function prReviewTask() {
|
|
325
|
-
return staticReviewTask(ZED_PR_REVIEW_TASK_NAME, PR_REVIEW_SCRIPT);
|
|
326
|
-
}
|
|
327
|
-
function bindsReviewTask(payload) {
|
|
328
|
-
return typeof payload === "object" && payload !== null && payload.task_name === ZED_LOCAL_REVIEW_TASK_NAME;
|
|
329
|
-
}
|
|
330
|
-
function migrateReviewKeymapEntry(entry) {
|
|
331
|
-
const bindings = Object.fromEntries(Object.entries(entry.bindings ?? {}).map(([key, action]) => {
|
|
332
|
-
if (Array.isArray(action) && action[0] === "task::Spawn" && typeof action[1] === "object" && action[1] !== null && action[1].task_name === LEGACY_ZED_REVIEW_TASK_NAME) {
|
|
333
|
-
return [key, ["task::Spawn", { ...action[1], task_name: ZED_LOCAL_REVIEW_TASK_NAME }]];
|
|
334
|
-
}
|
|
335
|
-
return [key, action];
|
|
336
|
-
}));
|
|
337
|
-
return entry.bindings ? { ...entry, bindings } : entry;
|
|
338
|
-
}
|
|
339
|
-
async function readOptional(path) {
|
|
340
|
-
try {
|
|
341
|
-
return await readFile2(path, "utf8");
|
|
342
|
-
} catch (error) {
|
|
343
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
344
|
-
return;
|
|
345
|
-
throw error;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
function parseJsonArray(content, path) {
|
|
349
|
-
if (!content?.trim())
|
|
350
|
-
return [];
|
|
351
|
-
let value;
|
|
352
|
-
try {
|
|
353
|
-
value = JSON.parse(content);
|
|
354
|
-
} catch {
|
|
355
|
-
throw new Error(`Cannot safely edit ${path}: not strict JSON (it may contain JSONC comments).`);
|
|
356
|
-
}
|
|
357
|
-
if (!Array.isArray(value))
|
|
358
|
-
throw new Error(`Expected a JSON array in ${path}.`);
|
|
359
|
-
return value;
|
|
360
|
-
}
|
|
361
|
-
async function writeJson(path, value) {
|
|
362
|
-
await mkdir(dirname2(path), { recursive: true });
|
|
363
|
-
await writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
364
|
-
`, "utf8");
|
|
365
|
-
}
|
|
366
|
-
|
|
367
181
|
// src/environment.ts
|
|
368
182
|
function detectIde(env = process.env) {
|
|
369
183
|
const program = (env.TERM_PROGRAM ?? "").toLowerCase();
|
|
@@ -441,35 +255,6 @@ async function openFileAdjacent(path, opts) {
|
|
|
441
255
|
}
|
|
442
256
|
return { launched: false, via: "print", command: path, reason: "No supported IDE or multiplexer was available." };
|
|
443
257
|
}
|
|
444
|
-
async function openInNewTab(command, opts) {
|
|
445
|
-
const env = opts.env ?? process.env;
|
|
446
|
-
const name = opts.name ?? "review";
|
|
447
|
-
const printable = command.join(" ");
|
|
448
|
-
const mux = detectMux(env);
|
|
449
|
-
if (mux !== "none") {
|
|
450
|
-
const opened = await openMuxTab(mux, command, opts.cwd, name, printable);
|
|
451
|
-
if (opened)
|
|
452
|
-
return opened;
|
|
453
|
-
}
|
|
454
|
-
if (detectIde(env) === "zed") {
|
|
455
|
-
try {
|
|
456
|
-
const taskName = zedReviewTaskName(command);
|
|
457
|
-
if (taskName === ZED_PLAN_ANNOTATE_TASK_NAME)
|
|
458
|
-
await ensureZedPlanTask(await packageVersion(), opts.homeDir);
|
|
459
|
-
else
|
|
460
|
-
await ensureZedReviewTask(opts.homeDir, command);
|
|
461
|
-
return {
|
|
462
|
-
launched: false,
|
|
463
|
-
configured: true,
|
|
464
|
-
via: "zed-task",
|
|
465
|
-
command: printable,
|
|
466
|
-
taskName,
|
|
467
|
-
instruction: `Run the Zed task "${taskName}".`
|
|
468
|
-
};
|
|
469
|
-
} catch {}
|
|
470
|
-
}
|
|
471
|
-
return { launched: false, via: "print", command: printable };
|
|
472
|
-
}
|
|
473
258
|
function screenWindowArgs(command, cwd, name) {
|
|
474
259
|
return ["-X", "screen", "-t", name, "sh", "-lc", 'cd -- "$1" && shift && exec "$@"', "sh", cwd, ...command];
|
|
475
260
|
}
|
|
@@ -487,17 +272,6 @@ async function resolveEditor(env) {
|
|
|
487
272
|
return candidate;
|
|
488
273
|
return;
|
|
489
274
|
}
|
|
490
|
-
async function packageVersion() {
|
|
491
|
-
let value;
|
|
492
|
-
try {
|
|
493
|
-
value = JSON.parse(await readFile3(join5(resolveBundledAgentsDir(), "..", "package.json"), "utf8"));
|
|
494
|
-
} catch (error) {
|
|
495
|
-
throw new Error("Cannot parse the installed @difflab/pi package metadata.", { cause: error });
|
|
496
|
-
}
|
|
497
|
-
if (typeof value.version !== "string")
|
|
498
|
-
throw new Error("Cannot resolve the installed @difflab/pi version.");
|
|
499
|
-
return value.version;
|
|
500
|
-
}
|
|
501
275
|
async function openMuxTab(mux, command, cwd, name, printable, tmuxCommand) {
|
|
502
276
|
if (mux === "zellij" && await findExecutable("zellij")) {
|
|
503
277
|
const result = await run("zellij", ["action", "new-tab", "--cwd", cwd, "--name", name, "--", ...command]);
|
|
@@ -521,7 +295,7 @@ async function openMuxTab(mux, command, cwd, name, printable, tmuxCommand) {
|
|
|
521
295
|
}
|
|
522
296
|
// src/extensions/gitx.ts
|
|
523
297
|
import { realpath } from "node:fs/promises";
|
|
524
|
-
import { basename as basename2, isAbsolute, join as
|
|
298
|
+
import { basename as basename2, isAbsolute, join as join3, resolve } from "node:path";
|
|
525
299
|
async function gitToplevel(cwd) {
|
|
526
300
|
const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
|
|
527
301
|
const top = result.stdout.trim();
|
|
@@ -533,7 +307,7 @@ async function inspectGitRepository(cwd) {
|
|
|
533
307
|
const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
|
|
534
308
|
const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
|
|
535
309
|
const common = commonResult.stdout.trim();
|
|
536
|
-
const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common :
|
|
310
|
+
const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common : join3(root, common)) : root;
|
|
537
311
|
const commonDir = await canonicalPath(commonPath);
|
|
538
312
|
return {
|
|
539
313
|
root,
|
|
@@ -870,9 +644,9 @@ function createForgeBackend(vcs) {
|
|
|
870
644
|
var createVcsBackend = createForgeBackend;
|
|
871
645
|
var createForge = createForgeBackend;
|
|
872
646
|
// src/extensions/misex.ts
|
|
873
|
-
import { mkdir
|
|
874
|
-
import { homedir as
|
|
875
|
-
import { basename as basename3, dirname
|
|
647
|
+
import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
|
|
648
|
+
import { homedir as homedir2 } from "node:os";
|
|
649
|
+
import { basename as basename3, dirname, join as join4 } from "node:path";
|
|
876
650
|
var MISE_HOOK_START = "# >>> @difflab/pi mise >>>";
|
|
877
651
|
var MISE_HOOK_END = "# <<< @difflab/pi mise <<<";
|
|
878
652
|
var mise = {
|
|
@@ -883,11 +657,11 @@ var mise = {
|
|
|
883
657
|
return run("mise", args, options);
|
|
884
658
|
},
|
|
885
659
|
async install(options = {}) {
|
|
886
|
-
const homeDir = options.homeDir ??
|
|
660
|
+
const homeDir = options.homeDir ?? homedir2();
|
|
887
661
|
const platform = options.platform ?? process.platform;
|
|
888
662
|
if (platform === "win32")
|
|
889
663
|
throw new Error("Automatic mise installation supports macOS and Linux only.");
|
|
890
|
-
const installedPath =
|
|
664
|
+
const installedPath = join4(homeDir, ".local", "bin", "mise");
|
|
891
665
|
if (options.dryRun)
|
|
892
666
|
return installedPath;
|
|
893
667
|
await runChecked("sh", ["-c", "curl -fsSL https://mise.run | sh"]);
|
|
@@ -897,7 +671,7 @@ var mise = {
|
|
|
897
671
|
return executable;
|
|
898
672
|
},
|
|
899
673
|
async hookEnsure(executable, options = {}) {
|
|
900
|
-
const homeDir = options.homeDir ??
|
|
674
|
+
const homeDir = options.homeDir ?? homedir2();
|
|
901
675
|
const hook = getShellHook(basename3(options.shell ?? process.env.SHELL ?? ""), executable, homeDir);
|
|
902
676
|
const current = await getOptionalFile(hook.path);
|
|
903
677
|
if (current.includes(MISE_HOOK_START))
|
|
@@ -907,8 +681,8 @@ var mise = {
|
|
|
907
681
|
const separator = current.length === 0 || current.endsWith(`
|
|
908
682
|
`) ? "" : `
|
|
909
683
|
`;
|
|
910
|
-
await
|
|
911
|
-
await
|
|
684
|
+
await mkdir(dirname(hook.path), { recursive: true });
|
|
685
|
+
await writeFile(hook.path, `${current}${separator}${hook.content}`, "utf8");
|
|
912
686
|
return { path: hook.path, changed: true, planned: false };
|
|
913
687
|
},
|
|
914
688
|
async toolCheckGlobal(executable, tool, minimumVersion) {
|
|
@@ -925,7 +699,7 @@ var mise = {
|
|
|
925
699
|
async toolInstallLocal(executable, specification, cwd = process.cwd()) {
|
|
926
700
|
await runChecked(executable, ["use", "--path", cwd, specification], { cwd });
|
|
927
701
|
},
|
|
928
|
-
async toolUpdateAllGlobal(executable, homeDir =
|
|
702
|
+
async toolUpdateAllGlobal(executable, homeDir = homedir2()) {
|
|
929
703
|
await runChecked(executable, ["upgrade"], { cwd: homeDir });
|
|
930
704
|
}
|
|
931
705
|
};
|
|
@@ -934,7 +708,7 @@ function getShellHook(shell, executable, homeDir) {
|
|
|
934
708
|
switch (shell.toLowerCase()) {
|
|
935
709
|
case "zsh":
|
|
936
710
|
return {
|
|
937
|
-
path:
|
|
711
|
+
path: join4(homeDir, ".zshrc"),
|
|
938
712
|
content: `${MISE_HOOK_START}
|
|
939
713
|
eval "$(${command} activate zsh)"
|
|
940
714
|
${MISE_HOOK_END}
|
|
@@ -942,7 +716,7 @@ ${MISE_HOOK_END}
|
|
|
942
716
|
};
|
|
943
717
|
case "fish":
|
|
944
718
|
return {
|
|
945
|
-
path:
|
|
719
|
+
path: join4(homeDir, ".config", "fish", "config.fish"),
|
|
946
720
|
content: `${MISE_HOOK_START}
|
|
947
721
|
${command} activate fish | source
|
|
948
722
|
${MISE_HOOK_END}
|
|
@@ -951,7 +725,7 @@ ${MISE_HOOK_END}
|
|
|
951
725
|
case "nu":
|
|
952
726
|
case "nushell":
|
|
953
727
|
return {
|
|
954
|
-
path:
|
|
728
|
+
path: join4(homeDir, ".config", "nushell", "config.nu"),
|
|
955
729
|
content: `${MISE_HOOK_START}
|
|
956
730
|
let mise_bin = ${command}
|
|
957
731
|
let mise_path = $nu.default-config-dir | path join mise.nu
|
|
@@ -962,7 +736,7 @@ ${MISE_HOOK_END}
|
|
|
962
736
|
};
|
|
963
737
|
case "xonsh":
|
|
964
738
|
return {
|
|
965
|
-
path:
|
|
739
|
+
path: join4(homeDir, ".xonshrc"),
|
|
966
740
|
content: `${MISE_HOOK_START}
|
|
967
741
|
execx($(${command} activate xonsh))
|
|
968
742
|
${MISE_HOOK_END}
|
|
@@ -970,7 +744,7 @@ ${MISE_HOOK_END}
|
|
|
970
744
|
};
|
|
971
745
|
case "elvish":
|
|
972
746
|
return {
|
|
973
|
-
path:
|
|
747
|
+
path: join4(homeDir, ".config", "elvish", "rc.elv"),
|
|
974
748
|
content: `${MISE_HOOK_START}
|
|
975
749
|
var mise: = (ns [&])
|
|
976
750
|
eval (${command} activate elvish | slurp) &ns=$mise: &on-end={|ns| set mise: = $ns }
|
|
@@ -981,7 +755,7 @@ ${MISE_HOOK_END}
|
|
|
981
755
|
case "pwsh":
|
|
982
756
|
case "powershell":
|
|
983
757
|
return {
|
|
984
|
-
path:
|
|
758
|
+
path: join4(homeDir, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"),
|
|
985
759
|
content: `${MISE_HOOK_START}
|
|
986
760
|
(& ${command} activate pwsh) | Out-String | Invoke-Expression
|
|
987
761
|
${MISE_HOOK_END}
|
|
@@ -990,7 +764,7 @@ ${MISE_HOOK_END}
|
|
|
990
764
|
case "bash":
|
|
991
765
|
default:
|
|
992
766
|
return {
|
|
993
|
-
path:
|
|
767
|
+
path: join4(homeDir, ".bashrc"),
|
|
994
768
|
content: `${MISE_HOOK_START}
|
|
995
769
|
eval "$(${command} activate bash)"
|
|
996
770
|
${MISE_HOOK_END}
|
|
@@ -1029,7 +803,7 @@ function isVersionAtLeast(version, minimumVersion) {
|
|
|
1029
803
|
}
|
|
1030
804
|
async function getOptionalFile(path) {
|
|
1031
805
|
try {
|
|
1032
|
-
return await
|
|
806
|
+
return await readFile2(path, "utf8");
|
|
1033
807
|
} catch (error) {
|
|
1034
808
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
1035
809
|
return "";
|
|
@@ -1121,9 +895,6 @@ function yymmdd(date = new Date) {
|
|
|
1121
895
|
function reviewRecordName(target, date = new Date) {
|
|
1122
896
|
return `${yymmdd(date)}-${reviewSlug(target) || "uncommitted"}`;
|
|
1123
897
|
}
|
|
1124
|
-
function isLocalResponse(body) {
|
|
1125
|
-
return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
|
|
1126
|
-
}
|
|
1127
898
|
function dedupeFindings(findings) {
|
|
1128
899
|
const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
|
|
1129
900
|
const byKey = new Map;
|
|
@@ -1168,142 +939,6 @@ function renderCommentBody(finding) {
|
|
|
1168
939
|
> **Reference:** ${finding.reference}` : "";
|
|
1169
940
|
return `${prefix}${finding.body}${reference}`;
|
|
1170
941
|
}
|
|
1171
|
-
// src/review/review-markdown.ts
|
|
1172
|
-
import { z as z4 } from "zod";
|
|
1173
|
-
var reviewThreadRecordSchema = z4.object({
|
|
1174
|
-
id: z4.string().min(1),
|
|
1175
|
-
file: z4.string().nullable().optional().transform((value) => value ?? undefined),
|
|
1176
|
-
line: z4.number().int().positive().nullable().optional().transform((value) => value ?? undefined),
|
|
1177
|
-
body: z4.string(),
|
|
1178
|
-
author: z4.string().optional(),
|
|
1179
|
-
resolved: z4.boolean(),
|
|
1180
|
-
addressed: z4.boolean().optional(),
|
|
1181
|
-
question: z4.boolean(),
|
|
1182
|
-
replies: z4.array(z4.string()).optional()
|
|
1183
|
-
});
|
|
1184
|
-
function renderReviewDoc(input) {
|
|
1185
|
-
const anchored = dedupeFindings(input.findings).filter((finding) => finding.line > 0);
|
|
1186
|
-
const lines = [`# Review: ${input.title}`, "", "## Metadata"];
|
|
1187
|
-
if (input.number !== undefined)
|
|
1188
|
-
lines.push(`- **PR/MR**: #${input.number}${input.url ? ` — ${input.url}` : ""}`);
|
|
1189
|
-
if (input.author)
|
|
1190
|
-
lines.push(`- **Author**: ${input.author}`);
|
|
1191
|
-
if (input.model)
|
|
1192
|
-
lines.push(`- **Review agent**: ${input.model}`);
|
|
1193
|
-
if (input.headRef && input.baseRef)
|
|
1194
|
-
lines.push(`- **Branch**: ${input.headRef} → ${input.baseRef}`);
|
|
1195
|
-
if (input.additions !== undefined)
|
|
1196
|
-
lines.push(`- **Stats**: +${input.additions} -${input.deletions ?? 0} across ${input.changedFiles ?? 0} files`);
|
|
1197
|
-
lines.push(`- **Reviewed**: ${input.timestamp ?? new Date().toISOString()}`, "");
|
|
1198
|
-
if (input.overallIssues.length > 0) {
|
|
1199
|
-
lines.push("## Overall issues", "");
|
|
1200
|
-
for (const issue of input.overallIssues)
|
|
1201
|
-
lines.push(`- ${issue}`);
|
|
1202
|
-
lines.push("");
|
|
1203
|
-
}
|
|
1204
|
-
lines.push("## Verification", "");
|
|
1205
|
-
for (const gate of input.gates)
|
|
1206
|
-
lines.push(`- ${gate.name}: ${gate.status} — ${gate.detail}`);
|
|
1207
|
-
lines.push("");
|
|
1208
|
-
if (input.notVerified.length > 0) {
|
|
1209
|
-
lines.push("## What was NOT verified", "");
|
|
1210
|
-
for (const item of input.notVerified)
|
|
1211
|
-
lines.push(`- ${item}`);
|
|
1212
|
-
lines.push("");
|
|
1213
|
-
}
|
|
1214
|
-
lines.push("## Inline Comments", "");
|
|
1215
|
-
for (const finding of anchored) {
|
|
1216
|
-
lines.push(`### ${finding.file}:${finding.line} — ${finding.severity}`, "", finding.body, "");
|
|
1217
|
-
if (finding.reference)
|
|
1218
|
-
lines.push(`> **Reference:** ${finding.reference}`, "");
|
|
1219
|
-
lines.push("---", "");
|
|
1220
|
-
}
|
|
1221
|
-
return `${lines.join(`
|
|
1222
|
-
`).trimEnd()}
|
|
1223
|
-
`;
|
|
1224
|
-
}
|
|
1225
|
-
function renderThreadArtifact(title, target, threads, options = {}) {
|
|
1226
|
-
const records = threads.map((thread) => ({
|
|
1227
|
-
id: thread.id,
|
|
1228
|
-
file: thread.file,
|
|
1229
|
-
line: thread.line,
|
|
1230
|
-
body: thread.body,
|
|
1231
|
-
author: thread.author,
|
|
1232
|
-
resolved: thread.resolved,
|
|
1233
|
-
addressed: thread.addressed,
|
|
1234
|
-
question: thread.question,
|
|
1235
|
-
replies: thread.replies
|
|
1236
|
-
}));
|
|
1237
|
-
const payload = Buffer.from(JSON.stringify(records), "utf8").toString("base64url");
|
|
1238
|
-
const lines = [
|
|
1239
|
-
`<!-- diffpi-threads:${payload} -->`,
|
|
1240
|
-
`# Review threads: ${title}`,
|
|
1241
|
-
"",
|
|
1242
|
-
"## Metadata",
|
|
1243
|
-
"",
|
|
1244
|
-
`- Target: ${target}`,
|
|
1245
|
-
`- Pulled: ${options.timestamp ?? new Date().toISOString()}`
|
|
1246
|
-
];
|
|
1247
|
-
if (options.number !== undefined)
|
|
1248
|
-
lines.push(`- PR/MR: #${options.number}${options.url ? ` — ${options.url}` : ""}`);
|
|
1249
|
-
lines.push("", "## Replies", "");
|
|
1250
|
-
for (const thread of threads) {
|
|
1251
|
-
const id = Buffer.from(thread.id, "utf8").toString("base64url");
|
|
1252
|
-
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} (${thread.id})`, "", `<!-- diffpi-reply-start:${id} -->`, thread.reply ?? "", `<!-- diffpi-reply-end:${id} -->`, "");
|
|
1253
|
-
}
|
|
1254
|
-
lines.push("## Source comments", "");
|
|
1255
|
-
for (const thread of threads) {
|
|
1256
|
-
lines.push(`### ${thread.file ?? "review"}:${thread.line ?? "n/a"} — ${thread.author ?? "unknown"}`, "", `Thread: ${thread.id}`, `Status: ${thread.resolved ? "resolved" : thread.addressed ? "addressed" : "open"}`, "", "**Source comment**", "", thread.body, "", "**Recorded response**", "", thread.reply?.trim() || "_No response recorded._", "", "---", "");
|
|
1257
|
-
}
|
|
1258
|
-
return `${lines.join(`
|
|
1259
|
-
`).trimEnd()}
|
|
1260
|
-
`;
|
|
1261
|
-
}
|
|
1262
|
-
function parseThreadArtifact(content) {
|
|
1263
|
-
const payload = content.match(/^<!-- diffpi-threads:([A-Za-z0-9_-]+) -->$/m)?.[1];
|
|
1264
|
-
if (!payload)
|
|
1265
|
-
throw new Error("This file is not a Diffpi thread artifact.");
|
|
1266
|
-
let threads;
|
|
1267
|
-
try {
|
|
1268
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
1269
|
-
threads = z4.array(reviewThreadRecordSchema).parse(decoded);
|
|
1270
|
-
} catch {
|
|
1271
|
-
throw new Error("Cannot parse the Diffpi thread artifact payload.");
|
|
1272
|
-
}
|
|
1273
|
-
const replies = content.split(/^## Source comments$/m, 1)[0] ?? "";
|
|
1274
|
-
return threads.map((thread) => {
|
|
1275
|
-
const id = Buffer.from(thread.id, "utf8").toString("base64url");
|
|
1276
|
-
const startMarker = `<!-- diffpi-reply-start:${id} -->
|
|
1277
|
-
`;
|
|
1278
|
-
const endMarker = `
|
|
1279
|
-
<!-- diffpi-reply-end:${id} -->`;
|
|
1280
|
-
const start = replies.indexOf(startMarker);
|
|
1281
|
-
const end = start < 0 ? -1 : replies.indexOf(endMarker, start + startMarker.length);
|
|
1282
|
-
const reply = start >= 0 && end >= 0 ? replies.slice(start + startMarker.length, end).trim() : "";
|
|
1283
|
-
return reply ? { ...thread, reply } : thread;
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
function upsertThreadReply(content, threadId, body, question, resolved) {
|
|
1287
|
-
const threads = parseThreadArtifact(content);
|
|
1288
|
-
const thread = threads.find((candidate) => candidate.id === threadId);
|
|
1289
|
-
if (!thread)
|
|
1290
|
-
throw new Error(`Review thread ${threadId} was not found in the local artifact.`);
|
|
1291
|
-
thread.reply = body;
|
|
1292
|
-
thread.addressed = true;
|
|
1293
|
-
if (question !== undefined)
|
|
1294
|
-
thread.question = question;
|
|
1295
|
-
if (resolved !== undefined)
|
|
1296
|
-
thread.resolved = resolved;
|
|
1297
|
-
const title = content.match(/^# Review threads: (.+)$/m)?.[1] ?? "review";
|
|
1298
|
-
const target = content.match(/^- Target: (.+)$/m)?.[1] ?? "local";
|
|
1299
|
-
const timestamp = content.match(/^- Pulled: (.+)$/m)?.[1];
|
|
1300
|
-
const pr = content.match(/^- PR\/MR: #(\d+)(?: — (.+))?$/m);
|
|
1301
|
-
return renderThreadArtifact(title, target, threads, {
|
|
1302
|
-
timestamp,
|
|
1303
|
-
number: pr ? Number.parseInt(pr[1], 10) : undefined,
|
|
1304
|
-
url: pr?.[2]
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
942
|
// src/review/github-review-backend.ts
|
|
1308
943
|
function GitHubReviewBackend(vcs, number) {
|
|
1309
944
|
const pendingReview = async () => {
|
|
@@ -1683,210 +1318,6 @@ function hasGitlabDraftNotes(input) {
|
|
|
1683
1318
|
}
|
|
1684
1319
|
}
|
|
1685
1320
|
|
|
1686
|
-
// src/review/local-review-backend.ts
|
|
1687
|
-
import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
1688
|
-
|
|
1689
|
-
// src/extensions/tuicrx.ts
|
|
1690
|
-
import { readFile as readFile5, realpath as realpath2 } from "node:fs/promises";
|
|
1691
|
-
import { resolve as resolve2 } from "node:path";
|
|
1692
|
-
async function tuicrAvailable() {
|
|
1693
|
-
return Boolean(await findExecutable("tuicr"));
|
|
1694
|
-
}
|
|
1695
|
-
async function listSessions(repo = ".") {
|
|
1696
|
-
await requireTuicr();
|
|
1697
|
-
const result = await run("tuicr", ["review", "list", "--repo", repo]);
|
|
1698
|
-
if (result.code !== 0 || !result.stdout.trim())
|
|
1699
|
-
return [];
|
|
1700
|
-
try {
|
|
1701
|
-
const raw = JSON.parse(result.stdout);
|
|
1702
|
-
return raw.map((entry) => ({
|
|
1703
|
-
slug: entry.slug,
|
|
1704
|
-
kind: entry.kind,
|
|
1705
|
-
path: entry.path,
|
|
1706
|
-
updatedAt: entry.updated_at,
|
|
1707
|
-
commentCount: entry.comment_count,
|
|
1708
|
-
anchor: entry.anchor,
|
|
1709
|
-
active: entry.active
|
|
1710
|
-
}));
|
|
1711
|
-
} catch {
|
|
1712
|
-
throw new Error("Cannot parse tuicr review list output.");
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
async function addComment(session, body, opts = {}) {
|
|
1716
|
-
await requireTuicr();
|
|
1717
|
-
const args = ["review", "add", "--session", session, body];
|
|
1718
|
-
if (opts.targetFile)
|
|
1719
|
-
args.push("--target-file", opts.targetFile);
|
|
1720
|
-
if (opts.line !== undefined)
|
|
1721
|
-
args.push("--line", String(opts.line));
|
|
1722
|
-
if (opts.side)
|
|
1723
|
-
args.push("--side", opts.side);
|
|
1724
|
-
if (opts.username)
|
|
1725
|
-
args.push("--username", opts.username);
|
|
1726
|
-
await runChecked("tuicr", args);
|
|
1727
|
-
}
|
|
1728
|
-
async function launch(cwd, pr, localBase) {
|
|
1729
|
-
let command;
|
|
1730
|
-
if (pr !== undefined)
|
|
1731
|
-
command = ["tuicr", "pr", String(pr)];
|
|
1732
|
-
else if (localBase)
|
|
1733
|
-
command = ["tuicr", "-w", "-r", `${localBase}..HEAD`];
|
|
1734
|
-
else
|
|
1735
|
-
throw new Error("Cannot launch a local tuicr review: no base branch could be determined.");
|
|
1736
|
-
if (!await tuicrAvailable()) {
|
|
1737
|
-
return {
|
|
1738
|
-
launched: false,
|
|
1739
|
-
via: "print",
|
|
1740
|
-
command: command.join(" "),
|
|
1741
|
-
reason: "tuicr is not installed or is not available on PATH.",
|
|
1742
|
-
instruction: `Install tuicr, then run: ${command.join(" ")}`
|
|
1743
|
-
};
|
|
1744
|
-
}
|
|
1745
|
-
const name = diffpiLaunchName(cwd, pr === undefined ? "local review" : `PR #${pr}`);
|
|
1746
|
-
return openInNewTab(command, { cwd, name });
|
|
1747
|
-
}
|
|
1748
|
-
async function resolveSession(cwd, branch) {
|
|
1749
|
-
const sessions = await listSessions(cwd);
|
|
1750
|
-
return findMatchingSession(sessions, cwd, branch);
|
|
1751
|
-
}
|
|
1752
|
-
async function resolveReviewSession(cwd, target) {
|
|
1753
|
-
if (!target.workingTree && target.owner && target.repo && target.number !== undefined) {
|
|
1754
|
-
return resolvePrSession(cwd, target.owner, target.repo, target.number);
|
|
1755
|
-
}
|
|
1756
|
-
return resolveSession(cwd, target.branch);
|
|
1757
|
-
}
|
|
1758
|
-
async function resolvePrSession(cwd, owner, repo, number) {
|
|
1759
|
-
const coordinate = `${owner}/${repo}`.toLowerCase();
|
|
1760
|
-
const sessions = await listSessions(cwd);
|
|
1761
|
-
return sessions.find((session) => {
|
|
1762
|
-
const slug = session.slug.toLowerCase();
|
|
1763
|
-
return session.kind === "pr" && slug.includes(coordinate) && (slug.endsWith(`/pr/${number}`) || slug.endsWith(`/mr/${number}`));
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
async function findMatchingSession(sessions, cwd, branch) {
|
|
1767
|
-
const repository = await canonicalPath2(await gitToplevel(cwd));
|
|
1768
|
-
for (const session of sessions) {
|
|
1769
|
-
if (session.kind !== "local")
|
|
1770
|
-
continue;
|
|
1771
|
-
try {
|
|
1772
|
-
const data = await readSession(session.path);
|
|
1773
|
-
if (data.branch_name !== branch || !data.repo_path)
|
|
1774
|
-
continue;
|
|
1775
|
-
if (await canonicalPath2(data.repo_path) === repository)
|
|
1776
|
-
return session;
|
|
1777
|
-
} catch {}
|
|
1778
|
-
}
|
|
1779
|
-
return;
|
|
1780
|
-
}
|
|
1781
|
-
async function readSession(path) {
|
|
1782
|
-
const content = await readFile5(path, "utf8");
|
|
1783
|
-
try {
|
|
1784
|
-
return JSON.parse(content);
|
|
1785
|
-
} catch {
|
|
1786
|
-
throw new Error(`Cannot parse tuicr session JSON: ${path}`);
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
function toFindings(session, options = {}) {
|
|
1790
|
-
const comments = [];
|
|
1791
|
-
const include = (comment) => (!options.agentOnly || commentAuthor(comment)?.startsWith("Agent: ")) && (!options.excludeLocalResponses || !isLocalResponse(comment.content));
|
|
1792
|
-
const publishableBody = (comment) => {
|
|
1793
|
-
const author = commentAuthor(comment);
|
|
1794
|
-
const route = author?.match(/^Agent:\s*(.+)$/)?.[1];
|
|
1795
|
-
return route ? withRemoteProvenance(comment.content, route) : comment.content;
|
|
1796
|
-
};
|
|
1797
|
-
const bodyParts = (session.review_comments ?? []).flatMap((comment) => include(comment) ? [publishableBody(comment)] : []);
|
|
1798
|
-
for (const [file, entry] of Object.entries(session.files ?? {})) {
|
|
1799
|
-
for (const fileComment of entry.file_comments ?? []) {
|
|
1800
|
-
if (!include(fileComment))
|
|
1801
|
-
continue;
|
|
1802
|
-
const comment = {
|
|
1803
|
-
file,
|
|
1804
|
-
body: fileComment.content,
|
|
1805
|
-
...fileComment.id ? { sourceCommentId: String(fileComment.id) } : {}
|
|
1806
|
-
};
|
|
1807
|
-
const author = commentAuthor(fileComment);
|
|
1808
|
-
if (author)
|
|
1809
|
-
comment.author = author;
|
|
1810
|
-
comments.push(comment);
|
|
1811
|
-
}
|
|
1812
|
-
for (const [lineKey, lineComments] of Object.entries(entry.line_comments ?? {})) {
|
|
1813
|
-
const line = Number.parseInt(lineKey, 10);
|
|
1814
|
-
if (!Number.isFinite(line))
|
|
1815
|
-
continue;
|
|
1816
|
-
for (const lineComment of lineComments) {
|
|
1817
|
-
if (!include(lineComment))
|
|
1818
|
-
continue;
|
|
1819
|
-
const comment = {
|
|
1820
|
-
file,
|
|
1821
|
-
line,
|
|
1822
|
-
side: lineComment.side === "old" ? "LEFT" : "RIGHT",
|
|
1823
|
-
body: lineComment.content,
|
|
1824
|
-
...lineComment.id ? { sourceCommentId: String(lineComment.id) } : {}
|
|
1825
|
-
};
|
|
1826
|
-
const author = commentAuthor(lineComment);
|
|
1827
|
-
if (author)
|
|
1828
|
-
comment.author = author;
|
|
1829
|
-
comments.push(comment);
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
return { comments, body: bodyParts.join(`
|
|
1834
|
-
|
|
1835
|
-
`) };
|
|
1836
|
-
}
|
|
1837
|
-
async function requireTuicr() {
|
|
1838
|
-
if (!await tuicrAvailable())
|
|
1839
|
-
throw new Error("tuicr is not installed or is not available on PATH.");
|
|
1840
|
-
}
|
|
1841
|
-
function commentAuthor(comment) {
|
|
1842
|
-
return comment.username ?? comment.author;
|
|
1843
|
-
}
|
|
1844
|
-
async function canonicalPath2(path) {
|
|
1845
|
-
try {
|
|
1846
|
-
return await realpath2(path);
|
|
1847
|
-
} catch {
|
|
1848
|
-
return resolve2(path);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
// src/review/local-review-backend.ts
|
|
1853
|
-
function LocalReviewBackend(options) {
|
|
1854
|
-
return {
|
|
1855
|
-
kind: "local",
|
|
1856
|
-
async stage(draft) {
|
|
1857
|
-
for (const comment of draft.comments) {
|
|
1858
|
-
await addComment(options.session, comment.body, {
|
|
1859
|
-
targetFile: comment.file,
|
|
1860
|
-
line: comment.line,
|
|
1861
|
-
side: comment.side === "LEFT" ? "old" : "new",
|
|
1862
|
-
username: options.author
|
|
1863
|
-
});
|
|
1864
|
-
}
|
|
1865
|
-
if (draft.body.trim())
|
|
1866
|
-
await addComment(options.session, draft.body, { username: options.author });
|
|
1867
|
-
},
|
|
1868
|
-
async readDraft() {
|
|
1869
|
-
return toFindings(await readSession(options.session), { excludeLocalResponses: true });
|
|
1870
|
-
},
|
|
1871
|
-
async listThreads() {
|
|
1872
|
-
try {
|
|
1873
|
-
return parseThreadArtifact(await readFile6(options.artifactPath, "utf8"));
|
|
1874
|
-
} catch (error) {
|
|
1875
|
-
if (error.code === "ENOENT")
|
|
1876
|
-
return [];
|
|
1877
|
-
throw error;
|
|
1878
|
-
}
|
|
1879
|
-
},
|
|
1880
|
-
async reply(input) {
|
|
1881
|
-
const content = await readFile6(options.artifactPath, "utf8");
|
|
1882
|
-
await writeFile3(options.artifactPath, upsertThreadReply(content, input.threadId, input.body, input.question, input.resolve), "utf8");
|
|
1883
|
-
},
|
|
1884
|
-
async publish() {
|
|
1885
|
-
throw new Error("Promote a local draft through a remote review backend before publishing it.");
|
|
1886
|
-
}
|
|
1887
|
-
};
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
1321
|
// src/review/review-backend.ts
|
|
1891
1322
|
function createRemoteReviewBackend(vcs, number) {
|
|
1892
1323
|
if (vcs.provider === "github")
|
|
@@ -1895,35 +1326,32 @@ function createRemoteReviewBackend(vcs, number) {
|
|
|
1895
1326
|
return GitLabReviewBackend(vcs, number);
|
|
1896
1327
|
throw new Error("A remote review backend requires a GitHub or GitLab repository.");
|
|
1897
1328
|
}
|
|
1898
|
-
function createLocalReviewBackend(options) {
|
|
1899
|
-
return LocalReviewBackend(options);
|
|
1900
|
-
}
|
|
1901
1329
|
// src/review/review-state.ts
|
|
1902
1330
|
import { createHash as createHash2 } from "node:crypto";
|
|
1903
|
-
import { readFile as
|
|
1904
|
-
import { join as
|
|
1905
|
-
import { z as
|
|
1331
|
+
import { readFile as readFile3, rename, writeFile as writeFile2 } from "node:fs/promises";
|
|
1332
|
+
import { join as join6 } from "node:path";
|
|
1333
|
+
import { z as z4 } from "zod";
|
|
1906
1334
|
|
|
1907
1335
|
// src/store.ts
|
|
1908
1336
|
import { createHash } from "node:crypto";
|
|
1909
|
-
import { lstat, mkdir as
|
|
1910
|
-
import { homedir as
|
|
1911
|
-
import { dirname as
|
|
1337
|
+
import { lstat, mkdir as mkdir2, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
|
|
1338
|
+
import { homedir as homedir3 } from "node:os";
|
|
1339
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "node:path";
|
|
1912
1340
|
var STORE_LINK = ".diffpi";
|
|
1913
|
-
var LEGACY_STORE_LINK =
|
|
1341
|
+
var LEGACY_STORE_LINK = join5(".pi", "diffpi");
|
|
1914
1342
|
async function computeProjectSlug(cwd) {
|
|
1915
1343
|
return projectSlug(await inspectGitRepository(cwd));
|
|
1916
1344
|
}
|
|
1917
|
-
function storeGlobalRoot(homeDir =
|
|
1918
|
-
return
|
|
1345
|
+
function storeGlobalRoot(homeDir = homedir3()) {
|
|
1346
|
+
return join5(homeDir, ".difflab", "diffpi", "projects");
|
|
1919
1347
|
}
|
|
1920
|
-
async function ensureStore(cwd, homeDir =
|
|
1348
|
+
async function ensureStore(cwd, homeDir = homedir3()) {
|
|
1921
1349
|
const repository = await inspectGitRepository(cwd);
|
|
1922
1350
|
const root = repository.root;
|
|
1923
1351
|
const slug = projectSlug(repository);
|
|
1924
|
-
const dest =
|
|
1925
|
-
const link =
|
|
1926
|
-
await
|
|
1352
|
+
const dest = join5(storeGlobalRoot(homeDir), slug);
|
|
1353
|
+
const link = join5(root, STORE_LINK);
|
|
1354
|
+
await mkdir2(dest, { recursive: true });
|
|
1927
1355
|
try {
|
|
1928
1356
|
await assertStoreLink(link, dest);
|
|
1929
1357
|
} catch (error) {
|
|
@@ -1931,29 +1359,29 @@ async function ensureStore(cwd, homeDir = homedir4()) {
|
|
|
1931
1359
|
throw error;
|
|
1932
1360
|
await symlink(dest, link);
|
|
1933
1361
|
}
|
|
1934
|
-
await removeLegacyStoreLink(
|
|
1362
|
+
await removeLegacyStoreLink(join5(root, LEGACY_STORE_LINK), dest);
|
|
1935
1363
|
return { slug, root, dest, link, linked: true };
|
|
1936
1364
|
}
|
|
1937
|
-
async function storeDir(cwd, homeDir =
|
|
1365
|
+
async function storeDir(cwd, homeDir = homedir3()) {
|
|
1938
1366
|
const store = await ensureStore(cwd, homeDir);
|
|
1939
1367
|
return store.dest;
|
|
1940
1368
|
}
|
|
1941
|
-
async function plansDir(cwd, homeDir =
|
|
1369
|
+
async function plansDir(cwd, homeDir = homedir3()) {
|
|
1942
1370
|
const store = await ensureStore(cwd, homeDir);
|
|
1943
|
-
const dir =
|
|
1944
|
-
await
|
|
1371
|
+
const dir = join5(store.link, "plan");
|
|
1372
|
+
await mkdir2(dir, { recursive: true });
|
|
1945
1373
|
return dir;
|
|
1946
1374
|
}
|
|
1947
|
-
async function reviewsDir(cwd, homeDir =
|
|
1375
|
+
async function reviewsDir(cwd, homeDir = homedir3()) {
|
|
1948
1376
|
const store = await ensureStore(cwd, homeDir);
|
|
1949
|
-
const dir =
|
|
1950
|
-
await
|
|
1377
|
+
const dir = join5(store.link, "review");
|
|
1378
|
+
await mkdir2(dir, { recursive: true });
|
|
1951
1379
|
return dir;
|
|
1952
1380
|
}
|
|
1953
|
-
async function sessionsDir(cwd, homeDir =
|
|
1381
|
+
async function sessionsDir(cwd, homeDir = homedir3()) {
|
|
1954
1382
|
const store = await ensureStore(cwd, homeDir);
|
|
1955
|
-
const dir =
|
|
1956
|
-
await
|
|
1383
|
+
const dir = join5(store.link, "sessions");
|
|
1384
|
+
await mkdir2(dir, { recursive: true });
|
|
1957
1385
|
return dir;
|
|
1958
1386
|
}
|
|
1959
1387
|
async function assertStoreLink(path, dest) {
|
|
@@ -1961,7 +1389,7 @@ async function assertStoreLink(path, dest) {
|
|
|
1961
1389
|
if (!entry.isSymbolicLink())
|
|
1962
1390
|
throw new Error(`${path} exists and is not a symlink.`);
|
|
1963
1391
|
const target = await symlinkTarget(path);
|
|
1964
|
-
if (target !== await
|
|
1392
|
+
if (target !== await canonicalPath2(dest))
|
|
1965
1393
|
throw new Error(`${path} points to ${target}, not ${dest}.`);
|
|
1966
1394
|
}
|
|
1967
1395
|
async function removeLegacyStoreLink(path, dest) {
|
|
@@ -1970,7 +1398,7 @@ async function removeLegacyStoreLink(path, dest) {
|
|
|
1970
1398
|
if (!entry.isSymbolicLink())
|
|
1971
1399
|
return;
|
|
1972
1400
|
const target = await symlinkTarget(path);
|
|
1973
|
-
if (target === await
|
|
1401
|
+
if (target === await canonicalPath2(dest))
|
|
1974
1402
|
await unlink(path);
|
|
1975
1403
|
} catch (error) {
|
|
1976
1404
|
if (error.code !== "ENOENT")
|
|
@@ -1979,13 +1407,13 @@ async function removeLegacyStoreLink(path, dest) {
|
|
|
1979
1407
|
}
|
|
1980
1408
|
async function symlinkTarget(path) {
|
|
1981
1409
|
const target = await readlink(path);
|
|
1982
|
-
return
|
|
1410
|
+
return canonicalPath2(isAbsolute2(target) ? target : resolve2(dirname2(path), target));
|
|
1983
1411
|
}
|
|
1984
|
-
async function
|
|
1412
|
+
async function canonicalPath2(path) {
|
|
1985
1413
|
try {
|
|
1986
|
-
return await
|
|
1414
|
+
return await realpath2(path);
|
|
1987
1415
|
} catch {
|
|
1988
|
-
return
|
|
1416
|
+
return resolve2(path);
|
|
1989
1417
|
}
|
|
1990
1418
|
}
|
|
1991
1419
|
function projectSlug(repository) {
|
|
@@ -1995,13 +1423,13 @@ function projectSlug(repository) {
|
|
|
1995
1423
|
}
|
|
1996
1424
|
|
|
1997
1425
|
// src/review/review-state.ts
|
|
1998
|
-
var reviewPublicationStateSchema =
|
|
1999
|
-
target:
|
|
2000
|
-
bodies:
|
|
2001
|
-
comments:
|
|
2002
|
-
stagedComments:
|
|
2003
|
-
replies:
|
|
2004
|
-
overlayPath:
|
|
1426
|
+
var reviewPublicationStateSchema = z4.object({
|
|
1427
|
+
target: z4.string().optional(),
|
|
1428
|
+
bodies: z4.array(z4.string()).default([]),
|
|
1429
|
+
comments: z4.array(z4.string()).default([]),
|
|
1430
|
+
stagedComments: z4.array(z4.string()).default([]),
|
|
1431
|
+
replies: z4.array(z4.string()).default([]),
|
|
1432
|
+
overlayPath: z4.string().optional()
|
|
2005
1433
|
});
|
|
2006
1434
|
function reviewBodyFingerprint(body) {
|
|
2007
1435
|
return digest(body);
|
|
@@ -2017,15 +1445,15 @@ function unpublishedReviewComments(comments, knownFingerprints) {
|
|
|
2017
1445
|
}
|
|
2018
1446
|
async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
2019
1447
|
const target = `${vcs.provider}:${vcs.owner}/${vcs.repo}#${number}`;
|
|
2020
|
-
const path =
|
|
1448
|
+
const path = join6(await sessionsDir(cwd, homeDir), `review-publish-${digest(target).slice(0, 16)}.json`);
|
|
2021
1449
|
try {
|
|
2022
|
-
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await
|
|
1450
|
+
const parsed = reviewPublicationStateSchema.parse(JSON.parse(await readFile3(path, "utf8")));
|
|
2023
1451
|
return { path, state: { ...parsed, target } };
|
|
2024
1452
|
} catch (error) {
|
|
2025
1453
|
if (error.code === "ENOENT") {
|
|
2026
1454
|
return { path, state: { target, bodies: [], comments: [], stagedComments: [], replies: [] } };
|
|
2027
1455
|
}
|
|
2028
|
-
if (error instanceof SyntaxError || error instanceof
|
|
1456
|
+
if (error instanceof SyntaxError || error instanceof z4.ZodError) {
|
|
2029
1457
|
throw new Error(`Cannot parse review publication state: ${path}`);
|
|
2030
1458
|
}
|
|
2031
1459
|
throw error;
|
|
@@ -2033,7 +1461,7 @@ async function loadReviewPublicationState(cwd, vcs, number, homeDir) {
|
|
|
2033
1461
|
}
|
|
2034
1462
|
async function saveReviewPublicationState(path, state) {
|
|
2035
1463
|
const temp = `${path}.${process.pid}.tmp`;
|
|
2036
|
-
await
|
|
1464
|
+
await writeFile2(temp, `${JSON.stringify(state, null, 2)}
|
|
2037
1465
|
`, "utf8");
|
|
2038
1466
|
await rename(temp, path);
|
|
2039
1467
|
}
|
|
@@ -2041,12 +1469,12 @@ function digest(value) {
|
|
|
2041
1469
|
return createHash2("sha256").update(value).digest("hex");
|
|
2042
1470
|
}
|
|
2043
1471
|
// src/mcp.ts
|
|
2044
|
-
import { mkdir as
|
|
2045
|
-
import { homedir as
|
|
2046
|
-
import { dirname as
|
|
1472
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
1473
|
+
import { homedir as homedir4 } from "node:os";
|
|
1474
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
2047
1475
|
var mcp = {
|
|
2048
|
-
globalConfigPath(homeDir =
|
|
2049
|
-
return
|
|
1476
|
+
globalConfigPath(homeDir = homedir4()) {
|
|
1477
|
+
return join7(homeDir, ".config", "mcp", "mcp.json");
|
|
2050
1478
|
},
|
|
2051
1479
|
async serversEnsure(servers, options = {}) {
|
|
2052
1480
|
const path = options.path ?? mcp.globalConfigPath();
|
|
@@ -2059,8 +1487,8 @@ var mcp = {
|
|
|
2059
1487
|
const next = { ...current, mcpServers: nextServers };
|
|
2060
1488
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
2061
1489
|
if (changed && !options.dryRun) {
|
|
2062
|
-
await
|
|
2063
|
-
await
|
|
1490
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
1491
|
+
await writeFile3(path, `${JSON.stringify(next, null, 2)}
|
|
2064
1492
|
`, "utf8");
|
|
2065
1493
|
}
|
|
2066
1494
|
return { path, changed, existed: currentText !== undefined, planned: changed && options.dryRun === true };
|
|
@@ -2089,7 +1517,7 @@ function getParsedConfig(content, path) {
|
|
|
2089
1517
|
}
|
|
2090
1518
|
async function getOptionalFile2(path) {
|
|
2091
1519
|
try {
|
|
2092
|
-
return await
|
|
1520
|
+
return await readFile4(path, "utf8");
|
|
2093
1521
|
} catch (error) {
|
|
2094
1522
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
2095
1523
|
return;
|
|
@@ -2100,9 +1528,9 @@ function isRecord(value) {
|
|
|
2100
1528
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2101
1529
|
}
|
|
2102
1530
|
// src/pi.ts
|
|
2103
|
-
import { mkdir as
|
|
2104
|
-
import { homedir as
|
|
2105
|
-
import { dirname as
|
|
1531
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "node:fs/promises";
|
|
1532
|
+
import { homedir as homedir5 } from "node:os";
|
|
1533
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
2106
1534
|
var pi = {
|
|
2107
1535
|
executableCheck: findPiExecutable,
|
|
2108
1536
|
packageList: listPiPackages,
|
|
@@ -2115,19 +1543,19 @@ var pi = {
|
|
|
2115
1543
|
configEnsure: ensurePiConfig
|
|
2116
1544
|
};
|
|
2117
1545
|
async function ensurePiAgent(filename, content, agentDir = resolvePiAgentDir(), dryRun = false) {
|
|
2118
|
-
const path =
|
|
1546
|
+
const path = join8(agentDir, "agents", filename);
|
|
2119
1547
|
const currentText = await readTextIfExists(path);
|
|
2120
1548
|
const changed = currentText !== content;
|
|
2121
1549
|
if (changed && !dryRun) {
|
|
2122
|
-
await
|
|
2123
|
-
await
|
|
1550
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
1551
|
+
await writeFile4(path, content, "utf8");
|
|
2124
1552
|
}
|
|
2125
1553
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
2126
1554
|
}
|
|
2127
|
-
async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir =
|
|
2128
|
-
const roots = [
|
|
1555
|
+
async function checkGlobalPiSkill(name, agentDir = resolvePiAgentDir(), sharedSkillsDir = join8(homedir5(), ".agents", "skills")) {
|
|
1556
|
+
const roots = [join8(agentDir, "skills"), sharedSkillsDir];
|
|
2129
1557
|
for (const root of roots) {
|
|
2130
|
-
if (await readTextIfExists(
|
|
1558
|
+
if (await readTextIfExists(join8(root, name, "SKILL.md")) !== undefined)
|
|
2131
1559
|
return true;
|
|
2132
1560
|
}
|
|
2133
1561
|
return false;
|
|
@@ -2156,8 +1584,8 @@ async function ensurePiConfig(path, update, dryRun = false) {
|
|
|
2156
1584
|
const next = update(current);
|
|
2157
1585
|
const changed = JSON.stringify(current) !== JSON.stringify(next);
|
|
2158
1586
|
if (changed && !dryRun) {
|
|
2159
|
-
await
|
|
2160
|
-
await
|
|
1587
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
1588
|
+
await writeFile4(path, `${JSON.stringify(next, null, 2)}
|
|
2161
1589
|
`, "utf8");
|
|
2162
1590
|
}
|
|
2163
1591
|
return { path, changed, existed: currentText !== undefined, planned: changed && dryRun };
|
|
@@ -2176,8 +1604,8 @@ function hasPiPackage(listOutput, source) {
|
|
|
2176
1604
|
async function installPiPackage(executable, source) {
|
|
2177
1605
|
await runChecked(executable, ["install", source]);
|
|
2178
1606
|
}
|
|
2179
|
-
function resolvePiAgentDir(homeDir =
|
|
2180
|
-
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ?
|
|
1607
|
+
function resolvePiAgentDir(homeDir = homedir5()) {
|
|
1608
|
+
return process.env.PI_CODING_AGENT_DIR ?? (process.env.XDG_CONFIG_HOME ? join8(process.env.XDG_CONFIG_HOME, "pi") : join8(homeDir, ".pi", "agent"));
|
|
2181
1609
|
}
|
|
2182
1610
|
function parseJsonObject(content, path) {
|
|
2183
1611
|
if (!content?.trim())
|
|
@@ -2190,27 +1618,27 @@ function parseJsonObject(content, path) {
|
|
|
2190
1618
|
throw new Error(`Expected valid JSON object in ${path}.`);
|
|
2191
1619
|
}
|
|
2192
1620
|
// src/plan/verify.ts
|
|
2193
|
-
import { readFile as
|
|
2194
|
-
import { basename as basename4, dirname as
|
|
1621
|
+
import { lstat as lstat2, readFile as readFile5, readdir as readdir2, stat } from "node:fs/promises";
|
|
1622
|
+
import { basename as basename4, dirname as dirname5, join as join9, resolve as resolve3 } from "node:path";
|
|
2195
1623
|
async function verifyLivePlan(requested, cwd = process.cwd()) {
|
|
2196
|
-
const path =
|
|
1624
|
+
const path = resolve3(cwd, requested);
|
|
2197
1625
|
let planPath = path;
|
|
2198
1626
|
try {
|
|
2199
1627
|
if ((await stat(path)).isDirectory())
|
|
2200
|
-
planPath =
|
|
1628
|
+
planPath = join9(path, "PLAN.md");
|
|
2201
1629
|
} catch {}
|
|
2202
1630
|
const issues = [];
|
|
2203
1631
|
const briefPaths = [];
|
|
2204
|
-
const planDir =
|
|
1632
|
+
const planDir = dirname5(planPath);
|
|
2205
1633
|
if (basename4(planPath) !== "PLAN.md") {
|
|
2206
1634
|
addIssue(issues, planPath, 0, "path", "Select PLAN.md or its directory.");
|
|
2207
1635
|
return { planPath, briefPaths, ok: false, issues };
|
|
2208
1636
|
}
|
|
2209
|
-
if (!/^\d{6}(?:-[a-z0-9]+)+$/.test(basename4(planDir)) || basename4(
|
|
1637
|
+
if (!/^\d{6}(?:-[a-z0-9]+)+$/.test(basename4(planDir)) || basename4(dirname5(planDir)) !== "plan" || basename4(dirname5(dirname5(planDir))) !== ".diffpi")
|
|
2210
1638
|
addIssue(issues, planPath, 0, "layout", "Place PLAN.md under .diffpi/plan/<YYMMDD[-ticket]-short-slug>/.");
|
|
2211
1639
|
let source;
|
|
2212
1640
|
try {
|
|
2213
|
-
source = await
|
|
1641
|
+
source = await readFile5(planPath, "utf8");
|
|
2214
1642
|
} catch {
|
|
2215
1643
|
addIssue(issues, planPath, 0, "missing-plan", "PLAN.md is not readable.");
|
|
2216
1644
|
return { planPath, briefPaths, ok: false, issues };
|
|
@@ -2224,7 +1652,7 @@ async function verifyLivePlan(requested, cwd = process.cwd()) {
|
|
|
2224
1652
|
if (!planLines[line]?.startsWith(`### Phase ${index + 1}: `))
|
|
2225
1653
|
addIssue(issues, planPath, line + 1, "phase-order", "Number phase headings from 1 without gaps.");
|
|
2226
1654
|
}
|
|
2227
|
-
const implementation =
|
|
1655
|
+
const implementation = join9(planDir, "implementation");
|
|
2228
1656
|
let rootEntries = [];
|
|
2229
1657
|
let briefEntries = [];
|
|
2230
1658
|
try {
|
|
@@ -2232,19 +1660,28 @@ async function verifyLivePlan(requested, cwd = process.cwd()) {
|
|
|
2232
1660
|
briefEntries = await readdir2(implementation);
|
|
2233
1661
|
} catch {}
|
|
2234
1662
|
for (const entry of rootEntries) {
|
|
2235
|
-
|
|
2236
|
-
|
|
1663
|
+
const directory = ["implementation", "revisions", "logs"].includes(entry);
|
|
1664
|
+
const logFile = entry === "logs.jsonl";
|
|
1665
|
+
let valid = entry === "PLAN.md";
|
|
1666
|
+
if (directory || logFile) {
|
|
1667
|
+
try {
|
|
1668
|
+
const details = await lstat2(join9(planDir, entry));
|
|
1669
|
+
valid = !details.isSymbolicLink() && (directory ? details.isDirectory() : details.isFile());
|
|
1670
|
+
} catch {}
|
|
1671
|
+
}
|
|
1672
|
+
if (!valid)
|
|
1673
|
+
addIssue(issues, join9(planDir, entry), 0, "layout", "Only PLAN.md, implementation/, revisions/, logs.jsonl, and historical logs/ belong in the plan directory.");
|
|
2237
1674
|
}
|
|
2238
1675
|
for (const entry of briefEntries) {
|
|
2239
1676
|
const ordinal = /^phase-(\d+)\.md$/.exec(entry)?.[1];
|
|
2240
1677
|
if (!ordinal || !Number(ordinal) || Number(ordinal) > phases.length || entry !== `phase-${Number(ordinal)}.md`)
|
|
2241
|
-
addIssue(issues,
|
|
1678
|
+
addIssue(issues, join9(implementation, entry), 0, "extra-brief", "Only matching numbered phase briefs belong in implementation/.");
|
|
2242
1679
|
}
|
|
2243
1680
|
for (let index = 0;index < phases.length; index++) {
|
|
2244
|
-
const brief =
|
|
1681
|
+
const brief = join9(implementation, `phase-${index + 1}.md`);
|
|
2245
1682
|
briefPaths.push(brief);
|
|
2246
1683
|
try {
|
|
2247
|
-
const lines = visibleLines(await
|
|
1684
|
+
const lines = visibleLines(await readFile5(brief, "utf8"));
|
|
2248
1685
|
if (!/^# Phase \d+:\s*\S/.test(lines[0] ?? "") || !lines[0]?.startsWith(`# Phase ${index + 1}: `))
|
|
2249
1686
|
addIssue(issues, brief, 1, "phase-title", `Start with # Phase ${index + 1}: <title>.`);
|
|
2250
1687
|
checkHeadings(lines, brief, ["Objective", "Files Affected", "Tasks", "Implementation Constraints"], issues);
|
|
@@ -2277,19 +1714,31 @@ function checkHeadings(lines, file, names, issues) {
|
|
|
2277
1714
|
}
|
|
2278
1715
|
// src/setup.ts
|
|
2279
1716
|
import { parseFrontmatter as parseFrontmatter2 } from "@earendil-works/pi-coding-agent";
|
|
2280
|
-
import { readdir as readdir3, readFile as
|
|
2281
|
-
import { homedir as
|
|
2282
|
-
import { basename as basename5, join as
|
|
1717
|
+
import { readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
|
|
1718
|
+
import { homedir as homedir6 } from "node:os";
|
|
1719
|
+
import { basename as basename5, join as join11 } from "node:path";
|
|
1720
|
+
|
|
1721
|
+
// src/assets.ts
|
|
1722
|
+
import { existsSync } from "node:fs";
|
|
1723
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
1724
|
+
import { fileURLToPath } from "node:url";
|
|
1725
|
+
function resolveBundledAssetDir(name, moduleUrl = import.meta.url) {
|
|
1726
|
+
const moduleDir = dirname6(fileURLToPath(moduleUrl));
|
|
1727
|
+
const candidates = [join10(moduleDir, name), join10(moduleDir, "..", name), join10(moduleDir, "..", "..", name)];
|
|
1728
|
+
return candidates.find((path) => existsSync(path)) ?? candidates[1];
|
|
1729
|
+
}
|
|
1730
|
+
function resolveBundledAgentsDir(moduleUrl = import.meta.url) {
|
|
1731
|
+
return resolveBundledAssetDir("agents", moduleUrl);
|
|
1732
|
+
}
|
|
1733
|
+
function resolveBundledTemplatesDir(moduleUrl = import.meta.url) {
|
|
1734
|
+
return resolveBundledAssetDir("templates", moduleUrl);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// src/setup.ts
|
|
2283
1738
|
var MISE_DEPENDENCIES = [
|
|
2284
1739
|
{ name: "node", tool: "node", spec: "node@22", minimumVersion: "22.19.0" },
|
|
2285
1740
|
{ name: "zellij", tool: "zellij", spec: "zellij@latest", minimumVersion: undefined },
|
|
2286
1741
|
{ name: "helix", tool: "helix", spec: "helix@latest", minimumVersion: undefined },
|
|
2287
|
-
{
|
|
2288
|
-
name: "tuicr",
|
|
2289
|
-
tool: "github:agavra/tuicr",
|
|
2290
|
-
spec: "github:agavra/tuicr@latest",
|
|
2291
|
-
minimumVersion: undefined
|
|
2292
|
-
},
|
|
2293
1742
|
{
|
|
2294
1743
|
name: "context-mode",
|
|
2295
1744
|
tool: "npm:context-mode",
|
|
@@ -2318,8 +1767,8 @@ var FORGE_DEPENDENCIES = {
|
|
|
2318
1767
|
gitlab: { name: "glab", tool: "glab", spec: "glab@latest", minimumVersion: undefined }
|
|
2319
1768
|
};
|
|
2320
1769
|
async function ensureMise(options = {}) {
|
|
2321
|
-
const homeDir = options.homeDir ??
|
|
2322
|
-
const current = await mise.executableCheck() ?? await mise.executableCheck(
|
|
1770
|
+
const homeDir = options.homeDir ?? homedir6();
|
|
1771
|
+
const current = await mise.executableCheck() ?? await mise.executableCheck(join11(homeDir, ".local", "bin", "mise"));
|
|
2323
1772
|
if (current)
|
|
2324
1773
|
return { executable: current, action: createSetupAction("mise", "ready", current) };
|
|
2325
1774
|
reportProgress(options, "Installing mise");
|
|
@@ -2368,9 +1817,9 @@ async function ensureMiseDeps(miseExecutable, options = {}) {
|
|
|
2368
1817
|
async function ensurePiPlugins(options = {}) {
|
|
2369
1818
|
const actions = await ensurePiPackages(PI_PACKAGES, options);
|
|
2370
1819
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
2371
|
-
const webSearch = await pi.configEnsure(
|
|
1820
|
+
const webSearch = await pi.configEnsure(join11(agentDir, "web-search.json"), (config) => ({ ...config, workflow: "auto-summary" }), options.dryRun);
|
|
2372
1821
|
actions.push(getConfigSetupAction("web search settings", webSearch));
|
|
2373
|
-
const lsp = await pi.configEnsure(
|
|
1822
|
+
const lsp = await pi.configEnsure(join11(agentDir, "pi-lsp.json"), (config) => ({
|
|
2374
1823
|
...config,
|
|
2375
1824
|
progressive: { ...getRecord(config.progressive), enabled: true, inject: "none" }
|
|
2376
1825
|
}), options.dryRun);
|
|
@@ -2385,7 +1834,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
2385
1834
|
const actions = [];
|
|
2386
1835
|
for (const entry of entries) {
|
|
2387
1836
|
const id = basename5(entry.name, ".md").replace(/^diffpi-/, "");
|
|
2388
|
-
const source = await
|
|
1837
|
+
const source = await readFile6(join11(bundledAgentsDir, entry.name), "utf8");
|
|
2389
1838
|
const content = materializeAgentModels(source, id, userConfig.config, options.availableModels);
|
|
2390
1839
|
const result = await pi.agentEnsure(entry.name, content, agentDir, options.dryRun);
|
|
2391
1840
|
actions.push(getConfigSetupAction(`pi agent ${id}`, result));
|
|
@@ -2394,7 +1843,7 @@ async function ensurePiAgents(options = {}) {
|
|
|
2394
1843
|
}
|
|
2395
1844
|
async function ensurePiSkills(miseExecutable, options = {}) {
|
|
2396
1845
|
const agentDir = options.agentDir ?? pi.agentDir(options.homeDir);
|
|
2397
|
-
const sharedSkillsDir =
|
|
1846
|
+
const sharedSkillsDir = join11(options.homeDir ?? homedir6(), ".agents", "skills");
|
|
2398
1847
|
const actions = [];
|
|
2399
1848
|
for (const source of PI_SKILL_SOURCES) {
|
|
2400
1849
|
const missing = [];
|
|
@@ -2462,8 +1911,6 @@ async function setupPi(options = {}) {
|
|
|
2462
1911
|
actions.push(...await ensurePiAgents(options));
|
|
2463
1912
|
actions.push(...await ensurePiSkills(miseResult.executable, options));
|
|
2464
1913
|
actions.push(...await ensureMcpAdapters(miseResult.executable, options));
|
|
2465
|
-
if (options.bindZedKey)
|
|
2466
|
-
actions.push(...await ensureZedIntegration(options));
|
|
2467
1914
|
return {
|
|
2468
1915
|
actions,
|
|
2469
1916
|
restartPi: setupRequiresRestart(actions)
|
|
@@ -2472,35 +1919,6 @@ async function setupPi(options = {}) {
|
|
|
2472
1919
|
function setupRequiresRestart(actions) {
|
|
2473
1920
|
return actions.some((item) => (item.status === "installed" || item.status === "updated") && (item.name.startsWith("pi package ") || item.name.startsWith("pi agent ") || item.name.startsWith("pi skill ") || item.name === "MCP configuration" || item.name === "web search settings" || item.name === "pi-lsp settings"));
|
|
2474
1921
|
}
|
|
2475
|
-
async function ensureZedIntegration(options = {}) {
|
|
2476
|
-
if (options.dryRun) {
|
|
2477
|
-
const actions = [
|
|
2478
|
-
createSetupAction("Zed review tasks", "planned", "global static runtime-resolver tasks in tasks.json"),
|
|
2479
|
-
createSetupAction("Zed plan task", "planned", "pinned package CLI task in tasks.json")
|
|
2480
|
-
];
|
|
2481
|
-
if (options.bindZedKey)
|
|
2482
|
-
actions.push(createSetupAction("Zed review keybinding", "planned", "keymap.json"));
|
|
2483
|
-
return actions;
|
|
2484
|
-
}
|
|
2485
|
-
const actions = [];
|
|
2486
|
-
try {
|
|
2487
|
-
const tasks = await ensureZedReviewTask(options.homeDir);
|
|
2488
|
-
actions.push(createSetupAction("Zed review tasks", tasks.changed ? "installed" : "ready", tasks.path));
|
|
2489
|
-
const planTask = await ensureZedPlanTask(await packageVersion2(), options.homeDir);
|
|
2490
|
-
actions.push(createSetupAction("Zed plan task", planTask.changed ? "installed" : "ready", planTask.path));
|
|
2491
|
-
} catch (error) {
|
|
2492
|
-
actions.push(createSetupAction("Zed review tasks", "skipped", error instanceof Error ? error.message : String(error)));
|
|
2493
|
-
}
|
|
2494
|
-
if (options.bindZedKey) {
|
|
2495
|
-
try {
|
|
2496
|
-
const key = await ensureZedReviewKeybinding(options.homeDir);
|
|
2497
|
-
actions.push(createSetupAction("Zed review keybinding", key.changed ? "installed" : "ready", key.path));
|
|
2498
|
-
} catch (error) {
|
|
2499
|
-
actions.push(createSetupAction("Zed review keybinding", "skipped", error instanceof Error ? error.message : String(error)));
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
return actions;
|
|
2503
|
-
}
|
|
2504
1922
|
async function gitlabMcpHost(projectDir) {
|
|
2505
1923
|
try {
|
|
2506
1924
|
const vcs = await detectVcs(projectDir);
|
|
@@ -2589,29 +2007,17 @@ function createSetupAction(name, status, detail) {
|
|
|
2589
2007
|
function reportProgress(options, message) {
|
|
2590
2008
|
options.onProgress?.(message);
|
|
2591
2009
|
}
|
|
2592
|
-
async function packageVersion2() {
|
|
2593
|
-
const source = await readFile10(join13(resolveBundledAgentsDir(), "..", "package.json"), "utf8");
|
|
2594
|
-
let value;
|
|
2595
|
-
try {
|
|
2596
|
-
value = JSON.parse(source);
|
|
2597
|
-
} catch (error) {
|
|
2598
|
-
throw new Error("Cannot parse the installed @difflab/pi package metadata.", { cause: error });
|
|
2599
|
-
}
|
|
2600
|
-
if (typeof value.version !== "string")
|
|
2601
|
-
throw new Error("Cannot resolve the installed @difflab/pi version.");
|
|
2602
|
-
return value.version;
|
|
2603
|
-
}
|
|
2604
2010
|
function getRecord(value) {
|
|
2605
2011
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2606
2012
|
}
|
|
2607
2013
|
// src/templates.ts
|
|
2608
|
-
import { readFile as
|
|
2609
|
-
import { homedir as
|
|
2610
|
-
import { join as
|
|
2014
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
2015
|
+
import { homedir as homedir7 } from "node:os";
|
|
2016
|
+
import { join as join12, normalize } from "node:path";
|
|
2611
2017
|
async function loadTemplate(name, options = {}) {
|
|
2612
2018
|
const relative = templateRelativePath(name);
|
|
2613
|
-
const userPath =
|
|
2614
|
-
const bundledPath =
|
|
2019
|
+
const userPath = join12(options.homeDir ?? homedir7(), ".difflab", "diffpi", "templates", relative);
|
|
2020
|
+
const bundledPath = join12(options.bundledDir ?? resolveBundledTemplatesDir(), relative);
|
|
2615
2021
|
const user = await readOptionalFile(userPath);
|
|
2616
2022
|
if (user !== undefined)
|
|
2617
2023
|
return { name, path: userPath, source: "user", content: user };
|
|
@@ -2632,7 +2038,7 @@ function templateRelativePath(name) {
|
|
|
2632
2038
|
}
|
|
2633
2039
|
async function readOptionalFile(path) {
|
|
2634
2040
|
try {
|
|
2635
|
-
return await
|
|
2041
|
+
return await readFile7(path, "utf8");
|
|
2636
2042
|
} catch (error) {
|
|
2637
2043
|
if (error.code === "ENOENT")
|
|
2638
2044
|
return;
|
|
@@ -2643,18 +2049,12 @@ export {
|
|
|
2643
2049
|
CONVENTIONAL_COMMIT,
|
|
2644
2050
|
GitHubVcsBackend,
|
|
2645
2051
|
GitLabVcsBackend,
|
|
2646
|
-
ZED_LOCAL_REVIEW_TASK_NAME,
|
|
2647
|
-
ZED_PLAN_ANNOTATE_TASK_NAME,
|
|
2648
|
-
ZED_PR_REVIEW_TASK_NAME,
|
|
2649
|
-
ZED_REVIEW_TASK_NAME,
|
|
2650
|
-
addComment,
|
|
2651
2052
|
assertGitHubMergeReady,
|
|
2652
2053
|
assertReviewEventSupported,
|
|
2653
2054
|
checkConventionalSubject,
|
|
2654
2055
|
computeProjectSlug,
|
|
2655
2056
|
createForge,
|
|
2656
2057
|
createForgeBackend,
|
|
2657
|
-
createLocalReviewBackend,
|
|
2658
2058
|
createRemoteReviewBackend,
|
|
2659
2059
|
createVcsBackend,
|
|
2660
2060
|
dedupeFindings,
|
|
@@ -2672,17 +2072,12 @@ export {
|
|
|
2672
2072
|
ensurePiPlugins,
|
|
2673
2073
|
ensurePiSkills,
|
|
2674
2074
|
ensureStore,
|
|
2675
|
-
ensureZedPlanTask,
|
|
2676
|
-
ensureZedReviewKeybinding,
|
|
2677
|
-
ensureZedReviewTask,
|
|
2678
2075
|
findPreferredModel,
|
|
2679
2076
|
findingSchema,
|
|
2680
2077
|
findingsSchema,
|
|
2681
2078
|
gitToplevel,
|
|
2682
2079
|
githubReviewSubmissionEndpoint,
|
|
2683
2080
|
hasGitlabDraftNotes,
|
|
2684
|
-
launch,
|
|
2685
|
-
listSessions,
|
|
2686
2081
|
loadDiffpiConfig,
|
|
2687
2082
|
loadReviewPublicationState,
|
|
2688
2083
|
loadTemplate,
|
|
@@ -2690,20 +2085,12 @@ export {
|
|
|
2690
2085
|
mcp,
|
|
2691
2086
|
mise,
|
|
2692
2087
|
openFileAdjacent,
|
|
2693
|
-
openInNewTab,
|
|
2694
2088
|
parseGitlabDiffRefs,
|
|
2695
2089
|
parseRemote,
|
|
2696
|
-
parseThreadArtifact,
|
|
2697
2090
|
pi,
|
|
2698
2091
|
plansDir,
|
|
2699
|
-
readSession,
|
|
2700
|
-
renderReviewDoc,
|
|
2701
2092
|
renderTemplate,
|
|
2702
|
-
renderThreadArtifact,
|
|
2703
2093
|
resolveAgentModelPreferences,
|
|
2704
|
-
resolvePrSession,
|
|
2705
|
-
resolveReviewSession,
|
|
2706
|
-
resolveSession,
|
|
2707
2094
|
reviewBodyFingerprint,
|
|
2708
2095
|
reviewCommentFingerprint,
|
|
2709
2096
|
reviewRecordName,
|
|
@@ -2718,15 +2105,9 @@ export {
|
|
|
2718
2105
|
storeDir,
|
|
2719
2106
|
storeGlobalRoot,
|
|
2720
2107
|
templateRelativePath,
|
|
2721
|
-
toFindings,
|
|
2722
2108
|
toReviewComments,
|
|
2723
|
-
tuicrAvailable,
|
|
2724
2109
|
unpublishedReviewComments,
|
|
2725
|
-
upsertThreadReply,
|
|
2726
2110
|
verifyLivePlan,
|
|
2727
2111
|
withRemoteProvenance,
|
|
2728
|
-
yymmdd
|
|
2729
|
-
zedKeymapPath,
|
|
2730
|
-
zedReviewTaskName,
|
|
2731
|
-
zedTasksPath
|
|
2112
|
+
yymmdd
|
|
2732
2113
|
};
|