@aayambansal/squint 0.4.3 → 0.4.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/README.md +5 -1
- package/dist/{cdp-TPP2RGSR.js → cdp-C6MOKVEN.js} +1 -1
- package/dist/{chunk-Y47X4ENN.js → chunk-JFFNQIBU.js} +51 -1
- package/dist/{chunk-WCLQZFOR.js → chunk-ZUEGDTAM.js} +16 -6
- package/dist/cli.js +40 -10
- package/dist/nextMcp-42Y63M7W.js +97 -0
- package/dist/{preview-SKHIXVCE.js → preview-Q43LOK6P.js} +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,7 +169,11 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
169
169
|
design token, and the mechanical anti-slop sweep flags generic-AI tells as
|
|
170
170
|
distinctiveness debt in `/review`. The phantom-class check diffs every DOM class
|
|
171
171
|
against the compiled CSS — hallucinated utilities surface as named problems instead
|
|
172
|
-
of silently unstyled elements
|
|
172
|
+
of silently unstyled elements — and version-aware rule-packs catch Tailwind v3
|
|
173
|
+
muscle memory in v4 projects at gate time, rename in hand. view-transition breakage (duplicate names, missing reduced-motion handling) is
|
|
174
|
+
flagged from the live page, and on Next 16+ the framework's own `/_next/mcp`
|
|
175
|
+
channel feeds structured errors straight into the fix loop. `/context` itemizes
|
|
176
|
+
the injected-context bill per source, with staleness warnings.
|
|
173
177
|
- **The design ledger**: `/decide` (plus chosen variants, rollbacks, accepted
|
|
174
178
|
sandboxes) appends to a committed `.squint/design-log.jsonl`; recent decisions ride
|
|
175
179
|
into every ask so they stop getting silently undone between sessions.
|
|
@@ -360,6 +360,46 @@ var PHANTOM_AUDIT = `(() => {
|
|
|
360
360
|
}
|
|
361
361
|
return out;
|
|
362
362
|
})()`;
|
|
363
|
+
var VT_AUDIT = `(() => {
|
|
364
|
+
const findings = [];
|
|
365
|
+
const names = new Map();
|
|
366
|
+
for (const el of document.querySelectorAll('*')) {
|
|
367
|
+
const name = getComputedStyle(el).viewTransitionName;
|
|
368
|
+
if (name && name !== 'none' && name !== 'auto') {
|
|
369
|
+
const label = el.tagName.toLowerCase() + (el.id ? '#' + el.id : '');
|
|
370
|
+
const list = names.get(name) || [];
|
|
371
|
+
list.push(label);
|
|
372
|
+
names.set(name, list);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const [name, els] of names) {
|
|
376
|
+
if (els.length > 1) {
|
|
377
|
+
findings.push('duplicate view-transition-name "' + name + '" on ' + els.length + ' elements (' + els.slice(0, 3).join(', ') + ') \u2014 the browser skips the entire transition');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let vtCss = false;
|
|
381
|
+
let reducedGuard = false;
|
|
382
|
+
const walk = (list, inReduced) => {
|
|
383
|
+
for (const rule of list) {
|
|
384
|
+
const media = rule.media && rule.media.mediaText || '';
|
|
385
|
+
const nowReduced = inReduced || /prefers-reduced-motion/.test(media);
|
|
386
|
+
if (rule.cssText && rule.cssText.includes('::view-transition')) {
|
|
387
|
+
vtCss = true;
|
|
388
|
+
if (nowReduced) reducedGuard = true;
|
|
389
|
+
}
|
|
390
|
+
if (nowReduced && rule.cssText && /animation|transition/.test(rule.cssText)) reducedGuard = true;
|
|
391
|
+
if (rule.cssRules) walk(rule.cssRules, nowReduced);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
for (const sheet of document.styleSheets) {
|
|
395
|
+
let rules; try { rules = sheet.cssRules; } catch { continue; }
|
|
396
|
+
try { walk(rules, false); } catch {}
|
|
397
|
+
}
|
|
398
|
+
if ((names.size > 0 || vtCss) && !reducedGuard) {
|
|
399
|
+
findings.push('view transitions declared with no prefers-reduced-motion handling anywhere in the CSS \u2014 motion-sensitive users get the full animation');
|
|
400
|
+
}
|
|
401
|
+
return findings;
|
|
402
|
+
})()`;
|
|
363
403
|
async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
|
|
364
404
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
365
405
|
const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
|
|
@@ -369,6 +409,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
369
409
|
let perf = {};
|
|
370
410
|
let narration = [];
|
|
371
411
|
let phantoms = [];
|
|
412
|
+
let viewTransitions = [];
|
|
372
413
|
const requests = /* @__PURE__ */ new Map();
|
|
373
414
|
let connection = null;
|
|
374
415
|
try {
|
|
@@ -450,6 +491,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
450
491
|
if (Array.isArray(result?.value)) phantoms = result.value.map(String);
|
|
451
492
|
} catch {
|
|
452
493
|
}
|
|
494
|
+
try {
|
|
495
|
+
const { result } = await connection.send(
|
|
496
|
+
"Runtime.evaluate",
|
|
497
|
+
{ expression: VT_AUDIT, returnByValue: true },
|
|
498
|
+
sessionId
|
|
499
|
+
);
|
|
500
|
+
if (Array.isArray(result?.value)) viewTransitions = result.value.map(String);
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
453
503
|
try {
|
|
454
504
|
await connection.send("Accessibility.enable", {}, sessionId);
|
|
455
505
|
const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
|
|
@@ -514,7 +564,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
514
564
|
child.kill("SIGKILL");
|
|
515
565
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
516
566
|
}
|
|
517
|
-
return { report, shots, a11y, slop, perf, narration, phantoms };
|
|
567
|
+
return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions };
|
|
518
568
|
}
|
|
519
569
|
|
|
520
570
|
export {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
cdpCapture,
|
|
11
11
|
hasWebSocket
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JFFNQIBU.js";
|
|
13
13
|
|
|
14
14
|
// src/preview/preview.ts
|
|
15
15
|
import fs from "fs";
|
|
@@ -47,7 +47,7 @@ async function captureViewports(cwd, url) {
|
|
|
47
47
|
const base = url.replace(/\/+$/, "");
|
|
48
48
|
if (hasWebSocket()) {
|
|
49
49
|
try {
|
|
50
|
-
const { report, shots: shots2, a11y, slop, narration, phantoms } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
|
|
50
|
+
const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
|
|
51
51
|
const errors2 = [];
|
|
52
52
|
for (const route of routes.slice(1)) {
|
|
53
53
|
try {
|
|
@@ -62,7 +62,7 @@ async function captureViewports(cwd, url) {
|
|
|
62
62
|
errors2.push(`${route}: capture failed`);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms };
|
|
65
|
+
return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions };
|
|
66
66
|
} catch {
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -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-C6MOKVEN.js");
|
|
137
137
|
return pixelDiffPct(chrome, previous, current);
|
|
138
138
|
}
|
|
139
139
|
function buildRuntimeFixPrompt(report) {
|
|
@@ -161,6 +161,16 @@ ${narration.join("\n")}
|
|
|
161
161
|
|
|
162
162
|
Judge this narration as an experience: does the reading order make sense? do names actually describe their targets? is anything announced as "(no accessible name)"? Fix real incoherence \u2014 this is how non-visual users meet the page.`;
|
|
163
163
|
}
|
|
164
|
+
function vtSection(viewTransitions) {
|
|
165
|
+
if (!viewTransitions || viewTransitions.length === 0) return "";
|
|
166
|
+
return `
|
|
167
|
+
|
|
168
|
+
## View-transition findings
|
|
169
|
+
|
|
170
|
+
${viewTransitions.join("\n")}
|
|
171
|
+
|
|
172
|
+
Duplicate names abort the whole transition at runtime; missing reduced-motion handling animates for users who opted out. Fix the names / add the media query rather than removing the transitions.`;
|
|
173
|
+
}
|
|
164
174
|
function phantomSection(phantoms) {
|
|
165
175
|
if (!phantoms || phantoms.length === 0) return "";
|
|
166
176
|
return `
|
|
@@ -181,13 +191,13 @@ ${findings.join("\n")}
|
|
|
181
191
|
|
|
182
192
|
These patterns make the page read as template output. Rework them within the committed design direction \u2014 this is style debt, not a defect list.`;
|
|
183
193
|
}
|
|
184
|
-
function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms) {
|
|
194
|
+
function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions) {
|
|
185
195
|
const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
|
|
186
196
|
return `Screenshots of the running app were just captured:
|
|
187
197
|
|
|
188
198
|
${list}
|
|
189
199
|
|
|
190
|
-
Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}${narrationSection(narration)}${phantomSection(phantoms)}`;
|
|
200
|
+
Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}${narrationSection(narration)}${phantomSection(phantoms)}${vtSection(viewTransitions)}`;
|
|
191
201
|
}
|
|
192
202
|
|
|
193
203
|
export {
|
package/dist/cli.js
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
comparePulse,
|
|
43
43
|
probeRuntime,
|
|
44
44
|
runtimeSummary
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-ZUEGDTAM.js";
|
|
46
46
|
import {
|
|
47
47
|
clearState,
|
|
48
48
|
loadState,
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
detectEngines,
|
|
59
59
|
getEngine
|
|
60
60
|
} from "./chunk-KVYGPLWW.js";
|
|
61
|
-
import "./chunk-
|
|
61
|
+
import "./chunk-JFFNQIBU.js";
|
|
62
62
|
import {
|
|
63
63
|
appendDecision,
|
|
64
64
|
enrich
|
|
@@ -240,7 +240,7 @@ function registerEnv(program2) {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
|
|
243
|
-
const { hasWebSocket } = await import("./cdp-
|
|
243
|
+
const { hasWebSocket } = await import("./cdp-C6MOKVEN.js");
|
|
244
244
|
const chrome = findChrome2();
|
|
245
245
|
console.log(
|
|
246
246
|
chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -463,7 +463,7 @@ function registerQuality(program2) {
|
|
|
463
463
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
464
464
|
});
|
|
465
465
|
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) => {
|
|
466
|
-
const { captureViewports: captureViewports2 } = await import("./preview-
|
|
466
|
+
const { captureViewports: captureViewports2 } = await import("./preview-Q43LOK6P.js");
|
|
467
467
|
const result = await captureViewports2(process.cwd(), url);
|
|
468
468
|
if (!result) {
|
|
469
469
|
console.error(pc4.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -825,7 +825,7 @@ ${question}`,
|
|
|
825
825
|
return;
|
|
826
826
|
}
|
|
827
827
|
await this.runTurn(
|
|
828
|
-
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
|
|
828
|
+
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions),
|
|
829
829
|
`\u{1F441} polish round ${round}/${rounds}`
|
|
830
830
|
);
|
|
831
831
|
}
|
|
@@ -1132,6 +1132,18 @@ ${rulePackSummary(soft)}`);
|
|
|
1132
1132
|
if (result.error !== "interrupted" && dev && (dev.state === "running" || dev.state === "starting")) {
|
|
1133
1133
|
await delay(1500);
|
|
1134
1134
|
const errors = dev.errorsSince(runStart);
|
|
1135
|
+
if (this.state.devUrl) {
|
|
1136
|
+
try {
|
|
1137
|
+
const { hasNextMcp, probeNextMcp } = await import("./nextMcp-42Y63M7W.js");
|
|
1138
|
+
if (hasNextMcp(this.execCwd())) {
|
|
1139
|
+
const mcp = await probeNextMcp(this.state.devUrl);
|
|
1140
|
+
if (mcp.available && mcp.errors.length > 0) {
|
|
1141
|
+
for (const err of mcp.errors) errors.push(`[next mcp] ${err}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1135
1147
|
if (errors.length > 0) {
|
|
1136
1148
|
this.addProblem(
|
|
1137
1149
|
"dev",
|
|
@@ -1163,7 +1175,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1163
1175
|
const captureResult = await this.capture();
|
|
1164
1176
|
if (captureResult) {
|
|
1165
1177
|
await this.runTurn(
|
|
1166
|
-
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms),
|
|
1178
|
+
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions),
|
|
1167
1179
|
"\u{1F441} auto-review rendered UI"
|
|
1168
1180
|
);
|
|
1169
1181
|
}
|
|
@@ -1303,6 +1315,24 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1303
1315
|
this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
|
|
1304
1316
|
}
|
|
1305
1317
|
}
|
|
1318
|
+
const vtHard = (result.viewTransitions ?? []).filter((f) => f.startsWith("duplicate"));
|
|
1319
|
+
if (vtHard.length > 0) {
|
|
1320
|
+
this.push("error", `view transitions: ${vtHard.length} broken
|
|
1321
|
+
${vtHard.join("\n")}`);
|
|
1322
|
+
this.addProblem(
|
|
1323
|
+
"runtime",
|
|
1324
|
+
`${vtHard.length} view transition(s) the browser will skip`,
|
|
1325
|
+
`Duplicate view-transition-name values make the browser abort the entire transition at runtime:
|
|
1326
|
+
|
|
1327
|
+
${vtHard.join("\n")}
|
|
1328
|
+
|
|
1329
|
+
Give each simultaneously rendered element a unique name (or scope names per item, e.g. per list key). Do not remove the transitions.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const vtSoft = (result.viewTransitions ?? []).filter((f) => !f.startsWith("duplicate"));
|
|
1333
|
+
if (vtSoft.length > 0) {
|
|
1334
|
+
this.push("status", `view transitions: ${vtSoft.join("; ")}`);
|
|
1335
|
+
}
|
|
1306
1336
|
if (result.phantoms && result.phantoms.length > 0) {
|
|
1307
1337
|
this.push("error", `phantom classes: ${result.phantoms.length} (in the DOM, absent from CSS)
|
|
1308
1338
|
${result.phantoms.slice(0, 5).join("\n")}`);
|
|
@@ -1549,8 +1579,8 @@ They are objective defects, not style preferences.`
|
|
|
1549
1579
|
this.push("error", "no Chrome/Chromium found for flows");
|
|
1550
1580
|
return;
|
|
1551
1581
|
}
|
|
1552
|
-
const { runFlow } = await import("./cdp-
|
|
1553
|
-
const { previewDir } = await import("./preview-
|
|
1582
|
+
const { runFlow } = await import("./cdp-C6MOKVEN.js");
|
|
1583
|
+
const { previewDir } = await import("./preview-Q43LOK6P.js");
|
|
1554
1584
|
this.push("status", `replaying ${flows.length} flow(s)\u2026`);
|
|
1555
1585
|
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1556
1586
|
const failures = [];
|
|
@@ -1688,7 +1718,7 @@ They are objective defects, not style preferences.`
|
|
|
1688
1718
|
const result = await this.capture();
|
|
1689
1719
|
if (result) {
|
|
1690
1720
|
await this.runTurn(
|
|
1691
|
-
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
|
|
1721
|
+
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions),
|
|
1692
1722
|
`\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
|
|
1693
1723
|
);
|
|
1694
1724
|
}
|
|
@@ -2493,7 +2523,7 @@ function registerTui(program2) {
|
|
|
2493
2523
|
}
|
|
2494
2524
|
|
|
2495
2525
|
// src/cli.tsx
|
|
2496
|
-
var VERSION = true ? "0.4.
|
|
2526
|
+
var VERSION = true ? "0.4.4" : "0.0.0-dev";
|
|
2497
2527
|
var program = new Command();
|
|
2498
2528
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2499
2529
|
registerRun(program);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/preview/nextMcp.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
function parseBody(contentType, text) {
|
|
7
|
+
try {
|
|
8
|
+
if (contentType.includes("text/event-stream")) {
|
|
9
|
+
const events = text.split("\n").filter((l) => l.startsWith("data:"));
|
|
10
|
+
const last = events.at(-1);
|
|
11
|
+
return last ? JSON.parse(last.slice(5).trim()) : null;
|
|
12
|
+
}
|
|
13
|
+
return text.trim().length > 0 ? JSON.parse(text) : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function rpc(url, sessionId, body, timeoutMs) {
|
|
19
|
+
const headers = {
|
|
20
|
+
"content-type": "application/json",
|
|
21
|
+
accept: "application/json, text/event-stream"
|
|
22
|
+
};
|
|
23
|
+
if (sessionId) headers["mcp-session-id"] = sessionId;
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers,
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
29
|
+
});
|
|
30
|
+
const newSession = res.headers.get("mcp-session-id") ?? sessionId;
|
|
31
|
+
if (res.status === 202) return { response: {}, sessionId: newSession };
|
|
32
|
+
if (!res.ok) return { response: null, sessionId: newSession };
|
|
33
|
+
const text = await res.text();
|
|
34
|
+
return { response: parseBody(res.headers.get("content-type") ?? "", text), sessionId: newSession };
|
|
35
|
+
}
|
|
36
|
+
function extractText(result) {
|
|
37
|
+
const content = result?.content;
|
|
38
|
+
if (!Array.isArray(content)) return [];
|
|
39
|
+
return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text) : "").filter((t) => t.trim().length > 0);
|
|
40
|
+
}
|
|
41
|
+
async function probeNextMcp(baseUrl, timeoutMs = 4e3) {
|
|
42
|
+
const url = new URL("/_next/mcp", baseUrl).toString();
|
|
43
|
+
const none = { available: false, tools: [], errors: [] };
|
|
44
|
+
let id = 0;
|
|
45
|
+
try {
|
|
46
|
+
const init = await rpc(url, null, {
|
|
47
|
+
jsonrpc: "2.0",
|
|
48
|
+
id: ++id,
|
|
49
|
+
method: "initialize",
|
|
50
|
+
params: {
|
|
51
|
+
protocolVersion: "2025-06-18",
|
|
52
|
+
capabilities: {},
|
|
53
|
+
clientInfo: { name: "squint", version: "0" }
|
|
54
|
+
}
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
if (!init.response?.result) return none;
|
|
57
|
+
const session = init.sessionId;
|
|
58
|
+
await rpc(url, session, { jsonrpc: "2.0", method: "notifications/initialized" }, timeoutMs).catch(() => null);
|
|
59
|
+
const list = await rpc(url, session, { jsonrpc: "2.0", id: ++id, method: "tools/list" }, timeoutMs);
|
|
60
|
+
const toolDefs = list.response?.result?.tools ?? [];
|
|
61
|
+
const tools = toolDefs.map((t) => String(t.name ?? "")).filter((n) => n.length > 0);
|
|
62
|
+
const errors = [];
|
|
63
|
+
for (const name of tools.filter((n) => /error|issue|diagnostic/i.test(n)).slice(0, 3)) {
|
|
64
|
+
const call = await rpc(url, session, {
|
|
65
|
+
jsonrpc: "2.0",
|
|
66
|
+
id: ++id,
|
|
67
|
+
method: "tools/call",
|
|
68
|
+
params: { name, arguments: {} }
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
for (const text of extractText(call.response?.result)) {
|
|
71
|
+
if (!/no (build |runtime )?errors|^\s*\[\]\s*$|no issues/i.test(text)) {
|
|
72
|
+
errors.push(`[${name}] ${text.slice(0, 600)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (errors.length >= 5) break;
|
|
76
|
+
}
|
|
77
|
+
return { available: true, tools, errors };
|
|
78
|
+
} catch {
|
|
79
|
+
return none;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function hasNextMcp(cwd) {
|
|
83
|
+
try {
|
|
84
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
85
|
+
for (const key of ["dependencies", "devDependencies"]) {
|
|
86
|
+
const range = pkg?.[key]?.next;
|
|
87
|
+
const major = range?.match(/(\d+)/)?.[1];
|
|
88
|
+
if (major) return Number.parseInt(major, 10) >= 16;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
hasNextMcp,
|
|
96
|
+
probeNextMcp
|
|
97
|
+
};
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
probeRuntime,
|
|
11
11
|
routeShotName,
|
|
12
12
|
runtimeSummary
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-ZUEGDTAM.js";
|
|
14
14
|
import "./chunk-O2S6PAJE.js";
|
|
15
15
|
import "./chunk-IMDRXXFU.js";
|
|
16
16
|
import "./chunk-KVYGPLWW.js";
|
|
17
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-JFFNQIBU.js";
|
|
18
18
|
export {
|
|
19
19
|
VIEWPORTS,
|
|
20
20
|
buildReviewPrompt,
|
package/package.json
CHANGED