@aayambansal/squint 0.3.0 → 0.3.1
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/{cdp-4IZHB3W5.js → cdp-2XAU5MEA.js} +1 -1
- package/dist/{chunk-QEENJGCL.js → chunk-DAETGO2M.js} +37 -1
- package/dist/{chunk-CFWKEIJJ.js → chunk-IH4L2KR6.js} +1 -0
- package/dist/{chunk-RQFFAJFE.js → chunk-OTCH66M7.js} +4 -4
- package/dist/cli.js +82 -7
- package/dist/{commands-QKDWUD4K.js → commands-E7JIVJDP.js} +1 -1
- package/dist/{preview-56UCV5A3.js → preview-PS7WJ2KO.js} +2 -2
- package/dist/tokens-XYGRRAXC.js +104 -0
- package/package.json +1 -1
|
@@ -5,6 +5,32 @@ import { spawn } from "child_process";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import os from "os";
|
|
7
7
|
import path from "path";
|
|
8
|
+
var PERF_PROBE = `(() => {
|
|
9
|
+
const out = {};
|
|
10
|
+
const buffered = (type) => {
|
|
11
|
+
try {
|
|
12
|
+
const po = new PerformanceObserver(() => {});
|
|
13
|
+
po.observe({ type, buffered: true });
|
|
14
|
+
const records = po.takeRecords();
|
|
15
|
+
po.disconnect();
|
|
16
|
+
return records;
|
|
17
|
+
} catch { return []; }
|
|
18
|
+
};
|
|
19
|
+
const lcp = buffered('largest-contentful-paint');
|
|
20
|
+
if (lcp.length > 0) out.lcpMs = Math.round(lcp[lcp.length - 1].startTime);
|
|
21
|
+
let cls = 0;
|
|
22
|
+
for (const e of buffered('layout-shift')) { if (!e.hadRecentInput) cls += e.value; }
|
|
23
|
+
out.cls = Math.round(cls * 1000) / 1000;
|
|
24
|
+
try {
|
|
25
|
+
const resources = performance.getEntriesByType('resource');
|
|
26
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
27
|
+
let bytes = nav ? (nav.transferSize || 0) : 0;
|
|
28
|
+
for (const r of resources) bytes += r.transferSize || 0;
|
|
29
|
+
out.transferBytes = bytes;
|
|
30
|
+
out.requests = resources.length + (nav ? 1 : 0);
|
|
31
|
+
} catch {}
|
|
32
|
+
return out;
|
|
33
|
+
})()`;
|
|
8
34
|
var A11Y_AUDIT = `(() => {
|
|
9
35
|
const out = [];
|
|
10
36
|
const name = (el) => (el.getAttribute('aria-label') || el.textContent || el.getAttribute('title') || '').trim();
|
|
@@ -251,6 +277,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
251
277
|
const shots = [];
|
|
252
278
|
let a11y = [];
|
|
253
279
|
let slop = [];
|
|
280
|
+
let perf = {};
|
|
254
281
|
const requests = /* @__PURE__ */ new Map();
|
|
255
282
|
let connection = null;
|
|
256
283
|
try {
|
|
@@ -295,6 +322,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
295
322
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
296
323
|
}
|
|
297
324
|
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
325
|
+
try {
|
|
326
|
+
const { result } = await connection.send(
|
|
327
|
+
"Runtime.evaluate",
|
|
328
|
+
{ expression: PERF_PROBE, returnByValue: true },
|
|
329
|
+
sessionId
|
|
330
|
+
);
|
|
331
|
+
if (result?.value && typeof result.value === "object") perf = result.value;
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
298
334
|
if (audit) {
|
|
299
335
|
try {
|
|
300
336
|
const { result } = await connection.send(
|
|
@@ -337,7 +373,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
337
373
|
child.kill("SIGKILL");
|
|
338
374
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
339
375
|
}
|
|
340
|
-
return { report, shots, a11y, slop };
|
|
376
|
+
return { report, shots, a11y, slop, perf };
|
|
341
377
|
}
|
|
342
378
|
|
|
343
379
|
export {
|
|
@@ -19,6 +19,7 @@ var COMMANDS = [
|
|
|
19
19
|
{ name: "checkpoints", group: "explore", description: "list per-ask checkpoints" },
|
|
20
20
|
{ name: "restore", args: "<n>", group: "explore", description: "rewind files to before ask n" },
|
|
21
21
|
{ name: "theme", args: "[name]", group: "session", description: "switch the TUI theme", viewLevel: true },
|
|
22
|
+
{ name: "btw", args: "<question>", group: "session", description: "read-only side question; the main thread is untouched" },
|
|
22
23
|
{ name: "copy", group: "session", description: "copy the last reply to the clipboard" },
|
|
23
24
|
{ name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
|
|
24
25
|
{ name: "resume", group: "session", description: "pick up the previous session for this repo" },
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
cdpCapture,
|
|
8
8
|
hasWebSocket
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-DAETGO2M.js";
|
|
10
10
|
import {
|
|
11
11
|
ensureSquintIgnore
|
|
12
12
|
} from "./chunk-O2S6PAJE.js";
|
|
@@ -118,14 +118,14 @@ async function probeRuntime(url, cwd) {
|
|
|
118
118
|
if (!chrome || !hasWebSocket()) return null;
|
|
119
119
|
try {
|
|
120
120
|
const dir = cwd ? previewDir(cwd) : os.tmpdir();
|
|
121
|
-
const { report, shots } = await cdpCapture(
|
|
121
|
+
const { report, shots, perf } = await cdpCapture(
|
|
122
122
|
chrome,
|
|
123
123
|
url,
|
|
124
124
|
dir,
|
|
125
125
|
cwd ? [{ name: "pulse", width: 1280, height: 800 }] : [],
|
|
126
126
|
1500
|
|
127
127
|
);
|
|
128
|
-
return { report, pulsePath: shots[0]?.path };
|
|
128
|
+
return { report, pulsePath: shots[0]?.path, perf };
|
|
129
129
|
} catch {
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
@@ -133,7 +133,7 @@ async function probeRuntime(url, cwd) {
|
|
|
133
133
|
async function comparePulse(previous, current) {
|
|
134
134
|
const chrome = findChrome();
|
|
135
135
|
if (!chrome || !hasWebSocket()) return null;
|
|
136
|
-
const { pixelDiffPct } = await import("./cdp-
|
|
136
|
+
const { pixelDiffPct } = await import("./cdp-2XAU5MEA.js");
|
|
137
137
|
return pixelDiffPct(chrome, previous, current);
|
|
138
138
|
}
|
|
139
139
|
function buildRuntimeFixPrompt(report) {
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
completeCommand
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IH4L2KR6.js";
|
|
5
5
|
import {
|
|
6
6
|
applySandbox,
|
|
7
7
|
discardSandbox,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
comparePulse,
|
|
43
43
|
probeRuntime,
|
|
44
44
|
runtimeSummary
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-OTCH66M7.js";
|
|
46
46
|
import {
|
|
47
47
|
runAgent
|
|
48
48
|
} from "./chunk-VH7OOFQP.js";
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
detectEngines,
|
|
54
54
|
getEngine
|
|
55
55
|
} from "./chunk-KVYGPLWW.js";
|
|
56
|
-
import "./chunk-
|
|
56
|
+
import "./chunk-DAETGO2M.js";
|
|
57
57
|
import {
|
|
58
58
|
enrich
|
|
59
59
|
} from "./chunk-4LAU5TGK.js";
|
|
@@ -236,7 +236,7 @@ function registerEnv(program2) {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
|
|
239
|
-
const { hasWebSocket } = await import("./cdp-
|
|
239
|
+
const { hasWebSocket } = await import("./cdp-2XAU5MEA.js");
|
|
240
240
|
const chrome = findChrome2();
|
|
241
241
|
console.log(
|
|
242
242
|
chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -459,7 +459,7 @@ function registerQuality(program2) {
|
|
|
459
459
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
460
460
|
});
|
|
461
461
|
program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
|
|
462
|
-
const { captureViewports: captureViewports2 } = await import("./preview-
|
|
462
|
+
const { captureViewports: captureViewports2 } = await import("./preview-PS7WJ2KO.js");
|
|
463
463
|
const result = await captureViewports2(process.cwd(), url);
|
|
464
464
|
if (!result) {
|
|
465
465
|
console.error(pc4.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -672,6 +672,7 @@ var Session = class {
|
|
|
672
672
|
fixAttempts = 0;
|
|
673
673
|
reviewTipShown = false;
|
|
674
674
|
lastPulse = null;
|
|
675
|
+
lastPerf = null;
|
|
675
676
|
autoReviewedThisAsk = false;
|
|
676
677
|
startedAt = Date.now();
|
|
677
678
|
subscribe(listener) {
|
|
@@ -733,6 +734,41 @@ var Session = class {
|
|
|
733
734
|
this.dispatchFix([...this.state.problems]);
|
|
734
735
|
return true;
|
|
735
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* A side question: read-only, no session resume, so the main thread's
|
|
739
|
+
* context is untouched (Cursor's /btw). Costs count; loops don't run.
|
|
740
|
+
*/
|
|
741
|
+
async btw(question) {
|
|
742
|
+
this.push("user", `\u{1F4AC} btw: ${question}`);
|
|
743
|
+
this.notify({ running: true, runStartedAt: Date.now() });
|
|
744
|
+
const engine = getEngine(this.state.engineId);
|
|
745
|
+
this.abort = new AbortController();
|
|
746
|
+
const result = await runAgent(
|
|
747
|
+
engine,
|
|
748
|
+
{
|
|
749
|
+
prompt: `Answer this question about the repository. Investigate as needed but make no changes:
|
|
750
|
+
|
|
751
|
+
${question}`,
|
|
752
|
+
cwd: this.execCwd(),
|
|
753
|
+
model: this.state.model,
|
|
754
|
+
mode: "plan"
|
|
755
|
+
},
|
|
756
|
+
this.handleEvent,
|
|
757
|
+
this.abort.signal
|
|
758
|
+
);
|
|
759
|
+
this.abort = null;
|
|
760
|
+
this.commitLive();
|
|
761
|
+
this.flushToolCollapse();
|
|
762
|
+
if (result.ok) {
|
|
763
|
+
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
764
|
+
this.push("status", `btw answered${cost}`);
|
|
765
|
+
this.notify({
|
|
766
|
+
totals: { costUsd: this.state.totals.costUsd + (result.costUsd ?? 0), turns: this.state.totals.turns }
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
this.notify({ running: false });
|
|
770
|
+
this.drainQueue();
|
|
771
|
+
}
|
|
736
772
|
setMode(mode) {
|
|
737
773
|
this.notify({ mode });
|
|
738
774
|
const hint = mode === "plan" ? "read-only: the engine investigates and proposes, edits nothing" : mode === "yolo" ? "no approval friction \u2014 the engine can do anything" : "edits auto-approved inside the workspace";
|
|
@@ -943,6 +979,18 @@ var Session = class {
|
|
|
943
979
|
const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
|
|
944
980
|
const work = stat ? ` \xB7 ${stat}` : this.turnEdits > 0 ? ` \xB7 ${this.turnEdits} edit${this.turnEdits === 1 ? "" : "s"}` : this.turnTools > 0 ? ` \xB7 ${this.turnTools} tool call${this.turnTools === 1 ? "" : "s"}` : "";
|
|
945
981
|
this.push("status", `done${secs}${cost}${work}`);
|
|
982
|
+
if (checkpoint) {
|
|
983
|
+
try {
|
|
984
|
+
const { driftSummary, loadTokenIndex, scanDrift } = await import("./tokens-XYGRRAXC.js");
|
|
985
|
+
const index = loadTokenIndex(this.execCwd());
|
|
986
|
+
const drift = scanDrift(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD", index);
|
|
987
|
+
if (drift.length > 0) {
|
|
988
|
+
this.push("status", `token drift: ${drift.length} hardcoded color(s)
|
|
989
|
+
${driftSummary(drift)}`);
|
|
990
|
+
}
|
|
991
|
+
} catch {
|
|
992
|
+
}
|
|
993
|
+
}
|
|
946
994
|
const before = this.state.totals.costUsd;
|
|
947
995
|
this.notify({
|
|
948
996
|
totals: {
|
|
@@ -1018,6 +1066,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1018
1066
|
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
1019
1067
|
} else if (probe) {
|
|
1020
1068
|
this.clearProblems("runtime");
|
|
1069
|
+
this.perfPulse(probe.perf);
|
|
1021
1070
|
const pct = await this.visualPulse(probe.pulsePath);
|
|
1022
1071
|
if (this.opts.autoReview && pct !== null && pct >= 10 && !this.autoReviewedThisAsk) {
|
|
1023
1072
|
this.autoReviewedThisAsk = true;
|
|
@@ -1083,6 +1132,25 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1083
1132
|
this.push("error", `restore failed: ${result.detail ?? "unknown error"}`);
|
|
1084
1133
|
}
|
|
1085
1134
|
}
|
|
1135
|
+
/** Cross-turn load-performance deltas: the perf twin of the visual pulse. */
|
|
1136
|
+
perfPulse(perf) {
|
|
1137
|
+
if (!perf || perf.lcpMs === void 0 && perf.transferBytes === void 0) return;
|
|
1138
|
+
const previous = this.lastPerf;
|
|
1139
|
+
this.lastPerf = perf;
|
|
1140
|
+
const mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
1141
|
+
const parts = [];
|
|
1142
|
+
if (perf.lcpMs !== void 0) {
|
|
1143
|
+
const delta = previous?.lcpMs !== void 0 && Math.abs(perf.lcpMs - previous.lcpMs) >= 100 ? ` (${perf.lcpMs > previous.lcpMs ? "+" : "\u2212"}${Math.abs(perf.lcpMs - previous.lcpMs)}ms)` : "";
|
|
1144
|
+
parts.push(`LCP ${perf.lcpMs}ms${delta}`);
|
|
1145
|
+
}
|
|
1146
|
+
if (perf.cls !== void 0 && perf.cls > 0.05) parts.push(`CLS ${perf.cls}`);
|
|
1147
|
+
if (perf.transferBytes !== void 0) {
|
|
1148
|
+
const delta = previous?.transferBytes !== void 0 && Math.abs(perf.transferBytes - previous.transferBytes) > 100 * 1024 ? ` (${perf.transferBytes > previous.transferBytes ? "+" : "\u2212"}${mb(Math.abs(perf.transferBytes - previous.transferBytes))})` : "";
|
|
1149
|
+
parts.push(`${mb(perf.transferBytes)}${delta}`);
|
|
1150
|
+
}
|
|
1151
|
+
if (perf.requests !== void 0) parts.push(`${perf.requests} req`);
|
|
1152
|
+
if (parts.length > 0) this.push("status", `perf: ${parts.join(" \xB7 ")}`);
|
|
1153
|
+
}
|
|
1086
1154
|
/**
|
|
1087
1155
|
* Cross-turn visual drift check: compare this turn's pulse screenshot
|
|
1088
1156
|
* with the previous one and report how much of the page changed.
|
|
@@ -1295,6 +1363,13 @@ They are objective defects, not style preferences.`
|
|
|
1295
1363
|
})();
|
|
1296
1364
|
break;
|
|
1297
1365
|
}
|
|
1366
|
+
case "btw":
|
|
1367
|
+
if (!arg) {
|
|
1368
|
+
this.push("status", "usage: /btw <question> \u2014 read-only side question, main thread untouched");
|
|
1369
|
+
} else {
|
|
1370
|
+
void this.btw(arg);
|
|
1371
|
+
}
|
|
1372
|
+
break;
|
|
1298
1373
|
case "copy": {
|
|
1299
1374
|
const last = this.state.items.findLast((i) => i.role === "assistant");
|
|
1300
1375
|
if (!last) {
|
|
@@ -1539,7 +1614,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1539
1614
|
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
1540
1615
|
break;
|
|
1541
1616
|
case "help": {
|
|
1542
|
-
void import("./commands-
|
|
1617
|
+
void import("./commands-E7JIVJDP.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
1543
1618
|
break;
|
|
1544
1619
|
}
|
|
1545
1620
|
case "quit":
|
|
@@ -2155,7 +2230,7 @@ function registerTui(program2) {
|
|
|
2155
2230
|
}
|
|
2156
2231
|
|
|
2157
2232
|
// src/cli.tsx
|
|
2158
|
-
var VERSION = true ? "0.3.
|
|
2233
|
+
var VERSION = true ? "0.3.1" : "0.0.0-dev";
|
|
2159
2234
|
var program = new Command();
|
|
2160
2235
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2161
2236
|
registerRun(program);
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
probeRuntime,
|
|
11
11
|
routeShotName,
|
|
12
12
|
runtimeSummary
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OTCH66M7.js";
|
|
14
14
|
import "./chunk-IMDRXXFU.js";
|
|
15
15
|
import "./chunk-KVYGPLWW.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-DAETGO2M.js";
|
|
17
17
|
import "./chunk-O2S6PAJE.js";
|
|
18
18
|
export {
|
|
19
19
|
VIEWPORTS,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/quality/tokens.ts
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
var HEX_RE = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
|
|
8
|
+
var VAR_DEF_RE = /(--[a-zA-Z0-9-_]+)\s*:\s*([^;]+);/g;
|
|
9
|
+
function parseHex(hex) {
|
|
10
|
+
const raw = hex.replace("#", "");
|
|
11
|
+
const full = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
|
|
12
|
+
if (full.length !== 6) return null;
|
|
13
|
+
const n = Number.parseInt(full, 16);
|
|
14
|
+
if (Number.isNaN(n)) return null;
|
|
15
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
16
|
+
}
|
|
17
|
+
function loadTokenIndex(cwd) {
|
|
18
|
+
const colors = /* @__PURE__ */ new Map();
|
|
19
|
+
const cssFiles = [];
|
|
20
|
+
const walk = (dir, depth) => {
|
|
21
|
+
if (depth > 4 || cssFiles.length > 30) return;
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
25
|
+
} catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
30
|
+
const full = path.join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
32
|
+
else if (entry.name.endsWith(".css")) cssFiles.push(full);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
walk(path.join(cwd, "src"), 0);
|
|
36
|
+
walk(cwd, 1);
|
|
37
|
+
for (const file of cssFiles.slice(0, 30)) {
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = fs.readFileSync(file, "utf8");
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const match of text.matchAll(VAR_DEF_RE)) {
|
|
45
|
+
const value = match[2].trim();
|
|
46
|
+
const hex = value.match(/#[0-9a-fA-F]{3,6}\b/)?.[0];
|
|
47
|
+
const rgbFn = /rgb\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(value);
|
|
48
|
+
let rgb = null;
|
|
49
|
+
if (hex) rgb = parseHex(hex);
|
|
50
|
+
else if (rgbFn) rgb = [Number(rgbFn[1]), Number(rgbFn[2]), Number(rgbFn[3])];
|
|
51
|
+
if (rgb) colors.set(match[1], { value, rgb });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { colors };
|
|
55
|
+
}
|
|
56
|
+
var distance = (a, b) => Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2);
|
|
57
|
+
function scanDrift(cwd, source, index) {
|
|
58
|
+
if (index.colors.size === 0) return [];
|
|
59
|
+
let diff;
|
|
60
|
+
try {
|
|
61
|
+
diff = execFileSync("git", ["diff", "-U0", source, "--", "*.css", "*.tsx", "*.jsx", "*.ts", "*.html"], {
|
|
62
|
+
cwd,
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const findings = [];
|
|
70
|
+
let currentFile = "";
|
|
71
|
+
for (const line of diff.split("\n")) {
|
|
72
|
+
if (line.startsWith("+++ b/")) {
|
|
73
|
+
currentFile = line.slice(6);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!line.startsWith("+") || line.startsWith("+++")) continue;
|
|
77
|
+
if (/--[a-zA-Z0-9-_]+\s*:/.test(line)) continue;
|
|
78
|
+
for (const match of line.matchAll(HEX_RE)) {
|
|
79
|
+
const rgb = parseHex(match[0]);
|
|
80
|
+
if (!rgb) continue;
|
|
81
|
+
let best = null;
|
|
82
|
+
for (const [token, entry] of index.colors) {
|
|
83
|
+
const d = distance(rgb, entry.rgb);
|
|
84
|
+
if (!best || d < best.d) best = { token, d };
|
|
85
|
+
}
|
|
86
|
+
if (best) {
|
|
87
|
+
findings.push({ file: currentFile, literal: match[0], token: best.token, distance: Math.round(best.d) });
|
|
88
|
+
}
|
|
89
|
+
if (findings.length >= 10) return findings;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
|
94
|
+
function driftSummary(findings) {
|
|
95
|
+
return findings.map(
|
|
96
|
+
(f) => `${f.file}: ${f.literal} \u2192 use var(${f.token})${f.distance === 0 ? " (exact match)" : ` (closest, \u0394${f.distance})`}`
|
|
97
|
+
).join("\n");
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
driftSummary,
|
|
101
|
+
loadTokenIndex,
|
|
102
|
+
parseHex,
|
|
103
|
+
scanDrift
|
|
104
|
+
};
|
package/package.json
CHANGED