diffing 0.18.2 → 0.18.4
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 +4 -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 +170 -61
- package/extensions/pi/pi-modules.d.ts +1 -1
- 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
|
}
|
|
@@ -711,6 +711,7 @@ startupLease?.release();
|
|
|
711
711
|
startupLease = null;
|
|
712
712
|
const localUrl = appendSessionToken(`http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}${prMode ? "/gh/pr" : ""}`, authToken ?? void 0);
|
|
713
713
|
console.log(`diffing server running at ${localUrl}`);
|
|
714
|
+
console.error(`DIFFING_READY ${localUrl} mode=${prMode ? "gh-pr" : "web"} pid=${process.pid}`);
|
|
714
715
|
if (openModulePromise) try {
|
|
715
716
|
const settings = loadSettings();
|
|
716
717
|
const openUrl = appendSessionToken(`http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}${prMode ? "/gh/pr" : ""}`, authToken ?? void 0);
|
|
@@ -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
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -208,6 +194,45 @@ async function refreshStatus(ctx: ExtensionContext): Promise<void> {
|
|
|
208
194
|
}
|
|
209
195
|
}
|
|
210
196
|
|
|
197
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
198
|
+
// Verdict notices (herdr-visible)
|
|
199
|
+
//
|
|
200
|
+
// After a review/plan/mockup await returns a real verdict, surface a compact,
|
|
201
|
+
// greppable `DIFFING_VERDICT …` line in the pi pane so a sibling herdr pane can
|
|
202
|
+
// observe review state via `herdr pane read` / `herdr wait output`. We use pi's
|
|
203
|
+
// own UI hooks (notify + widget) rather than `herdr pane send-text`, which would
|
|
204
|
+
// type raw bytes into the pi TUI input box instead of the rendered scrollback.
|
|
205
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
interface VerdictNotice {
|
|
208
|
+
kind: "plan" | "mockup" | "review";
|
|
209
|
+
decision: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function verdictNotice(stderr: string): VerdictNotice | null {
|
|
213
|
+
const plan = stderr.match(/DIFFING_PLAN_DECISION=(\S+)/);
|
|
214
|
+
if (plan) return { kind: "plan", decision: plan[1] };
|
|
215
|
+
const mockup = stderr.match(/DIFFING_MOCKUP_DECISION=(\S+)/);
|
|
216
|
+
if (mockup) return { kind: "mockup", decision: mockup[1] };
|
|
217
|
+
const review = stderr.match(/DIFFING_REVIEW_ROUND=(\S+)/);
|
|
218
|
+
if (review) return { kind: "review", decision: `released round=${review[1]}` };
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function emitVerdictNotice(ctx: ExtensionContext, stderr: string): void {
|
|
223
|
+
const notice = verdictNotice(stderr);
|
|
224
|
+
if (!notice) return;
|
|
225
|
+
const line = `DIFFING_VERDICT ${notice.kind} decision=${notice.decision}`;
|
|
226
|
+
try {
|
|
227
|
+
if (ctx.hasUI) {
|
|
228
|
+
ctx.ui.notify(line, "info");
|
|
229
|
+
ctx.ui.setWidget("diffing-verdict", [line]);
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
// UI hooks are best-effort; never break the tool result.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
211
236
|
// ────────────────────────────────────────────────────────────────────────────
|
|
212
237
|
// Canonical skill root + self-heal
|
|
213
238
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -248,35 +273,6 @@ function findCanonicalRoot(cwd: string): string | null {
|
|
|
248
273
|
return null;
|
|
249
274
|
}
|
|
250
275
|
|
|
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
276
|
|
|
281
277
|
// ────────────────────────────────────────────────────────────────────────────
|
|
282
278
|
// Detached review server start
|
|
@@ -340,7 +336,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
340
336
|
// Keep the home skill links canonical for every session (idempotent).
|
|
341
337
|
pi.on("session_start", async (_event, ctx) => {
|
|
342
338
|
const canonical = findCanonicalRoot(ctx.cwd);
|
|
343
|
-
if (canonical)
|
|
339
|
+
if (canonical) {
|
|
340
|
+
selfHealSkillLinks(canonical, join(homedir(), ".agents", "skills"));
|
|
341
|
+
}
|
|
344
342
|
await refreshStatus(ctx);
|
|
345
343
|
});
|
|
346
344
|
|
|
@@ -601,6 +599,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
601
599
|
{ parked: true, exitCode: result.exitCode },
|
|
602
600
|
);
|
|
603
601
|
}
|
|
602
|
+
emitVerdictNotice(ctx, result.stderr);
|
|
604
603
|
return textResult(describe(result, "diffing await-review"), {
|
|
605
604
|
parked: false,
|
|
606
605
|
exitCode: result.exitCode,
|
|
@@ -708,6 +707,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
708
707
|
{ parked: true, exitCode: result.exitCode },
|
|
709
708
|
);
|
|
710
709
|
}
|
|
710
|
+
emitVerdictNotice(ctx, result.stderr);
|
|
711
711
|
return textResult(describe(result, "diffing plan await"), {
|
|
712
712
|
parked: false,
|
|
713
713
|
exitCode: result.exitCode,
|
|
@@ -825,7 +825,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
825
825
|
name: "diffing_mockup_submit",
|
|
826
826
|
label: "Diffing Mockup Submit",
|
|
827
827
|
description:
|
|
828
|
-
"Submit HTML mockup screen(s) for visual review.
|
|
828
|
+
"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
829
|
parameters: Type.Object({
|
|
830
830
|
html: Type.Optional(
|
|
831
831
|
Type.String({ description: "Single-screen HTML body." }),
|
|
@@ -854,6 +854,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
854
854
|
),
|
|
855
855
|
model: Type.Optional(Type.String()),
|
|
856
856
|
source: Type.Optional(Type.String()),
|
|
857
|
+
mode: Type.Optional(
|
|
858
|
+
StringEnum(["fragment", "document"] as const, {
|
|
859
|
+
description:
|
|
860
|
+
"fragment = body contents wrapped by the published design-system shell. document = full HTML, no wrap. Default: server default.",
|
|
861
|
+
}),
|
|
862
|
+
),
|
|
863
|
+
designSystem: Type.Optional(
|
|
864
|
+
Type.String({
|
|
865
|
+
description:
|
|
866
|
+
"Design-system id to bind (default system if omitted).",
|
|
867
|
+
}),
|
|
868
|
+
),
|
|
869
|
+
planId: Type.Optional(
|
|
870
|
+
Type.String({
|
|
871
|
+
description: "Optional plan id this mockup illustrates.",
|
|
872
|
+
}),
|
|
873
|
+
),
|
|
857
874
|
}),
|
|
858
875
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
859
876
|
const args = ["mockup", "submit"];
|
|
@@ -896,6 +913,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
896
913
|
if (params.mockupId) args.push("--id", params.mockupId);
|
|
897
914
|
if (params.model) args.push("--model", params.model);
|
|
898
915
|
if (params.source) args.push("--source", params.source);
|
|
916
|
+
if (params.mode) args.push("--mode", params.mode);
|
|
917
|
+
if (params.designSystem) args.push("--system", params.designSystem);
|
|
918
|
+
if (params.planId) args.push("--plan-id", params.planId);
|
|
899
919
|
try {
|
|
900
920
|
const result = await runDiffing(args, ctx.cwd, { stdin });
|
|
901
921
|
const stdout = result.stdout.trim();
|
|
@@ -941,6 +961,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
941
961
|
{ parked: true, exitCode: result.exitCode },
|
|
942
962
|
);
|
|
943
963
|
}
|
|
964
|
+
emitVerdictNotice(ctx, result.stderr);
|
|
944
965
|
return textResult(describe(result, "diffing mockup await"), {
|
|
945
966
|
parked: false,
|
|
946
967
|
exitCode: result.exitCode,
|
|
@@ -1079,12 +1100,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1079
1100
|
name: "diffing_mockup_inspect",
|
|
1080
1101
|
label: "Diffing Mockup Inspect",
|
|
1081
1102
|
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.",
|
|
1103
|
+
"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
1104
|
parameters: Type.Object({
|
|
1084
|
-
view: StringEnum(
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1105
|
+
view: StringEnum(
|
|
1106
|
+
["summary", "comments", "comment", "screen", "preview"] as const,
|
|
1107
|
+
{
|
|
1108
|
+
description: "What to read. Default: summary.",
|
|
1109
|
+
default: "summary",
|
|
1110
|
+
},
|
|
1111
|
+
),
|
|
1088
1112
|
mockupId: Type.Optional(
|
|
1089
1113
|
Type.String({ description: "Mockup id. Default: latest." }),
|
|
1090
1114
|
),
|
|
@@ -1152,11 +1176,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1152
1176
|
name: "diffing_mockup_screen",
|
|
1153
1177
|
label: "Diffing Mockup Screen",
|
|
1154
1178
|
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.",
|
|
1179
|
+
"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
1180
|
parameters: Type.Object({
|
|
1157
|
-
action: StringEnum(
|
|
1158
|
-
|
|
1159
|
-
|
|
1181
|
+
action: StringEnum(
|
|
1182
|
+
["upsert", "remove", "patch", "replace-region"] as const,
|
|
1183
|
+
{ description: "Revision op." },
|
|
1184
|
+
),
|
|
1160
1185
|
mockupId: Type.String({ description: "Mockup id." }),
|
|
1161
1186
|
screenId: Type.String({ description: "Screen id." }),
|
|
1162
1187
|
html: Type.Optional(
|
|
@@ -1177,8 +1202,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1177
1202
|
description: "For patch: exact text to replace (first occurrence).",
|
|
1178
1203
|
}),
|
|
1179
1204
|
),
|
|
1205
|
+
region: Type.Optional(
|
|
1206
|
+
Type.String({
|
|
1207
|
+
description:
|
|
1208
|
+
"For replace-region: data-diffing attribute value of the block to replace.",
|
|
1209
|
+
}),
|
|
1210
|
+
),
|
|
1180
1211
|
replacement: Type.Optional(
|
|
1181
|
-
Type.String({
|
|
1212
|
+
Type.String({
|
|
1213
|
+
description:
|
|
1214
|
+
"For patch: replacement text. For replace-region: new inner HTML.",
|
|
1215
|
+
}),
|
|
1182
1216
|
),
|
|
1183
1217
|
expectedVersion: Type.Optional(
|
|
1184
1218
|
Type.Number({
|
|
@@ -1202,6 +1236,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1202
1236
|
stdin = params.html;
|
|
1203
1237
|
}
|
|
1204
1238
|
if (params.label) args.push("--label", params.label);
|
|
1239
|
+
} else if (params.action === "replace-region") {
|
|
1240
|
+
if (params.region) args.push("--region", params.region);
|
|
1241
|
+
if (params.replacement !== undefined)
|
|
1242
|
+
args.push("--replacement", params.replacement);
|
|
1205
1243
|
} else if (params.action === "patch") {
|
|
1206
1244
|
if (params.text !== undefined) args.push("--text", params.text);
|
|
1207
1245
|
if (params.replacement !== undefined)
|
|
@@ -1264,6 +1302,77 @@ export default function (pi: ExtensionAPI) {
|
|
|
1264
1302
|
},
|
|
1265
1303
|
});
|
|
1266
1304
|
|
|
1305
|
+
pi.registerTool({
|
|
1306
|
+
name: "diffing_mockup_handoff",
|
|
1307
|
+
label: "Diffing Mockup Handoff",
|
|
1308
|
+
description:
|
|
1309
|
+
"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.",
|
|
1310
|
+
parameters: Type.Object({
|
|
1311
|
+
mockupId: Type.Optional(
|
|
1312
|
+
Type.String({ description: "Mockup id. Default: latest." }),
|
|
1313
|
+
),
|
|
1314
|
+
json: Type.Optional(
|
|
1315
|
+
Type.Boolean({ description: "Emit raw JSON. Default: XML handoff." }),
|
|
1316
|
+
),
|
|
1317
|
+
}),
|
|
1318
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1319
|
+
const args = ["mockup", "handoff"];
|
|
1320
|
+
if (params.mockupId) args.push(params.mockupId);
|
|
1321
|
+
if (params.json) args.push("--json");
|
|
1322
|
+
const result = await runDiffing(args, ctx.cwd);
|
|
1323
|
+
return textResult(describe(result, "diffing mockup handoff"), {
|
|
1324
|
+
exitCode: result.exitCode,
|
|
1325
|
+
stderr: result.stderr.trim() || undefined,
|
|
1326
|
+
});
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
pi.registerTool({
|
|
1331
|
+
name: "diffing_design",
|
|
1332
|
+
label: "Diffing Design System",
|
|
1333
|
+
description:
|
|
1334
|
+
"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.",
|
|
1335
|
+
parameters: Type.Object({
|
|
1336
|
+
action: StringEnum(
|
|
1337
|
+
["show", "list", "extract", "propose", "publish"] as const,
|
|
1338
|
+
{ description: "Design-system op. Default: show." },
|
|
1339
|
+
),
|
|
1340
|
+
id: Type.Optional(
|
|
1341
|
+
Type.String({ description: "System id. Default: default." }),
|
|
1342
|
+
),
|
|
1343
|
+
json: Type.Optional(
|
|
1344
|
+
Type.Boolean({ description: "Emit raw JSON. Default: false." }),
|
|
1345
|
+
),
|
|
1346
|
+
from: Type.Optional(
|
|
1347
|
+
StringEnum(["css", "text"] as const, {
|
|
1348
|
+
description: "For extract: scan source. Default: css.",
|
|
1349
|
+
}),
|
|
1350
|
+
),
|
|
1351
|
+
title: Type.Optional(
|
|
1352
|
+
Type.String({ description: "For extract/propose: system title." }),
|
|
1353
|
+
),
|
|
1354
|
+
guidelines: Type.Optional(
|
|
1355
|
+
Type.String({
|
|
1356
|
+
description: "For propose: markdown guidelines to write on the draft.",
|
|
1357
|
+
}),
|
|
1358
|
+
),
|
|
1359
|
+
}),
|
|
1360
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1361
|
+
const action = params.action ?? "show";
|
|
1362
|
+
const args = ["design", action];
|
|
1363
|
+
if (params.id) args.push(params.id);
|
|
1364
|
+
if (params.json) args.push("--json");
|
|
1365
|
+
if (params.from) args.push("--from", params.from);
|
|
1366
|
+
if (params.title) args.push("--title", params.title);
|
|
1367
|
+
if (params.guidelines) args.push("--guidelines", params.guidelines);
|
|
1368
|
+
const result = await runDiffing(args, ctx.cwd);
|
|
1369
|
+
return textResult(describe(result, `diffing design ${action}`), {
|
|
1370
|
+
exitCode: result.exitCode,
|
|
1371
|
+
stderr: result.stderr.trim() || undefined,
|
|
1372
|
+
});
|
|
1373
|
+
},
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1267
1376
|
pi.registerTool({
|
|
1268
1377
|
name: "diffing_url",
|
|
1269
1378
|
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
|
}
|