diffing 0.18.2 → 0.18.3
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/{cli-agent-Cpusc3Fl.mjs → cli-agent-DV5ZhMX1.mjs} +13 -1
- package/dist/cli.mjs +3 -3
- package/dist/{mcp-ClEsxza5.mjs → mcp-45VPdWIr.mjs} +2 -2
- package/dist/{mockup-format-Dzc3e_10.mjs → mockup-lint-BRjhxXSw.mjs} +152 -1
- package/dist/native/tui-darwin-arm64/diffing-tui +0 -0
- package/dist/native/tui-darwin-x64/diffing-tui +0 -0
- package/dist/native/tui-linux-arm64-gnu/diffing-tui +0 -0
- package/dist/native/tui-linux-arm64-musl/diffing-tui +0 -0
- package/dist/native/tui-linux-x64-gnu/diffing-tui +0 -0
- package/dist/native/tui-linux-x64-musl/diffing-tui +0 -0
- package/dist/native/tui-win32-x64-msvc/diffing-tui.exe +0 -0
- package/dist/{server-BqCIWbuc.mjs → server-CH-UPGFR.mjs} +2 -153
- package/extensions/pi/index.ts +128 -61
- package/extensions/pi/skill-heal.ts +73 -0
- package/extensions/pi/tsconfig.json +2 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { f as getProjectStorageDir, h as getRepoRoot } from "./git-BGj31GX-.mjs";
|
|
2
2
|
import { i as loadSettings, n as formatModeLabel, s as saveSettings } from "./settings-DIa-3C8o.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { a as formatPlanReview, m as slugifyScreenId, r as formatMockupReview, t as formatSubmitHints } from "./mockup-lint-BRjhxXSw.mjs";
|
|
4
4
|
import { t as formatComments } from "./comment-format-DS1wOWj_.mjs";
|
|
5
5
|
import { u as resolveActiveServerLock, x as SESSION_TOKEN_HEADER } from "./server-lock-ACw2cKDI.mjs";
|
|
6
6
|
import { i as reviewSessionUrl, t as appendSessionToken } from "./session-url-CyBpcLAH.mjs";
|
|
@@ -1146,6 +1146,13 @@ function warnMockupPathsInRepo(paths) {
|
|
|
1146
1146
|
if (abs === root || abs.startsWith(rootPrefix)) console.error(`Warning: ${abs} is inside the consumer repo. Never write diffing mockups into the working tree. Submit HTML via stdin or MCP submit_mockup({ html }), or stage files under ${storage}/mockup-sources/.`);
|
|
1147
1147
|
}
|
|
1148
1148
|
}
|
|
1149
|
+
function printMockupHints(payload) {
|
|
1150
|
+
if (!payload || typeof payload !== "object") return;
|
|
1151
|
+
const hints = payload.hints;
|
|
1152
|
+
if (!hints?.length) return;
|
|
1153
|
+
const text = formatSubmitHints(hints).trim();
|
|
1154
|
+
if (text) console.error(text);
|
|
1155
|
+
}
|
|
1149
1156
|
async function mockupSubmit(args) {
|
|
1150
1157
|
const { values, positionals } = parseArgs({
|
|
1151
1158
|
args,
|
|
@@ -1218,6 +1225,7 @@ async function mockupSubmit(args) {
|
|
|
1218
1225
|
const mockup = await submitRes.json();
|
|
1219
1226
|
console.error(`Submitted mockup ${mockup.id} (v${mockup.version}) — review at ${appendSessionToken(`${base}/mockup/${mockup.id}`, activeAuthToken)}`);
|
|
1220
1227
|
if (mockup.sourcePath) console.error(`Source path: ${mockup.sourcePath}`);
|
|
1228
|
+
printMockupHints(mockup);
|
|
1221
1229
|
if (!values.wait) {
|
|
1222
1230
|
console.error(CLI_MOCKUP_SUBMIT_PARK_HINT);
|
|
1223
1231
|
process.stdout.write(mockup.id + "\n");
|
|
@@ -1579,6 +1587,7 @@ One-screen revision. Version bumps on success; an --expected-version guard abort
|
|
|
1579
1587
|
}
|
|
1580
1588
|
const mockup = await res.json();
|
|
1581
1589
|
console.error(`Upserted screen ${screenId} on ${mockupId} → v${mockup.version} (${mockup.screens.length} screens).`);
|
|
1590
|
+
printMockupHints(mockup);
|
|
1582
1591
|
return EXIT_OK;
|
|
1583
1592
|
}
|
|
1584
1593
|
if (action === "remove") {
|
|
@@ -1616,6 +1625,7 @@ One-screen revision. Version bumps on success; an --expected-version guard abort
|
|
|
1616
1625
|
}
|
|
1617
1626
|
const data = await res.json();
|
|
1618
1627
|
console.error(`Replaced region "${region}" on ${screenId} (${data.occurrences} match(es)) → v${data.mockup.version}.`);
|
|
1628
|
+
printMockupHints(data.mockup);
|
|
1619
1629
|
return EXIT_OK;
|
|
1620
1630
|
}
|
|
1621
1631
|
const text = values.text;
|
|
@@ -1640,6 +1650,7 @@ One-screen revision. Version bumps on success; an --expected-version guard abort
|
|
|
1640
1650
|
}
|
|
1641
1651
|
const data = await res.json();
|
|
1642
1652
|
console.error(`Patched ${screenId} on ${mockupId} (${data.occurrences} exact match(es)) → v${data.mockup.version}.`);
|
|
1653
|
+
printMockupHints(data.mockup);
|
|
1643
1654
|
return EXIT_OK;
|
|
1644
1655
|
}
|
|
1645
1656
|
async function mockupThreadsCmd(args) {
|
|
@@ -1792,6 +1803,7 @@ Staging files, if needed, go under ~/.diffing/<repo>/mockup-sources/ only.
|
|
|
1792
1803
|
One state per screen: never tabs/accordions/toggles/modals/JS content-swapping — each variant is a separate screen.
|
|
1793
1804
|
|
|
1794
1805
|
submit [-] [--title T] [--screen id=path]... [--id ID] [--model M] [--wait]
|
|
1806
|
+
[--mode fragment|document] [--system ID] [--plan-id ID]
|
|
1795
1807
|
# or a path already under ~/.diffing/.../mockup-sources/
|
|
1796
1808
|
await [--timeout N]
|
|
1797
1809
|
list [--json]
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as buildTuiGitDiffArgs, c as printHelp, n as runTerminalDiff, o as intoShowMode, r as validateEnvironment, s as parseDiffOptions, t as startServer } from "./server-CH-UPGFR.mjs";
|
|
3
3
|
import { h as getRepoRoot, r as getBranchName } from "./git-BGj31GX-.mjs";
|
|
4
4
|
import { i as loadSettings } from "./settings-DIa-3C8o.mjs";
|
|
5
5
|
import { _ as generateSessionToken, b as isWildcardBindHost, c as removeServerLockIfOwned, p as writeServerLock, r as diffScopeKey, t as acquireServerStartupLease, u as resolveActiveServerLock } from "./server-lock-ACw2cKDI.mjs";
|
|
@@ -563,11 +563,11 @@ See docs/cli.md §5 (MCP) for the full tool table.`);
|
|
|
563
563
|
console.error("diffing mcp: --repo must be an absolute path");
|
|
564
564
|
process.exit(5);
|
|
565
565
|
}
|
|
566
|
-
const { startMcpServer } = await import("./mcp-
|
|
566
|
+
const { startMcpServer } = await import("./mcp-45VPdWIr.mjs");
|
|
567
567
|
await startMcpServer({ repoPath });
|
|
568
568
|
await new Promise(() => {});
|
|
569
569
|
}
|
|
570
|
-
const { runSubcommand } = await import("./cli-agent-
|
|
570
|
+
const { runSubcommand } = await import("./cli-agent-DV5ZhMX1.mjs");
|
|
571
571
|
const code = await runSubcommand(args[0], args.slice(1));
|
|
572
572
|
process.exit(code);
|
|
573
573
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as buildGitDiffArgs, s as parseDiffOptions, t as startServer } from "./server-CH-UPGFR.mjs";
|
|
2
|
+
import { a as formatPlanReview, r as formatMockupReview, t as formatSubmitHints } from "./mockup-lint-BRjhxXSw.mjs";
|
|
3
3
|
import { t as formatComments } from "./comment-format-DS1wOWj_.mjs";
|
|
4
4
|
import { _ as generateSessionToken, c as removeServerLockIfOwned, i as isLockAlive, p as writeServerLock, r as diffScopeKey, s as removeServerLock, t as acquireServerStartupLease, u as resolveActiveServerLock, x as SESSION_TOKEN_HEADER, y as isLoopbackHost } from "./server-lock-ACw2cKDI.mjs";
|
|
5
5
|
import { i as reviewSessionUrl, n as joinSessionApiUrl, r as reviewSessionBaseUrl, t as appendSessionToken } from "./session-url-CyBpcLAH.mjs";
|
|
@@ -326,4 +326,155 @@ function formatComment(comment, fallbackVersion, includeContext) {
|
|
|
326
326
|
return parts.join("\n");
|
|
327
327
|
}
|
|
328
328
|
//#endregion
|
|
329
|
-
|
|
329
|
+
//#region src/lib/mockup-lint.ts
|
|
330
|
+
const RULES = [
|
|
331
|
+
{
|
|
332
|
+
label: "tabs",
|
|
333
|
+
re: /role\s*=\s*["']tab(list|panel)?["']/i
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
label: "tabs",
|
|
337
|
+
re: /aria-selected\s*=/i
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
label: "tabs",
|
|
341
|
+
re: /data-(?:bs-)?toggle\s*=\s*["'](?:tab|pill)["']/i
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
label: "tabs",
|
|
345
|
+
re: /\bdata-tabs?\s*=/i
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
label: "accordion",
|
|
349
|
+
re: /<details[\s>]/i
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
label: "accordion",
|
|
353
|
+
re: /data-(?:bs-)?toggle\s*=\s*["']collapse["']/i
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
label: "accordion",
|
|
357
|
+
re: /class\s*=\s*["'][^"']*\baccordion\b/i
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
label: "modal",
|
|
361
|
+
re: /role\s*=\s*["'](?:alert)?dialog["']/i
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
label: "modal",
|
|
365
|
+
re: /data-(?:bs-)?toggle\s*=\s*["']modal["']/i
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
label: "modal",
|
|
369
|
+
re: /class\s*=\s*["'][^"']*\bmodal\b/i
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
label: "dropdown",
|
|
373
|
+
re: /role\s*=\s*["']menu(?:bar)?["']/i
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
label: "dropdown",
|
|
377
|
+
re: /data-(?:bs-)?toggle\s*=\s*["']dropdown["']/i
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
label: "dropdown",
|
|
381
|
+
re: /class\s*=\s*["'][^"']*\bdropdown\b/i
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
label: "toggle",
|
|
385
|
+
re: /role\s*=\s*["']switch["']/i
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
label: "toggle",
|
|
389
|
+
re: /class\s*=\s*["'][^"']*\b(?:toggle|switch)\b/i
|
|
390
|
+
}
|
|
391
|
+
];
|
|
392
|
+
const ORDERED_LABELS = [
|
|
393
|
+
"tabs",
|
|
394
|
+
"accordion",
|
|
395
|
+
"modal",
|
|
396
|
+
"dropdown",
|
|
397
|
+
"toggle"
|
|
398
|
+
];
|
|
399
|
+
/** Find the in-page state UI patterns present in one screen's HTML. */
|
|
400
|
+
function detectInPageState(html) {
|
|
401
|
+
const found = /* @__PURE__ */ new Set();
|
|
402
|
+
for (const rule of RULES) {
|
|
403
|
+
if (found.has(rule.label)) continue;
|
|
404
|
+
if (rule.re.test(html)) found.add(rule.label);
|
|
405
|
+
}
|
|
406
|
+
return ORDERED_LABELS.filter((label) => found.has(label));
|
|
407
|
+
}
|
|
408
|
+
const STYLE_RULES = [
|
|
409
|
+
{
|
|
410
|
+
label: "tailwind-cdn",
|
|
411
|
+
re: /cdn\.tailwindcss\.com/i
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
label: "tailwind-cdn",
|
|
415
|
+
re: /(?:unpkg\.com|cdn\.jsdelivr\.net|esm\.sh)\/(?:npm\/)?tailwindcss/i
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
label: "google-fonts",
|
|
419
|
+
re: /fonts\.googleapis\.com/i
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
label: "google-fonts",
|
|
423
|
+
re: /fonts\.gstatic\.com/i
|
|
424
|
+
}
|
|
425
|
+
];
|
|
426
|
+
const GENERIC_FONT_RE = /font-family\s*:\s*[^;{]*\bInter\b/i;
|
|
427
|
+
const GENERIC_INDIGO_RE = /#(?:4f46e5|4338ca|6366f1|818cf8|7c3aed|8b5cf6|a78bfa)\b|\b(?:indigo|violet)-(?:[4-9]00|950)\b/i;
|
|
428
|
+
/** Off-brand / generic-AI styling that should not ship as the product look. */
|
|
429
|
+
function detectGenericStyle(html) {
|
|
430
|
+
const found = /* @__PURE__ */ new Set();
|
|
431
|
+
for (const rule of STYLE_RULES) {
|
|
432
|
+
if (found.has(rule.label)) continue;
|
|
433
|
+
if (rule.re.test(html)) found.add(rule.label);
|
|
434
|
+
}
|
|
435
|
+
if (GENERIC_FONT_RE.test(html) && GENERIC_INDIGO_RE.test(html)) found.add("generic-palette");
|
|
436
|
+
return [
|
|
437
|
+
"tailwind-cdn",
|
|
438
|
+
"google-fonts",
|
|
439
|
+
"generic-palette"
|
|
440
|
+
].filter((label) => found.has(label));
|
|
441
|
+
}
|
|
442
|
+
/** Build submit-time hints for a set of screens (empty when all clean). */
|
|
443
|
+
function lintMockupScreens(screens) {
|
|
444
|
+
const hints = [];
|
|
445
|
+
for (const screen of screens) {
|
|
446
|
+
const state = detectInPageState(screen.html);
|
|
447
|
+
if (state.length > 0) hints.push({
|
|
448
|
+
screenId: screen.id,
|
|
449
|
+
kind: "state",
|
|
450
|
+
patterns: state,
|
|
451
|
+
message: `Screen "${screen.id}" contains in-page state UI (${state.join(", ")}). Split each state into its own screen instead.`
|
|
452
|
+
});
|
|
453
|
+
const style = detectGenericStyle(screen.html);
|
|
454
|
+
if (style.length > 0) hints.push({
|
|
455
|
+
screenId: screen.id,
|
|
456
|
+
kind: "style",
|
|
457
|
+
patterns: style,
|
|
458
|
+
message: `Screen "${screen.id}" looks generic (${style.join(", ")}). Drop CDN Tailwind / Google Fonts and do not invent Inter + indigo — match the product.`
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return hints;
|
|
462
|
+
}
|
|
463
|
+
/** Compact submit/revise warning for CLI and MCP. */
|
|
464
|
+
function formatSubmitHints(hints) {
|
|
465
|
+
if (hints.length === 0) return "";
|
|
466
|
+
const state = hints.filter((h) => h.kind !== "style");
|
|
467
|
+
const style = hints.filter((h) => h.kind === "style");
|
|
468
|
+
const lines = [];
|
|
469
|
+
if (state.length > 0) {
|
|
470
|
+
lines.push("⚠️ In-page state UI detected — split each state into its own screen:");
|
|
471
|
+
for (const hint of state) lines.push(` • ${hint.screenId}: ${hint.patterns.join(", ")}`);
|
|
472
|
+
}
|
|
473
|
+
if (style.length > 0) {
|
|
474
|
+
lines.push("⚠️ Generic styling — match the product (no CDN Tailwind, Google Fonts, or Inter+indigo):");
|
|
475
|
+
for (const hint of style) lines.push(` • ${hint.screenId}: ${hint.patterns.join(", ")}`);
|
|
476
|
+
}
|
|
477
|
+
return `\n${lines.join("\n")}`;
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
480
|
+
export { formatPlanReview as a, MOCKUP_SCREEN_ID_RE as c, isMockupViewport as d, normalizeMockupViewport as f, extractPlanLines as i, commentViewport as l, slugifyScreenId as m, lintMockupScreens as n, sectionTitleForLine as o, normalizeSubmitScreens as p, formatMockupReview as r, MOCKUP_MAX_SCREEN_BYTES as s, formatSubmitHints as t, defaultScreenLabel as u };
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { C as listRepoFiles, D as toSafeRelativePath, E as isSafePath, S as isGitRepo, T as revertHunk, _ as getShowDiff, a as getCommitSeriesSummary, b as getUntrackedFilePathsAsync, d as getMergeStatus, f as getProjectStorageDir, h as getRepoRoot, l as getGitDiffAsync, m as getRepoMetadataAsync, o as getCustomGitDiffAsync, s as getFileContent, u as getHunkHistory, x as gitAddFile, y as getTabSizeForFilesAsync } from "./git-BGj31GX-.mjs";
|
|
2
2
|
import { a as searchFiles, i as searchContent, o as searchSymbols, r as searchAll, s as trackSelection, t as getSearchStatus } from "./search-3R2E5xxr.mjs";
|
|
3
3
|
import { c as writeJsonAtomically, i as loadSettings, s as saveSettings } from "./settings-DIa-3C8o.mjs";
|
|
4
|
-
import { a as
|
|
4
|
+
import { a as formatPlanReview, c as MOCKUP_SCREEN_ID_RE, d as isMockupViewport, f as normalizeMockupViewport, i as extractPlanLines, l as commentViewport, n as lintMockupScreens, o as sectionTitleForLine, p as normalizeSubmitScreens, r as formatMockupReview, s as MOCKUP_MAX_SCREEN_BYTES, u as defaultScreenLabel } from "./mockup-lint-BRjhxXSw.mjs";
|
|
5
5
|
import { a as slugifyDesignId, i as emptyTokens, o as snapshotRevision, r as cloneTokens, s as tokensToCss, t as DEFAULT_DESIGN_SYSTEM_ID } from "./design-system-types-uiP9W2uj.mjs";
|
|
6
6
|
import { t as formatComments } from "./comment-format-DS1wOWj_.mjs";
|
|
7
7
|
import { g as createServerAuthMiddleware, h as buildSessionTokenSetCookieValue, v as injectSessionTokenIntoHtml } from "./server-lock-ACw2cKDI.mjs";
|
|
@@ -3006,157 +3006,6 @@ function injectMockupProbe(html, opts = {}) {
|
|
|
3006
3006
|
return script + html;
|
|
3007
3007
|
}
|
|
3008
3008
|
//#endregion
|
|
3009
|
-
//#region src/lib/mockup-lint.ts
|
|
3010
|
-
const RULES$1 = [
|
|
3011
|
-
{
|
|
3012
|
-
label: "tabs",
|
|
3013
|
-
re: /role\s*=\s*["']tab(list|panel)?["']/i
|
|
3014
|
-
},
|
|
3015
|
-
{
|
|
3016
|
-
label: "tabs",
|
|
3017
|
-
re: /aria-selected\s*=/i
|
|
3018
|
-
},
|
|
3019
|
-
{
|
|
3020
|
-
label: "tabs",
|
|
3021
|
-
re: /data-(?:bs-)?toggle\s*=\s*["'](?:tab|pill)["']/i
|
|
3022
|
-
},
|
|
3023
|
-
{
|
|
3024
|
-
label: "tabs",
|
|
3025
|
-
re: /\bdata-tabs?\s*=/i
|
|
3026
|
-
},
|
|
3027
|
-
{
|
|
3028
|
-
label: "accordion",
|
|
3029
|
-
re: /<details[\s>]/i
|
|
3030
|
-
},
|
|
3031
|
-
{
|
|
3032
|
-
label: "accordion",
|
|
3033
|
-
re: /data-(?:bs-)?toggle\s*=\s*["']collapse["']/i
|
|
3034
|
-
},
|
|
3035
|
-
{
|
|
3036
|
-
label: "accordion",
|
|
3037
|
-
re: /class\s*=\s*["'][^"']*\baccordion\b/i
|
|
3038
|
-
},
|
|
3039
|
-
{
|
|
3040
|
-
label: "modal",
|
|
3041
|
-
re: /role\s*=\s*["'](?:alert)?dialog["']/i
|
|
3042
|
-
},
|
|
3043
|
-
{
|
|
3044
|
-
label: "modal",
|
|
3045
|
-
re: /data-(?:bs-)?toggle\s*=\s*["']modal["']/i
|
|
3046
|
-
},
|
|
3047
|
-
{
|
|
3048
|
-
label: "modal",
|
|
3049
|
-
re: /class\s*=\s*["'][^"']*\bmodal\b/i
|
|
3050
|
-
},
|
|
3051
|
-
{
|
|
3052
|
-
label: "dropdown",
|
|
3053
|
-
re: /role\s*=\s*["']menu(?:bar)?["']/i
|
|
3054
|
-
},
|
|
3055
|
-
{
|
|
3056
|
-
label: "dropdown",
|
|
3057
|
-
re: /data-(?:bs-)?toggle\s*=\s*["']dropdown["']/i
|
|
3058
|
-
},
|
|
3059
|
-
{
|
|
3060
|
-
label: "dropdown",
|
|
3061
|
-
re: /class\s*=\s*["'][^"']*\bdropdown\b/i
|
|
3062
|
-
},
|
|
3063
|
-
{
|
|
3064
|
-
label: "toggle",
|
|
3065
|
-
re: /role\s*=\s*["']switch["']/i
|
|
3066
|
-
},
|
|
3067
|
-
{
|
|
3068
|
-
label: "toggle",
|
|
3069
|
-
re: /class\s*=\s*["'][^"']*\b(?:toggle|switch)\b/i
|
|
3070
|
-
}
|
|
3071
|
-
];
|
|
3072
|
-
const ORDERED_LABELS = [
|
|
3073
|
-
"tabs",
|
|
3074
|
-
"accordion",
|
|
3075
|
-
"modal",
|
|
3076
|
-
"dropdown",
|
|
3077
|
-
"toggle"
|
|
3078
|
-
];
|
|
3079
|
-
/** Find the in-page state UI patterns present in one screen's HTML. */
|
|
3080
|
-
function detectInPageState(html) {
|
|
3081
|
-
const found = /* @__PURE__ */ new Set();
|
|
3082
|
-
for (const rule of RULES$1) {
|
|
3083
|
-
if (found.has(rule.label)) continue;
|
|
3084
|
-
if (rule.re.test(html)) found.add(rule.label);
|
|
3085
|
-
}
|
|
3086
|
-
return ORDERED_LABELS.filter((label) => found.has(label));
|
|
3087
|
-
}
|
|
3088
|
-
const STYLE_RULES = [
|
|
3089
|
-
{
|
|
3090
|
-
label: "tailwind-cdn",
|
|
3091
|
-
re: /cdn\.tailwindcss\.com/i
|
|
3092
|
-
},
|
|
3093
|
-
{
|
|
3094
|
-
label: "tailwind-cdn",
|
|
3095
|
-
re: /(?:unpkg\.com|cdn\.jsdelivr\.net|esm\.sh)\/(?:npm\/)?tailwindcss/i
|
|
3096
|
-
},
|
|
3097
|
-
{
|
|
3098
|
-
label: "google-fonts",
|
|
3099
|
-
re: /fonts\.googleapis\.com/i
|
|
3100
|
-
},
|
|
3101
|
-
{
|
|
3102
|
-
label: "google-fonts",
|
|
3103
|
-
re: /fonts\.gstatic\.com/i
|
|
3104
|
-
}
|
|
3105
|
-
];
|
|
3106
|
-
const GENERIC_FONT_RE = /font-family\s*:\s*[^;{]*\bInter\b/i;
|
|
3107
|
-
const GENERIC_INDIGO_RE = /#(?:4f46e5|4338ca|6366f1|818cf8|7c3aed|8b5cf6|a78bfa)\b|\b(?:indigo|violet)-(?:[4-9]00|950)\b/i;
|
|
3108
|
-
/** Off-brand / generic-AI styling that should not ship as the product look. */
|
|
3109
|
-
function detectGenericStyle(html) {
|
|
3110
|
-
const found = /* @__PURE__ */ new Set();
|
|
3111
|
-
for (const rule of STYLE_RULES) {
|
|
3112
|
-
if (found.has(rule.label)) continue;
|
|
3113
|
-
if (rule.re.test(html)) found.add(rule.label);
|
|
3114
|
-
}
|
|
3115
|
-
if (GENERIC_FONT_RE.test(html) && GENERIC_INDIGO_RE.test(html)) found.add("generic-palette");
|
|
3116
|
-
return [
|
|
3117
|
-
"tailwind-cdn",
|
|
3118
|
-
"google-fonts",
|
|
3119
|
-
"generic-palette"
|
|
3120
|
-
].filter((label) => found.has(label));
|
|
3121
|
-
}
|
|
3122
|
-
/** Build submit-time hints for a set of screens (empty when all clean). */
|
|
3123
|
-
function lintMockupScreens(screens) {
|
|
3124
|
-
const hints = [];
|
|
3125
|
-
for (const screen of screens) {
|
|
3126
|
-
const state = detectInPageState(screen.html);
|
|
3127
|
-
if (state.length > 0) hints.push({
|
|
3128
|
-
screenId: screen.id,
|
|
3129
|
-
kind: "state",
|
|
3130
|
-
patterns: state,
|
|
3131
|
-
message: `Screen "${screen.id}" contains in-page state UI (${state.join(", ")}). Split each state into its own screen instead.`
|
|
3132
|
-
});
|
|
3133
|
-
const style = detectGenericStyle(screen.html);
|
|
3134
|
-
if (style.length > 0) hints.push({
|
|
3135
|
-
screenId: screen.id,
|
|
3136
|
-
kind: "style",
|
|
3137
|
-
patterns: style,
|
|
3138
|
-
message: `Screen "${screen.id}" looks generic (${style.join(", ")}). Drop CDN Tailwind / Google Fonts and do not invent Inter + indigo — match the product.`
|
|
3139
|
-
});
|
|
3140
|
-
}
|
|
3141
|
-
return hints;
|
|
3142
|
-
}
|
|
3143
|
-
/** Compact submit/revise warning for CLI and MCP. */
|
|
3144
|
-
function formatSubmitHints(hints) {
|
|
3145
|
-
if (hints.length === 0) return "";
|
|
3146
|
-
const state = hints.filter((h) => h.kind !== "style");
|
|
3147
|
-
const style = hints.filter((h) => h.kind === "style");
|
|
3148
|
-
const lines = [];
|
|
3149
|
-
if (state.length > 0) {
|
|
3150
|
-
lines.push("⚠️ In-page state UI detected — split each state into its own screen:");
|
|
3151
|
-
for (const hint of state) lines.push(` • ${hint.screenId}: ${hint.patterns.join(", ")}`);
|
|
3152
|
-
}
|
|
3153
|
-
if (style.length > 0) {
|
|
3154
|
-
lines.push("⚠️ Generic styling — match the product (no CDN Tailwind, Google Fonts, or Inter+indigo):");
|
|
3155
|
-
for (const hint of style) lines.push(` • ${hint.screenId}: ${hint.patterns.join(", ")}`);
|
|
3156
|
-
}
|
|
3157
|
-
return `\n${lines.join("\n")}`;
|
|
3158
|
-
}
|
|
3159
|
-
//#endregion
|
|
3160
3009
|
//#region src/lib/design-system.ts
|
|
3161
3010
|
function newId() {
|
|
3162
3011
|
return crypto.randomUUID();
|
|
@@ -7769,4 +7618,4 @@ async function startServer(options) {
|
|
|
7769
7618
|
});
|
|
7770
7619
|
}
|
|
7771
7620
|
//#endregion
|
|
7772
|
-
export {
|
|
7621
|
+
export { buildTuiGitDiffArgs as a, printHelp as c, buildGitDiffArgs as i, runTerminalDiff as n, intoShowMode as o, validateEnvironment as r, parseDiffOptions as s, startServer as t };
|
package/extensions/pi/index.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Bridges the local-first diffing review loop into pi:
|
|
5
5
|
* - Structured LLM tools mirroring the diffing MCP surface (status, review
|
|
6
|
-
* start, comments, reply/resolve, plan loop, mockup loop,
|
|
7
|
-
* sessions, GH PR).
|
|
6
|
+
* start, comments, reply/resolve, plan loop, mockup loop, design system,
|
|
7
|
+
* progress, sessions, GH PR).
|
|
8
8
|
* - `/diffing` command to open/reuse the review UI for the current repo.
|
|
9
9
|
* - Footer status showing the active review session.
|
|
10
10
|
* - Skill self-heal: keeps `~/.agents/skills/diffing*` as symlinks to the
|
|
@@ -20,12 +20,9 @@
|
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
21
|
import {
|
|
22
22
|
existsSync,
|
|
23
|
-
lstatSync,
|
|
24
23
|
mkdtempSync,
|
|
25
24
|
readFileSync,
|
|
26
|
-
realpathSync,
|
|
27
25
|
rmSync,
|
|
28
|
-
symlinkSync,
|
|
29
26
|
writeFileSync,
|
|
30
27
|
} from "node:fs";
|
|
31
28
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -37,30 +34,19 @@ import type {
|
|
|
37
34
|
ExtensionContext,
|
|
38
35
|
} from "@earendil-works/pi-coding-agent";
|
|
39
36
|
import { Type } from "typebox";
|
|
37
|
+
import {
|
|
38
|
+
selfHealSkillLinks,
|
|
39
|
+
SKILLS_REL,
|
|
40
|
+
} from "./skill-heal.ts";
|
|
40
41
|
|
|
41
42
|
// ────────────────────────────────────────────────────────────────────────────
|
|
42
43
|
// Constants
|
|
43
44
|
// ────────────────────────────────────────────────────────────────────────────
|
|
44
45
|
|
|
45
|
-
const SKILL_NAMES = [
|
|
46
|
-
"diffing",
|
|
47
|
-
"diffing-finish-review",
|
|
48
|
-
"diffing-mockup-author",
|
|
49
|
-
"diffing-mockup-review",
|
|
50
|
-
"diffing-plan-review",
|
|
51
|
-
"diffing-pr-address",
|
|
52
|
-
"diffing-pr-read",
|
|
53
|
-
"diffing-release",
|
|
54
|
-
"diffing-review",
|
|
55
|
-
"diffing-start-review",
|
|
56
|
-
] as const;
|
|
57
|
-
|
|
58
46
|
const MAX_OUTPUT_BYTES = 48 * 1024;
|
|
59
47
|
const REVIEW_START_TIMEOUT_MS = 15_000;
|
|
60
48
|
const REVIEW_START_POLL_MS = 400;
|
|
61
49
|
|
|
62
|
-
const SKILLS_REL = join(".agents", "skills");
|
|
63
|
-
|
|
64
50
|
// ────────────────────────────────────────────────────────────────────────────
|
|
65
51
|
// CLI runner
|
|
66
52
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -248,35 +234,6 @@ function findCanonicalRoot(cwd: string): string | null {
|
|
|
248
234
|
return null;
|
|
249
235
|
}
|
|
250
236
|
|
|
251
|
-
/**
|
|
252
|
-
* Keep the `~/.agents/skills/diffing*` entries as symlinks to the canonical
|
|
253
|
-
* checkout's `.agents/skills`. pi dedupes skills by canonical realpath, so
|
|
254
|
-
* symlinked home entries merge silently with the repo's project skills and no
|
|
255
|
-
* `[Skill conflicts]` banner appears. Repairs stale real copies left behind by
|
|
256
|
-
* `diffing setup skills` / `npx skills add --copy`. Only touches the known
|
|
257
|
-
* names in SKILL_NAMES; never deletes anything else.
|
|
258
|
-
*/
|
|
259
|
-
function selfHealSkillLinks(canonical: string): void {
|
|
260
|
-
const homeSkills = join(homedir(), ".agents", "skills");
|
|
261
|
-
if (!existsSync(homeSkills)) return;
|
|
262
|
-
for (const name of SKILL_NAMES) {
|
|
263
|
-
const target = join(canonical, SKILLS_REL, name);
|
|
264
|
-
if (!existsSync(target)) continue;
|
|
265
|
-
const link = join(homeSkills, name);
|
|
266
|
-
try {
|
|
267
|
-
if (existsSync(link) || lstatSync(link)) {
|
|
268
|
-
if (lstatSync(link).isSymbolicLink()) {
|
|
269
|
-
const real = realpathSync(link);
|
|
270
|
-
if (real === realpathSync(target)) continue; // already correct
|
|
271
|
-
}
|
|
272
|
-
rmSync(link, { recursive: true, force: true });
|
|
273
|
-
}
|
|
274
|
-
symlinkSync(target, link, "dir");
|
|
275
|
-
} catch {
|
|
276
|
-
// best-effort; a failed heal must not break the session
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
237
|
|
|
281
238
|
// ────────────────────────────────────────────────────────────────────────────
|
|
282
239
|
// Detached review server start
|
|
@@ -340,7 +297,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
340
297
|
// Keep the home skill links canonical for every session (idempotent).
|
|
341
298
|
pi.on("session_start", async (_event, ctx) => {
|
|
342
299
|
const canonical = findCanonicalRoot(ctx.cwd);
|
|
343
|
-
if (canonical)
|
|
300
|
+
if (canonical) {
|
|
301
|
+
selfHealSkillLinks(canonical, join(homedir(), ".agents", "skills"));
|
|
302
|
+
}
|
|
344
303
|
await refreshStatus(ctx);
|
|
345
304
|
});
|
|
346
305
|
|
|
@@ -825,7 +784,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
825
784
|
name: "diffing_mockup_submit",
|
|
826
785
|
label: "Diffing Mockup Submit",
|
|
827
786
|
description:
|
|
828
|
-
"Submit HTML mockup screen(s) for visual review.
|
|
787
|
+
"Submit HTML mockup screen(s) for visual review. HARD RULE — one state per screen: never tabs/accordions/toggles/modals/JS swaps. Pass html, a file/dir already under ~/.diffing/…/mockup-sources/, or screens [{id, html}]. Call diffing_design show first. Share the URL and park unless asked to wait. Resubmit with mockupId to bump the version. Fix any in-page-state or generic-style hints before parking.",
|
|
829
788
|
parameters: Type.Object({
|
|
830
789
|
html: Type.Optional(
|
|
831
790
|
Type.String({ description: "Single-screen HTML body." }),
|
|
@@ -854,6 +813,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
854
813
|
),
|
|
855
814
|
model: Type.Optional(Type.String()),
|
|
856
815
|
source: Type.Optional(Type.String()),
|
|
816
|
+
mode: Type.Optional(
|
|
817
|
+
StringEnum(["fragment", "document"] as const, {
|
|
818
|
+
description:
|
|
819
|
+
"fragment = body contents wrapped by the published design-system shell. document = full HTML, no wrap. Default: server default.",
|
|
820
|
+
}),
|
|
821
|
+
),
|
|
822
|
+
designSystem: Type.Optional(
|
|
823
|
+
Type.String({
|
|
824
|
+
description:
|
|
825
|
+
"Design-system id to bind (default system if omitted).",
|
|
826
|
+
}),
|
|
827
|
+
),
|
|
828
|
+
planId: Type.Optional(
|
|
829
|
+
Type.String({
|
|
830
|
+
description: "Optional plan id this mockup illustrates.",
|
|
831
|
+
}),
|
|
832
|
+
),
|
|
857
833
|
}),
|
|
858
834
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
859
835
|
const args = ["mockup", "submit"];
|
|
@@ -896,6 +872,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
896
872
|
if (params.mockupId) args.push("--id", params.mockupId);
|
|
897
873
|
if (params.model) args.push("--model", params.model);
|
|
898
874
|
if (params.source) args.push("--source", params.source);
|
|
875
|
+
if (params.mode) args.push("--mode", params.mode);
|
|
876
|
+
if (params.designSystem) args.push("--system", params.designSystem);
|
|
877
|
+
if (params.planId) args.push("--plan-id", params.planId);
|
|
899
878
|
try {
|
|
900
879
|
const result = await runDiffing(args, ctx.cwd, { stdin });
|
|
901
880
|
const stdout = result.stdout.trim();
|
|
@@ -1079,12 +1058,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1079
1058
|
name: "diffing_mockup_inspect",
|
|
1080
1059
|
label: "Diffing Mockup Inspect",
|
|
1081
1060
|
description:
|
|
1082
|
-
"Read compact, bounded mockup data without transferring screen HTML. view=summary (headline stats), comments (paged comment list), comment (one thread), screen (screen source). Filter by comment scope: status, screenId, viewport (desktop|tablet|mobile), version. context=none|anchor|source controls anchor detail. Prefer this over diffing_mockup_show.",
|
|
1061
|
+
"Read compact, bounded mockup data without transferring screen HTML. view=summary (headline stats), comments (paged comment list), comment (one thread), screen (screen source), preview (rendered preview metadata). Filter by comment scope: status, screenId, viewport (desktop|tablet|mobile), version. context=none|anchor|source controls anchor detail. Prefer this over diffing_mockup_show.",
|
|
1083
1062
|
parameters: Type.Object({
|
|
1084
|
-
view: StringEnum(
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1063
|
+
view: StringEnum(
|
|
1064
|
+
["summary", "comments", "comment", "screen", "preview"] as const,
|
|
1065
|
+
{
|
|
1066
|
+
description: "What to read. Default: summary.",
|
|
1067
|
+
default: "summary",
|
|
1068
|
+
},
|
|
1069
|
+
),
|
|
1088
1070
|
mockupId: Type.Optional(
|
|
1089
1071
|
Type.String({ description: "Mockup id. Default: latest." }),
|
|
1090
1072
|
),
|
|
@@ -1152,11 +1134,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1152
1134
|
name: "diffing_mockup_screen",
|
|
1153
1135
|
label: "Diffing Mockup Screen",
|
|
1154
1136
|
description:
|
|
1155
|
-
"One-screen revision of an HTML mockup. action=upsert adds/replaces a screen (html or file), remove deletes a screen, patch replaces the first exact occurrence of text with replacement. Every success bumps the mockup version; pass expectedVersion to guard racing edits (409 version-mismatch, nothing applied). For multi-screen revisions, resubmit via diffing_mockup_submit with the same mockupId.",
|
|
1137
|
+
"One-screen revision of an HTML mockup. action=upsert adds/replaces a screen (html or file), remove deletes a screen, patch replaces the first exact occurrence of text with replacement, replace-region replaces the inner HTML of the first [data-diffing=region] element (prefer this when the comment has a data-diffing target). Every success bumps the mockup version; pass expectedVersion to guard racing edits (409 version-mismatch, nothing applied). For multi-screen revisions, resubmit via diffing_mockup_submit with the same mockupId.",
|
|
1156
1138
|
parameters: Type.Object({
|
|
1157
|
-
action: StringEnum(
|
|
1158
|
-
|
|
1159
|
-
|
|
1139
|
+
action: StringEnum(
|
|
1140
|
+
["upsert", "remove", "patch", "replace-region"] as const,
|
|
1141
|
+
{ description: "Revision op." },
|
|
1142
|
+
),
|
|
1160
1143
|
mockupId: Type.String({ description: "Mockup id." }),
|
|
1161
1144
|
screenId: Type.String({ description: "Screen id." }),
|
|
1162
1145
|
html: Type.Optional(
|
|
@@ -1177,8 +1160,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1177
1160
|
description: "For patch: exact text to replace (first occurrence).",
|
|
1178
1161
|
}),
|
|
1179
1162
|
),
|
|
1163
|
+
region: Type.Optional(
|
|
1164
|
+
Type.String({
|
|
1165
|
+
description:
|
|
1166
|
+
"For replace-region: data-diffing attribute value of the block to replace.",
|
|
1167
|
+
}),
|
|
1168
|
+
),
|
|
1180
1169
|
replacement: Type.Optional(
|
|
1181
|
-
Type.String({
|
|
1170
|
+
Type.String({
|
|
1171
|
+
description:
|
|
1172
|
+
"For patch: replacement text. For replace-region: new inner HTML.",
|
|
1173
|
+
}),
|
|
1182
1174
|
),
|
|
1183
1175
|
expectedVersion: Type.Optional(
|
|
1184
1176
|
Type.Number({
|
|
@@ -1202,6 +1194,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1202
1194
|
stdin = params.html;
|
|
1203
1195
|
}
|
|
1204
1196
|
if (params.label) args.push("--label", params.label);
|
|
1197
|
+
} else if (params.action === "replace-region") {
|
|
1198
|
+
if (params.region) args.push("--region", params.region);
|
|
1199
|
+
if (params.replacement !== undefined)
|
|
1200
|
+
args.push("--replacement", params.replacement);
|
|
1205
1201
|
} else if (params.action === "patch") {
|
|
1206
1202
|
if (params.text !== undefined) args.push("--text", params.text);
|
|
1207
1203
|
if (params.replacement !== undefined)
|
|
@@ -1264,6 +1260,77 @@ export default function (pi: ExtensionAPI) {
|
|
|
1264
1260
|
},
|
|
1265
1261
|
});
|
|
1266
1262
|
|
|
1263
|
+
pi.registerTool({
|
|
1264
|
+
name: "diffing_mockup_handoff",
|
|
1265
|
+
label: "Diffing Mockup Handoff",
|
|
1266
|
+
description:
|
|
1267
|
+
"Compact implementation handoff after a mockup is approved: tokens, screens with intent, components used, leftover nits. Prefer this over dumping every screen's HTML. Call after decision=approved, before writing product UI.",
|
|
1268
|
+
parameters: Type.Object({
|
|
1269
|
+
mockupId: Type.Optional(
|
|
1270
|
+
Type.String({ description: "Mockup id. Default: latest." }),
|
|
1271
|
+
),
|
|
1272
|
+
json: Type.Optional(
|
|
1273
|
+
Type.Boolean({ description: "Emit raw JSON. Default: XML handoff." }),
|
|
1274
|
+
),
|
|
1275
|
+
}),
|
|
1276
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1277
|
+
const args = ["mockup", "handoff"];
|
|
1278
|
+
if (params.mockupId) args.push(params.mockupId);
|
|
1279
|
+
if (params.json) args.push("--json");
|
|
1280
|
+
const result = await runDiffing(args, ctx.cwd);
|
|
1281
|
+
return textResult(describe(result, "diffing mockup handoff"), {
|
|
1282
|
+
exitCode: result.exitCode,
|
|
1283
|
+
stderr: result.stderr.trim() || undefined,
|
|
1284
|
+
});
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
pi.registerTool({
|
|
1289
|
+
name: "diffing_design",
|
|
1290
|
+
label: "Diffing Design System",
|
|
1291
|
+
description:
|
|
1292
|
+
"Per-repo design system used before authoring mockup HTML. show/list: read tokens, guidelines, components. extract: scan the consumer repo and write a draft (does not publish). propose: update the draft. publish: human action only — do not publish unless the human asked. Omit id for the default system.",
|
|
1293
|
+
parameters: Type.Object({
|
|
1294
|
+
action: StringEnum(
|
|
1295
|
+
["show", "list", "extract", "propose", "publish"] as const,
|
|
1296
|
+
{ description: "Design-system op. Default: show." },
|
|
1297
|
+
),
|
|
1298
|
+
id: Type.Optional(
|
|
1299
|
+
Type.String({ description: "System id. Default: default." }),
|
|
1300
|
+
),
|
|
1301
|
+
json: Type.Optional(
|
|
1302
|
+
Type.Boolean({ description: "Emit raw JSON. Default: false." }),
|
|
1303
|
+
),
|
|
1304
|
+
from: Type.Optional(
|
|
1305
|
+
StringEnum(["css", "text"] as const, {
|
|
1306
|
+
description: "For extract: scan source. Default: css.",
|
|
1307
|
+
}),
|
|
1308
|
+
),
|
|
1309
|
+
title: Type.Optional(
|
|
1310
|
+
Type.String({ description: "For extract/propose: system title." }),
|
|
1311
|
+
),
|
|
1312
|
+
guidelines: Type.Optional(
|
|
1313
|
+
Type.String({
|
|
1314
|
+
description: "For propose: markdown guidelines to write on the draft.",
|
|
1315
|
+
}),
|
|
1316
|
+
),
|
|
1317
|
+
}),
|
|
1318
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1319
|
+
const action = params.action ?? "show";
|
|
1320
|
+
const args = ["design", action];
|
|
1321
|
+
if (params.id) args.push(params.id);
|
|
1322
|
+
if (params.json) args.push("--json");
|
|
1323
|
+
if (params.from) args.push("--from", params.from);
|
|
1324
|
+
if (params.title) args.push("--title", params.title);
|
|
1325
|
+
if (params.guidelines) args.push("--guidelines", params.guidelines);
|
|
1326
|
+
const result = await runDiffing(args, ctx.cwd);
|
|
1327
|
+
return textResult(describe(result, `diffing design ${action}`), {
|
|
1328
|
+
exitCode: result.exitCode,
|
|
1329
|
+
stderr: result.stderr.trim() || undefined,
|
|
1330
|
+
});
|
|
1331
|
+
},
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1267
1334
|
pi.registerTool({
|
|
1268
1335
|
name: "diffing_url",
|
|
1269
1336
|
label: "Diffing URL",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
realpathSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
symlinkSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export const DIFFING_SKILL_NAMES = [
|
|
11
|
+
"diffing",
|
|
12
|
+
"diffing-finish-review",
|
|
13
|
+
"diffing-mockup-author",
|
|
14
|
+
"diffing-mockup-review",
|
|
15
|
+
"diffing-plan-review",
|
|
16
|
+
"diffing-pr-address",
|
|
17
|
+
"diffing-pr-read",
|
|
18
|
+
"diffing-release",
|
|
19
|
+
"diffing-review",
|
|
20
|
+
"diffing-start-review",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export const SKILLS_REL = join(".agents", "skills");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Point `link` at `target`. Creates a missing link, repairs a stale/dangling
|
|
27
|
+
* one, and leaves a correct symlink alone. `existsSync` is false for both
|
|
28
|
+
* missing paths and dangling symlinks, so this uses `lstat` and treats ENOENT
|
|
29
|
+
* as "create".
|
|
30
|
+
*/
|
|
31
|
+
export function healSkillLink(
|
|
32
|
+
link: string,
|
|
33
|
+
target: string,
|
|
34
|
+
): "ok" | "created" | "repaired" {
|
|
35
|
+
if (!existsSync(target)) return "ok";
|
|
36
|
+
let st: ReturnType<typeof lstatSync> | null = null;
|
|
37
|
+
try {
|
|
38
|
+
st = lstatSync(link);
|
|
39
|
+
} catch {
|
|
40
|
+
st = null;
|
|
41
|
+
}
|
|
42
|
+
if (st) {
|
|
43
|
+
if (st.isSymbolicLink()) {
|
|
44
|
+
try {
|
|
45
|
+
if (realpathSync(link) === realpathSync(target)) return "ok";
|
|
46
|
+
} catch {
|
|
47
|
+
// dangling or unreadable — fall through and replace
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
rmSync(link, { recursive: true, force: true });
|
|
51
|
+
symlinkSync(target, link, "dir");
|
|
52
|
+
return "repaired";
|
|
53
|
+
}
|
|
54
|
+
symlinkSync(target, link, "dir");
|
|
55
|
+
return "created";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Keep `~/.agents/skills/diffing*` as symlinks to the canonical checkout. */
|
|
59
|
+
export function selfHealSkillLinks(
|
|
60
|
+
canonical: string,
|
|
61
|
+
homeSkills: string,
|
|
62
|
+
): void {
|
|
63
|
+
if (!existsSync(homeSkills)) return;
|
|
64
|
+
for (const name of DIFFING_SKILL_NAMES) {
|
|
65
|
+
const target = join(canonical, SKILLS_REL, name);
|
|
66
|
+
if (!existsSync(target)) continue;
|
|
67
|
+
try {
|
|
68
|
+
healSkillLink(join(homeSkills, name), target);
|
|
69
|
+
} catch {
|
|
70
|
+
// best-effort; a failed heal must not break the session
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
"moduleResolution": "bundler",
|
|
6
6
|
"strict": true,
|
|
7
7
|
"noEmit": true,
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
8
9
|
"skipLibCheck": true,
|
|
9
10
|
"esModuleInterop": true,
|
|
10
11
|
"forceConsistentCasingInFileNames": true,
|
|
11
12
|
"types": ["node"]
|
|
12
13
|
},
|
|
13
|
-
"include": ["index.ts", "pi-modules.d.ts"]
|
|
14
|
+
"include": ["index.ts", "skill-heal.ts", "pi-modules.d.ts"]
|
|
14
15
|
}
|