@loworbitstudio/visor 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.json +1 -1
- package/dist/index.js +576 -114
- package/dist/visor-manifest.json +1 -1
- package/package.json +2 -2
package/dist/CHANGELOG.json
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { dirname as dirname10, join as
|
|
4
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
5
|
+
import { dirname as dirname10, join as join25 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
7
7
|
import { Command as Command2 } from "commander";
|
|
8
8
|
|
|
@@ -5779,8 +5779,8 @@ function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
|
|
|
5779
5779
|
}
|
|
5780
5780
|
|
|
5781
5781
|
// src/commands/sandbox/init.ts
|
|
5782
|
-
import { existsSync as existsSync21, mkdirSync as
|
|
5783
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5782
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync24, rmSync as rmSync2 } from "fs";
|
|
5783
|
+
import { isAbsolute as isAbsolute2, join as join22, resolve as resolve15, dirname as dirname9 } from "path";
|
|
5784
5784
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5785
5785
|
import * as childProcess2 from "child_process";
|
|
5786
5786
|
|
|
@@ -6022,11 +6022,18 @@ function tsconfigTemplate() {
|
|
|
6022
6022
|
}
|
|
6023
6023
|
function nextConfigTemplate() {
|
|
6024
6024
|
return `import type { NextConfig } from "next"
|
|
6025
|
+
import path from "node:path"
|
|
6026
|
+
import { fileURLToPath } from "node:url"
|
|
6027
|
+
|
|
6028
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6025
6029
|
|
|
6026
6030
|
const nextConfig: NextConfig = {
|
|
6027
6031
|
reactStrictMode: true,
|
|
6028
6032
|
// Sandbox is local-only \u2014 disable production telemetry and image opt to keep startup fast.
|
|
6029
6033
|
images: { unoptimized: true },
|
|
6034
|
+
// Anchor turbopack root to the sandbox dir so a parent-repo package-lock.json
|
|
6035
|
+
// doesn't pull the workspace root upstream (VI-440).
|
|
6036
|
+
turbopack: { root: __dirname },
|
|
6030
6037
|
}
|
|
6031
6038
|
|
|
6032
6039
|
export default nextConfig
|
|
@@ -6160,6 +6167,43 @@ export default async function ScreenPage({ params }: { params: Promise<Params> }
|
|
|
6160
6167
|
}
|
|
6161
6168
|
`;
|
|
6162
6169
|
}
|
|
6170
|
+
function prototypeScreenRouteTemplate(screenMap) {
|
|
6171
|
+
const mapLiteral = JSON.stringify(screenMap, null, 2);
|
|
6172
|
+
return `import { notFound } from "next/navigation"
|
|
6173
|
+
import { manifest } from "@/lib/sandbox-manifest"
|
|
6174
|
+
import { ScreenSample } from "@/components/sandbox-sample"
|
|
6175
|
+
|
|
6176
|
+
const SCREEN_HTML: Record<string, string> = ${mapLiteral}
|
|
6177
|
+
|
|
6178
|
+
interface Params { name: string }
|
|
6179
|
+
|
|
6180
|
+
export function generateStaticParams() {
|
|
6181
|
+
return manifest.screens.map((s) => ({ name: s.name }))
|
|
6182
|
+
}
|
|
6183
|
+
|
|
6184
|
+
export default async function ScreenPage({ params }: { params: Promise<Params> }) {
|
|
6185
|
+
const { name } = await params
|
|
6186
|
+
const entry = manifest.screens.find((s) => s.name === name)
|
|
6187
|
+
if (!entry) notFound()
|
|
6188
|
+
const htmlFile = SCREEN_HTML[entry.name]
|
|
6189
|
+
return (
|
|
6190
|
+
<main style={{ padding: "32px", maxWidth: "1440px", margin: "0 auto" }}>
|
|
6191
|
+
<h1>{entry.title}</h1>
|
|
6192
|
+
{entry.route ? <p style={{ color: "var(--text-secondary, #555)" }}><code>{entry.route}</code></p> : null}
|
|
6193
|
+
{htmlFile ? (
|
|
6194
|
+
<iframe
|
|
6195
|
+
src={\`/prototype/\${htmlFile}\`}
|
|
6196
|
+
title={entry.title}
|
|
6197
|
+
style={{ width: "100%", height: "calc(100vh - 160px)", border: "1px solid var(--border-default, #e5e7eb)", borderRadius: "8px", background: "var(--bg-surface, #f7f7f8)" }}
|
|
6198
|
+
/>
|
|
6199
|
+
) : (
|
|
6200
|
+
<ScreenSample name={entry.name} />
|
|
6201
|
+
)}
|
|
6202
|
+
</main>
|
|
6203
|
+
)
|
|
6204
|
+
}
|
|
6205
|
+
`;
|
|
6206
|
+
}
|
|
6163
6207
|
function sandboxManifestModule(manifest) {
|
|
6164
6208
|
const payload = {
|
|
6165
6209
|
pattern: manifest.pattern,
|
|
@@ -6173,7 +6217,8 @@ function sandboxManifestModule(manifest) {
|
|
|
6173
6217
|
screens: manifest.screens.map((s) => ({
|
|
6174
6218
|
name: s.name,
|
|
6175
6219
|
title: s.title,
|
|
6176
|
-
route: s.route ?? null
|
|
6220
|
+
route: s.route ?? null,
|
|
6221
|
+
kind: s.kind ?? "named"
|
|
6177
6222
|
}))
|
|
6178
6223
|
};
|
|
6179
6224
|
return `export const manifest = ${JSON.stringify(payload, null, 2)} as const
|
|
@@ -6312,8 +6357,10 @@ function readmeTemplate(manifest, port) {
|
|
|
6312
6357
|
}
|
|
6313
6358
|
function captureScriptTemplate() {
|
|
6314
6359
|
return `// Auto-generated by 'visor sandbox approve'. Do not edit by hand \u2014 rewrite via approve.
|
|
6360
|
+
// Always captures to captures/pending/ and pixel-diffs against captures/approved/ if a baseline exists.
|
|
6361
|
+
// Promotion of pending \u2192 approved happens via 'visor sandbox approve --approve' (operator review gate).
|
|
6315
6362
|
import { chromium } from "@playwright/test"
|
|
6316
|
-
import { readFile, writeFile, mkdir,
|
|
6363
|
+
import { readFile, writeFile, mkdir, rm } from "node:fs/promises"
|
|
6317
6364
|
import { existsSync } from "node:fs"
|
|
6318
6365
|
import { join, dirname } from "node:path"
|
|
6319
6366
|
import { fileURLToPath } from "node:url"
|
|
@@ -6324,7 +6371,6 @@ const sandboxDir = dirname(fileURLToPath(import.meta.url))
|
|
|
6324
6371
|
const configPath = join(sandboxDir, "sandbox.json")
|
|
6325
6372
|
const config = JSON.parse(await readFile(configPath, "utf-8"))
|
|
6326
6373
|
|
|
6327
|
-
const diffMode = process.argv.includes("--diff")
|
|
6328
6374
|
const port = Number(process.env.SANDBOX_PORT ?? config.port)
|
|
6329
6375
|
const baseUrl = \`http://localhost:\${port}\`
|
|
6330
6376
|
|
|
@@ -6333,9 +6379,11 @@ const routes = ["/", ...config.primitives.map((p) => \`/primitives/\${p.name}\`)
|
|
|
6333
6379
|
const approvedDir = join(sandboxDir, "captures", "approved")
|
|
6334
6380
|
const pendingDir = join(sandboxDir, "captures", "pending")
|
|
6335
6381
|
const diffsDir = join(sandboxDir, "captures", "diffs")
|
|
6382
|
+
await rm(pendingDir, { recursive: true, force: true })
|
|
6383
|
+
await rm(diffsDir, { recursive: true, force: true })
|
|
6336
6384
|
await mkdir(approvedDir, { recursive: true })
|
|
6337
6385
|
await mkdir(pendingDir, { recursive: true })
|
|
6338
|
-
|
|
6386
|
+
await mkdir(diffsDir, { recursive: true })
|
|
6339
6387
|
|
|
6340
6388
|
function slugify(route) {
|
|
6341
6389
|
if (route === "/") return "index"
|
|
@@ -6343,56 +6391,56 @@ function slugify(route) {
|
|
|
6343
6391
|
}
|
|
6344
6392
|
|
|
6345
6393
|
const browser = await chromium.launch()
|
|
6346
|
-
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } })
|
|
6394
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 2 })
|
|
6347
6395
|
const page = await context.newPage()
|
|
6348
6396
|
const results = []
|
|
6349
6397
|
|
|
6350
6398
|
for (const route of routes) {
|
|
6351
6399
|
const slug = slugify(route)
|
|
6352
|
-
const
|
|
6353
|
-
const pngPath = join(targetDir, \`\${slug}.png\`)
|
|
6400
|
+
const pngPath = join(pendingDir, \`\${slug}.png\`)
|
|
6354
6401
|
await page.goto(baseUrl + route, { waitUntil: "networkidle" })
|
|
6355
6402
|
const buf = await page.screenshot({ fullPage: true })
|
|
6356
6403
|
await writeFile(pngPath, buf)
|
|
6357
|
-
|
|
6404
|
+
const entry = { route, png: pngPath }
|
|
6405
|
+
results.push(entry)
|
|
6358
6406
|
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6407
|
+
const baselinePath = join(approvedDir, \`\${slug}.png\`)
|
|
6408
|
+
if (!existsSync(baselinePath)) {
|
|
6409
|
+
entry.diff = "no-baseline"
|
|
6410
|
+
continue
|
|
6411
|
+
}
|
|
6412
|
+
const baselineBuf = await readFile(baselinePath)
|
|
6413
|
+
const baseline = PNG.sync.read(baselineBuf)
|
|
6414
|
+
const candidate = PNG.sync.read(buf)
|
|
6415
|
+
if (baseline.width !== candidate.width || baseline.height !== candidate.height) {
|
|
6416
|
+
const diffPath = join(diffsDir, \`\${slug}.diff.png\`)
|
|
6417
|
+
await writeFile(diffPath, buf)
|
|
6418
|
+
entry.diff = diffPath
|
|
6419
|
+
entry.dimensionsMismatch = true
|
|
6420
|
+
continue
|
|
6421
|
+
}
|
|
6422
|
+
const diffPng = new PNG({ width: baseline.width, height: baseline.height })
|
|
6423
|
+
const pixels = pixelmatch(
|
|
6424
|
+
baseline.data,
|
|
6425
|
+
candidate.data,
|
|
6426
|
+
diffPng.data,
|
|
6427
|
+
baseline.width,
|
|
6428
|
+
baseline.height,
|
|
6429
|
+
{ threshold: 0.1 }
|
|
6430
|
+
)
|
|
6431
|
+
if (pixels > 0) {
|
|
6432
|
+
const diffPath = join(diffsDir, \`\${slug}.diff.png\`)
|
|
6433
|
+
await writeFile(diffPath, PNG.sync.write(diffPng))
|
|
6434
|
+
entry.diff = diffPath
|
|
6435
|
+
entry.changedPixels = pixels
|
|
6436
|
+
} else {
|
|
6437
|
+
entry.diff = "clean"
|
|
6390
6438
|
}
|
|
6391
6439
|
}
|
|
6392
6440
|
|
|
6393
6441
|
await browser.close()
|
|
6394
|
-
await writeFile(join(sandboxDir, "captures", "last-run.json"), JSON.stringify({
|
|
6395
|
-
console.log(JSON.stringify({ ok: true,
|
|
6442
|
+
await writeFile(join(sandboxDir, "captures", "last-run.json"), JSON.stringify({ baseUrl, routes: results }, null, 2))
|
|
6443
|
+
console.log(JSON.stringify({ ok: true, mode: "pending", routes: results }, null, 2))
|
|
6396
6444
|
`;
|
|
6397
6445
|
}
|
|
6398
6446
|
function toPascalCase(s) {
|
|
@@ -6400,7 +6448,7 @@ function toPascalCase(s) {
|
|
|
6400
6448
|
}
|
|
6401
6449
|
|
|
6402
6450
|
// src/commands/sandbox/scaffold.ts
|
|
6403
|
-
function writeScaffold(sandboxDir, manifest, port) {
|
|
6451
|
+
function writeScaffold(sandboxDir, manifest, port, options = {}) {
|
|
6404
6452
|
mkdirSync7(sandboxDir, { recursive: true });
|
|
6405
6453
|
const created = [];
|
|
6406
6454
|
const writeIfNew = (rel, contents) => {
|
|
@@ -6419,7 +6467,10 @@ function writeScaffold(sandboxDir, manifest, port) {
|
|
|
6419
6467
|
writeIfNew("app/globals.css", globalsCssPlaceholder());
|
|
6420
6468
|
writeIfNew("app/page.tsx", indexPageTemplate());
|
|
6421
6469
|
writeIfNew("app/primitives/[name]/page.tsx", primitiveRouteTemplate());
|
|
6422
|
-
writeIfNew(
|
|
6470
|
+
writeIfNew(
|
|
6471
|
+
"app/screens/[name]/page.tsx",
|
|
6472
|
+
options.prototypeImport ? prototypeScreenRouteTemplate(options.prototypeImport.screenMap) : screenRouteTemplate()
|
|
6473
|
+
);
|
|
6423
6474
|
writeIfNew("components/sandbox-sample.tsx", sandboxSampleComponent());
|
|
6424
6475
|
writeIfNew("lib/sandbox-manifest.ts", sandboxManifestModule(manifest));
|
|
6425
6476
|
writeIfNew("lib/sandbox-mocks.ts", sandboxMocksModule(manifest.mockShapes));
|
|
@@ -6447,9 +6498,21 @@ function writeSandboxConfig(sandboxDir, manifest, port, options) {
|
|
|
6447
6498
|
primitives: manifest.primitives.map((p) => ({
|
|
6448
6499
|
name: p.name,
|
|
6449
6500
|
status: p.status,
|
|
6450
|
-
viTicket: p.viTicket ?? null
|
|
6501
|
+
viTicket: p.viTicket ?? null,
|
|
6502
|
+
kind: p.kind ?? null
|
|
6451
6503
|
})),
|
|
6452
|
-
screens: manifest.screens.map((s) => ({
|
|
6504
|
+
screens: manifest.screens.map((s) => ({
|
|
6505
|
+
name: s.name,
|
|
6506
|
+
title: s.title,
|
|
6507
|
+
route: s.route ?? null,
|
|
6508
|
+
kind: s.kind ?? "named"
|
|
6509
|
+
})),
|
|
6510
|
+
fromHtmlPrototype: options.prototypeImport ? {
|
|
6511
|
+
sourceDir: options.prototypeImport.sourceDir,
|
|
6512
|
+
screenMap: options.prototypeImport.screenMap,
|
|
6513
|
+
stateCoverageScreens: options.prototypeImport.stateCoverageScreens,
|
|
6514
|
+
stripChromeSelectors: options.prototypeImport.stripChromeSelectors
|
|
6515
|
+
} : null
|
|
6453
6516
|
};
|
|
6454
6517
|
writeFileSync12(join20(sandboxDir, "sandbox.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
6455
6518
|
}
|
|
@@ -6462,6 +6525,278 @@ function sandboxIsEmpty(sandboxDir) {
|
|
|
6462
6525
|
}
|
|
6463
6526
|
}
|
|
6464
6527
|
|
|
6528
|
+
// src/commands/sandbox/html-prototype.ts
|
|
6529
|
+
import { mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync23, statSync as statSync9, writeFileSync as writeFileSync13 } from "fs";
|
|
6530
|
+
import { join as join21 } from "path";
|
|
6531
|
+
|
|
6532
|
+
// src/commands/sandbox/strip-chrome.ts
|
|
6533
|
+
var DEFAULT_STRIP_SELECTORS = [
|
|
6534
|
+
".state-callout",
|
|
6535
|
+
".state-section__header",
|
|
6536
|
+
".proto-nav",
|
|
6537
|
+
"[data-documentary-chrome]",
|
|
6538
|
+
'[style*="mint"]'
|
|
6539
|
+
];
|
|
6540
|
+
var CLASS_SELECTOR = /^\.([a-zA-Z_][\w-]*)$/;
|
|
6541
|
+
var ATTR_SUBSTRING_SELECTOR = /^\[([a-zA-Z_][\w-]*)\*=(?:"([^"]*)"|'([^']*)')\]$/;
|
|
6542
|
+
var ATTR_PRESENCE_SELECTOR = /^\[([a-zA-Z_][\w-]*)\]$/;
|
|
6543
|
+
function parseSelector(selector) {
|
|
6544
|
+
const trimmed = selector.trim();
|
|
6545
|
+
let m;
|
|
6546
|
+
if (m = trimmed.match(CLASS_SELECTOR)) {
|
|
6547
|
+
return { kind: "class", name: m[1] };
|
|
6548
|
+
}
|
|
6549
|
+
if (m = trimmed.match(ATTR_SUBSTRING_SELECTOR)) {
|
|
6550
|
+
return { kind: "attr-substring", attr: m[1], value: m[2] ?? m[3] ?? "" };
|
|
6551
|
+
}
|
|
6552
|
+
if (m = trimmed.match(ATTR_PRESENCE_SELECTOR)) {
|
|
6553
|
+
return { kind: "attr-presence", attr: m[1] };
|
|
6554
|
+
}
|
|
6555
|
+
return { kind: "unknown" };
|
|
6556
|
+
}
|
|
6557
|
+
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
6558
|
+
"area",
|
|
6559
|
+
"base",
|
|
6560
|
+
"br",
|
|
6561
|
+
"col",
|
|
6562
|
+
"embed",
|
|
6563
|
+
"hr",
|
|
6564
|
+
"img",
|
|
6565
|
+
"input",
|
|
6566
|
+
"link",
|
|
6567
|
+
"meta",
|
|
6568
|
+
"param",
|
|
6569
|
+
"source",
|
|
6570
|
+
"track",
|
|
6571
|
+
"wbr"
|
|
6572
|
+
]);
|
|
6573
|
+
function findOpenTagEnd(html, openTagStart) {
|
|
6574
|
+
let i = openTagStart;
|
|
6575
|
+
let quote = null;
|
|
6576
|
+
while (i < html.length) {
|
|
6577
|
+
const ch = html[i];
|
|
6578
|
+
if (quote) {
|
|
6579
|
+
if (ch === quote) quote = null;
|
|
6580
|
+
} else {
|
|
6581
|
+
if (ch === '"' || ch === "'") quote = ch;
|
|
6582
|
+
else if (ch === ">") return i + 1;
|
|
6583
|
+
}
|
|
6584
|
+
i++;
|
|
6585
|
+
}
|
|
6586
|
+
return -1;
|
|
6587
|
+
}
|
|
6588
|
+
function findMatchingCloseTag(html, tagName, searchFrom) {
|
|
6589
|
+
const openRe = new RegExp(`<${tagName}(?=[\\s/>])`, "gi");
|
|
6590
|
+
const closeRe = new RegExp(`</${tagName}\\s*>`, "gi");
|
|
6591
|
+
let depth = 1;
|
|
6592
|
+
let pos = searchFrom;
|
|
6593
|
+
while (pos < html.length) {
|
|
6594
|
+
openRe.lastIndex = pos;
|
|
6595
|
+
closeRe.lastIndex = pos;
|
|
6596
|
+
const openMatch = openRe.exec(html);
|
|
6597
|
+
const closeMatch = closeRe.exec(html);
|
|
6598
|
+
if (!closeMatch) return -1;
|
|
6599
|
+
if (openMatch && openMatch.index < closeMatch.index) {
|
|
6600
|
+
depth++;
|
|
6601
|
+
const past = findOpenTagEnd(html, openMatch.index);
|
|
6602
|
+
pos = past === -1 ? openMatch.index + tagName.length + 1 : past;
|
|
6603
|
+
continue;
|
|
6604
|
+
}
|
|
6605
|
+
depth--;
|
|
6606
|
+
if (depth === 0) return closeMatch.index;
|
|
6607
|
+
pos = closeMatch.index + closeMatch[0].length;
|
|
6608
|
+
}
|
|
6609
|
+
return -1;
|
|
6610
|
+
}
|
|
6611
|
+
function openTagMatches(openTag, selector) {
|
|
6612
|
+
if (selector.kind === "class") {
|
|
6613
|
+
const classAttr = /\sclass\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(openTag);
|
|
6614
|
+
if (!classAttr) return false;
|
|
6615
|
+
const value = classAttr[1] ?? classAttr[2] ?? "";
|
|
6616
|
+
return value.split(/\s+/).some((token) => token === selector.name);
|
|
6617
|
+
}
|
|
6618
|
+
if (selector.kind === "attr-substring") {
|
|
6619
|
+
const attrRe = new RegExp(
|
|
6620
|
+
`\\s${escapeRegex2(selector.attr)}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`,
|
|
6621
|
+
"i"
|
|
6622
|
+
);
|
|
6623
|
+
const m = openTag.match(attrRe);
|
|
6624
|
+
if (!m) return false;
|
|
6625
|
+
const value = m[1] ?? m[2] ?? "";
|
|
6626
|
+
return value.includes(selector.value);
|
|
6627
|
+
}
|
|
6628
|
+
if (selector.kind === "attr-presence") {
|
|
6629
|
+
const attrRe = new RegExp(
|
|
6630
|
+
`\\s${escapeRegex2(selector.attr)}(?:\\s*=|\\s|/?>)`,
|
|
6631
|
+
"i"
|
|
6632
|
+
);
|
|
6633
|
+
return attrRe.test(openTag);
|
|
6634
|
+
}
|
|
6635
|
+
return false;
|
|
6636
|
+
}
|
|
6637
|
+
function escapeRegex2(s) {
|
|
6638
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6639
|
+
}
|
|
6640
|
+
var TAG_NAME_RE = /^<([a-zA-Z][a-zA-Z0-9-]*)/;
|
|
6641
|
+
function stripOnce(html, selectors) {
|
|
6642
|
+
const tagRe = /<([a-zA-Z][a-zA-Z0-9-]*)\b/g;
|
|
6643
|
+
let m;
|
|
6644
|
+
while (m = tagRe.exec(html)) {
|
|
6645
|
+
const openTagStart = m.index;
|
|
6646
|
+
const tagNameMatch = html.slice(openTagStart, openTagStart + 200).match(TAG_NAME_RE);
|
|
6647
|
+
if (!tagNameMatch) continue;
|
|
6648
|
+
const tagName = tagNameMatch[1].toLowerCase();
|
|
6649
|
+
const openTagEnd = findOpenTagEnd(html, openTagStart);
|
|
6650
|
+
if (openTagEnd === -1) continue;
|
|
6651
|
+
const openTag = html.slice(openTagStart, openTagEnd);
|
|
6652
|
+
const matched = selectors.some((sel) => openTagMatches(openTag, sel));
|
|
6653
|
+
if (!matched) continue;
|
|
6654
|
+
let elementEnd;
|
|
6655
|
+
if (VOID_ELEMENTS.has(tagName) || /\/\s*>$/.test(openTag)) {
|
|
6656
|
+
elementEnd = openTagEnd;
|
|
6657
|
+
} else {
|
|
6658
|
+
const closeStart = findMatchingCloseTag(html, tagName, openTagEnd);
|
|
6659
|
+
if (closeStart === -1) continue;
|
|
6660
|
+
const closeRe = new RegExp(`</${tagName}\\s*>`, "i");
|
|
6661
|
+
const closeMatch = html.slice(closeStart).match(closeRe);
|
|
6662
|
+
if (!closeMatch) continue;
|
|
6663
|
+
elementEnd = closeStart + closeMatch[0].length;
|
|
6664
|
+
}
|
|
6665
|
+
let cutEnd = elementEnd;
|
|
6666
|
+
if (html[cutEnd] === "\n") cutEnd++;
|
|
6667
|
+
let cutStart = openTagStart;
|
|
6668
|
+
while (cutStart > 0 && (html[cutStart - 1] === " " || html[cutStart - 1] === " ")) {
|
|
6669
|
+
cutStart--;
|
|
6670
|
+
}
|
|
6671
|
+
if (cutStart > 0 && html[cutStart - 1] === "\n" && cutEnd <= html.length && (html[cutEnd - 1] === "\n" || cutEnd === html.length)) {
|
|
6672
|
+
cutStart--;
|
|
6673
|
+
}
|
|
6674
|
+
return {
|
|
6675
|
+
html: html.slice(0, cutStart) + html.slice(cutEnd),
|
|
6676
|
+
changed: true
|
|
6677
|
+
};
|
|
6678
|
+
}
|
|
6679
|
+
return { html, changed: false };
|
|
6680
|
+
}
|
|
6681
|
+
function resolveStripSelectors(stripChrome, additional) {
|
|
6682
|
+
if (stripChrome === void 0 || stripChrome === false) {
|
|
6683
|
+
if (additional && additional.trim() !== "") {
|
|
6684
|
+
return [...DEFAULT_STRIP_SELECTORS, ...parseList(additional)];
|
|
6685
|
+
}
|
|
6686
|
+
return [];
|
|
6687
|
+
}
|
|
6688
|
+
const base = stripChrome === true || typeof stripChrome === "string" && stripChrome.trim() === "" ? [...DEFAULT_STRIP_SELECTORS] : parseList(stripChrome);
|
|
6689
|
+
if (additional && additional.trim() !== "") {
|
|
6690
|
+
return [...base, ...parseList(additional)];
|
|
6691
|
+
}
|
|
6692
|
+
return base;
|
|
6693
|
+
}
|
|
6694
|
+
function parseList(input) {
|
|
6695
|
+
return input.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6696
|
+
}
|
|
6697
|
+
function stripDocumentaryChrome(html, selectors) {
|
|
6698
|
+
if (selectors.length === 0) return html;
|
|
6699
|
+
const parsed = selectors.map(parseSelector).filter((s) => s.kind !== "unknown");
|
|
6700
|
+
if (parsed.length === 0) return html;
|
|
6701
|
+
let current = html;
|
|
6702
|
+
for (let i = 0; i < 1e3; i++) {
|
|
6703
|
+
const { html: next, changed } = stripOnce(current, parsed);
|
|
6704
|
+
if (!changed) break;
|
|
6705
|
+
current = next;
|
|
6706
|
+
}
|
|
6707
|
+
return current;
|
|
6708
|
+
}
|
|
6709
|
+
|
|
6710
|
+
// src/commands/sandbox/html-prototype.ts
|
|
6711
|
+
var SCREEN_FILE_PATTERN = /^screen-(\d+)-([^/.]+)\.html$/i;
|
|
6712
|
+
function copyHtmlPrototype(sourceDir, sandboxDir, manifest, options = {}) {
|
|
6713
|
+
const warnings = [];
|
|
6714
|
+
const destAbs = join21(sandboxDir, "public", "prototype");
|
|
6715
|
+
mkdirSync8(destAbs, { recursive: true });
|
|
6716
|
+
const stripChromeSelectors = options.stripChromeSelectors ?? [];
|
|
6717
|
+
const copiedFiles = copyTreeRelative(sourceDir, destAbs, "", stripChromeSelectors);
|
|
6718
|
+
const screenFiles = listOrderedScreenFiles(sourceDir);
|
|
6719
|
+
if (screenFiles.length === 0) {
|
|
6720
|
+
warnings.push(
|
|
6721
|
+
`HTML prototype at ${sourceDir} has no 'screen-N-*.html' files \u2014 screen routes will fall back to placeholder.`
|
|
6722
|
+
);
|
|
6723
|
+
}
|
|
6724
|
+
const screenMap = {};
|
|
6725
|
+
manifest.screens.forEach((screen, idx) => {
|
|
6726
|
+
const file = screenFiles[idx];
|
|
6727
|
+
if (file) {
|
|
6728
|
+
screenMap[screen.name] = file;
|
|
6729
|
+
} else {
|
|
6730
|
+
warnings.push(
|
|
6731
|
+
`Manifest screen '${screen.name}' has no matching prototype HTML at position ${idx + 1} \u2014 route will use placeholder.`
|
|
6732
|
+
);
|
|
6733
|
+
}
|
|
6734
|
+
});
|
|
6735
|
+
const stateCoverageScreens = [];
|
|
6736
|
+
if (screenFiles.length > manifest.screens.length) {
|
|
6737
|
+
const extras = screenFiles.slice(manifest.screens.length);
|
|
6738
|
+
const existingNames = new Set(manifest.screens.map((s) => s.name));
|
|
6739
|
+
for (const file of extras) {
|
|
6740
|
+
const entry = deriveStateCoverageScreen(file, existingNames);
|
|
6741
|
+
manifest.screens.push(entry);
|
|
6742
|
+
existingNames.add(entry.name);
|
|
6743
|
+
screenMap[entry.name] = file;
|
|
6744
|
+
stateCoverageScreens.push(entry.name);
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6747
|
+
return {
|
|
6748
|
+
sourceDir,
|
|
6749
|
+
destDir: "public/prototype",
|
|
6750
|
+
copiedFiles,
|
|
6751
|
+
screenMap,
|
|
6752
|
+
stateCoverageScreens,
|
|
6753
|
+
stripChromeSelectors,
|
|
6754
|
+
warnings
|
|
6755
|
+
};
|
|
6756
|
+
}
|
|
6757
|
+
function deriveStateCoverageScreen(file, existingNames) {
|
|
6758
|
+
const m = file.match(SCREEN_FILE_PATTERN);
|
|
6759
|
+
const idx = m ? m[1] : "0";
|
|
6760
|
+
const suffix = m ? m[2].toLowerCase() : "extra";
|
|
6761
|
+
let slug2 = `state-coverage-${suffix}`;
|
|
6762
|
+
if (existingNames.has(slug2)) slug2 = `${slug2}-${idx}`;
|
|
6763
|
+
const title = `State coverage: ${suffix.replace(/-/g, " ")}`;
|
|
6764
|
+
return { name: slug2, title, kind: "state-coverage" };
|
|
6765
|
+
}
|
|
6766
|
+
function copyTreeRelative(srcDir, destDir, relDir = "", stripChromeSelectors = []) {
|
|
6767
|
+
const out = [];
|
|
6768
|
+
const entries = readdirSync13(join21(srcDir, relDir));
|
|
6769
|
+
for (const name of entries) {
|
|
6770
|
+
if (name.startsWith(".")) continue;
|
|
6771
|
+
const srcPath = join21(srcDir, relDir, name);
|
|
6772
|
+
const destPath = join21(destDir, relDir, name);
|
|
6773
|
+
const stat = statSync9(srcPath);
|
|
6774
|
+
if (stat.isDirectory()) {
|
|
6775
|
+
mkdirSync8(destPath, { recursive: true });
|
|
6776
|
+
out.push(...copyTreeRelative(srcDir, destDir, join21(relDir, name), stripChromeSelectors));
|
|
6777
|
+
} else if (stat.isFile()) {
|
|
6778
|
+
if (stripChromeSelectors.length > 0 && name.toLowerCase().endsWith(".html")) {
|
|
6779
|
+
const html = readFileSync23(srcPath, "utf-8");
|
|
6780
|
+
const stripped = stripDocumentaryChrome(html, stripChromeSelectors);
|
|
6781
|
+
writeFileSync13(destPath, stripped);
|
|
6782
|
+
} else {
|
|
6783
|
+
writeFileSync13(destPath, readFileSync23(srcPath));
|
|
6784
|
+
}
|
|
6785
|
+
out.push(join21("public", "prototype", relDir, name).replace(/\\/g, "/"));
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
return out;
|
|
6789
|
+
}
|
|
6790
|
+
function listOrderedScreenFiles(srcDir) {
|
|
6791
|
+
const names = readdirSync13(srcDir).filter((n) => SCREEN_FILE_PATTERN.test(n));
|
|
6792
|
+
names.sort((a, b) => {
|
|
6793
|
+
const na = Number(a.match(SCREEN_FILE_PATTERN)?.[1] ?? 0);
|
|
6794
|
+
const nb = Number(b.match(SCREEN_FILE_PATTERN)?.[1] ?? 0);
|
|
6795
|
+
return na - nb;
|
|
6796
|
+
});
|
|
6797
|
+
return names;
|
|
6798
|
+
}
|
|
6799
|
+
|
|
6465
6800
|
// src/commands/sandbox/init.ts
|
|
6466
6801
|
async function sandboxInitCommand(name, cwd, options) {
|
|
6467
6802
|
const json = options.json ?? false;
|
|
@@ -6509,7 +6844,7 @@ async function runInit(name, cwd, options) {
|
|
|
6509
6844
|
`Handoff has no primitives \u2014 refusing to scaffold an empty sandbox. Check the manifest at ${handoffPath}`
|
|
6510
6845
|
);
|
|
6511
6846
|
}
|
|
6512
|
-
const sandboxDir =
|
|
6847
|
+
const sandboxDir = join22(cwd, ".lo", "sandbox", name);
|
|
6513
6848
|
if (!sandboxIsEmpty(sandboxDir)) {
|
|
6514
6849
|
if (!options.overwrite) {
|
|
6515
6850
|
throw new Error(
|
|
@@ -6518,23 +6853,48 @@ async function runInit(name, cwd, options) {
|
|
|
6518
6853
|
}
|
|
6519
6854
|
rmSync2(sandboxDir, { recursive: true, force: true });
|
|
6520
6855
|
}
|
|
6521
|
-
|
|
6856
|
+
mkdirSync9(sandboxDir, { recursive: true });
|
|
6522
6857
|
const port = await findOpenPort();
|
|
6523
|
-
|
|
6858
|
+
let prototypeImport;
|
|
6859
|
+
if (options.fromHtmlPrototype) {
|
|
6860
|
+
const prototypeDir = isAbsolute2(options.fromHtmlPrototype) ? options.fromHtmlPrototype : resolve15(cwd, options.fromHtmlPrototype);
|
|
6861
|
+
if (!existsSync21(prototypeDir)) {
|
|
6862
|
+
throw new Error(`HTML prototype directory not found: ${prototypeDir}`);
|
|
6863
|
+
}
|
|
6864
|
+
const stripChromeSelectors = resolveStripSelectors(
|
|
6865
|
+
options.stripChrome,
|
|
6866
|
+
options.stripChromeAdditional
|
|
6867
|
+
);
|
|
6868
|
+
prototypeImport = copyHtmlPrototype(prototypeDir, sandboxDir, manifest, {
|
|
6869
|
+
stripChromeSelectors
|
|
6870
|
+
});
|
|
6871
|
+
for (const w of prototypeImport.warnings) manifest.warnings.push(w);
|
|
6872
|
+
}
|
|
6873
|
+
const known = loadKnownPrimitives();
|
|
6874
|
+
for (const p of manifest.primitives) {
|
|
6875
|
+
if (p.status !== "shipped" && p.status !== "gap-inflight") continue;
|
|
6876
|
+
if (known.has(p.name)) continue;
|
|
6877
|
+
p.status = "compose-recipe";
|
|
6878
|
+
p.viTicket = void 0;
|
|
6879
|
+
manifest.warnings.push(
|
|
6880
|
+
`'${p.name}' declared shipped in the handoff but absent from the registry \u2014 reclassified as compose-recipe`
|
|
6881
|
+
);
|
|
6882
|
+
}
|
|
6883
|
+
writeScaffold(sandboxDir, manifest, port, { prototypeImport });
|
|
6524
6884
|
if (!options.skipInstall) {
|
|
6525
6885
|
runNpmInstall(sandboxDir, options.json ?? false);
|
|
6526
6886
|
}
|
|
6527
|
-
applyThemeIfPossible(
|
|
6528
|
-
|
|
6887
|
+
applyThemeIfPossible(
|
|
6888
|
+
sandboxDir,
|
|
6889
|
+
manifest,
|
|
6890
|
+
options.theme,
|
|
6891
|
+
cwd,
|
|
6892
|
+
options.json ?? false,
|
|
6893
|
+
options.themeFile
|
|
6894
|
+
);
|
|
6529
6895
|
const shipped = [];
|
|
6530
6896
|
for (const p of manifest.primitives) {
|
|
6531
6897
|
if (p.status !== "shipped" && p.status !== "gap-inflight") continue;
|
|
6532
|
-
if (!known.has(p.name)) {
|
|
6533
|
-
manifest.warnings.push(
|
|
6534
|
-
`'${p.name}' is declared shipped in the handoff but is not in the registry \u2014 skipped`
|
|
6535
|
-
);
|
|
6536
|
-
continue;
|
|
6537
|
-
}
|
|
6538
6898
|
const ok = tryAddPrimitive(p, sandboxDir, options.json ?? false);
|
|
6539
6899
|
if (ok) shipped.push(p.name);
|
|
6540
6900
|
}
|
|
@@ -6542,34 +6902,50 @@ async function runInit(name, cwd, options) {
|
|
|
6542
6902
|
writeSandboxConfig(sandboxDir, manifest, port, {
|
|
6543
6903
|
handoffPath,
|
|
6544
6904
|
theme: options.theme,
|
|
6545
|
-
visorVersion: readCliVersion()
|
|
6905
|
+
visorVersion: readCliVersion(),
|
|
6906
|
+
prototypeImport
|
|
6546
6907
|
});
|
|
6547
6908
|
return {
|
|
6548
6909
|
success: true,
|
|
6549
6910
|
sandboxDir,
|
|
6550
6911
|
port,
|
|
6551
6912
|
primitives: { shipped, gaps },
|
|
6913
|
+
prototypeImport,
|
|
6552
6914
|
warnings: manifest.warnings
|
|
6553
6915
|
};
|
|
6554
6916
|
}
|
|
6555
|
-
function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json) {
|
|
6556
|
-
const
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6917
|
+
function applyThemeIfPossible(sandboxDir, manifest, theme2, cwd, json, themeFile) {
|
|
6918
|
+
const candidates = [];
|
|
6919
|
+
if (themeFile) {
|
|
6920
|
+
candidates.push(isAbsolute2(themeFile) ? themeFile : resolve15(cwd, themeFile));
|
|
6921
|
+
}
|
|
6922
|
+
candidates.push(isAbsolute2(theme2) ? theme2 : resolve15(cwd, theme2));
|
|
6923
|
+
const privateRoot = process.env.VISOR_THEMES_PRIVATE_PATH;
|
|
6924
|
+
if (privateRoot && privateRoot.length > 0) {
|
|
6925
|
+
candidates.push(join22(privateRoot, "themes", theme2, "theme.visor.yaml"));
|
|
6926
|
+
}
|
|
6927
|
+
candidates.push(
|
|
6928
|
+
join22(cwd, "themes", `${theme2}.visor.yaml`),
|
|
6929
|
+
join22(cwd, "custom-themes", `${theme2}.visor.yaml`)
|
|
6930
|
+
);
|
|
6562
6931
|
const yamlPath = candidates.find((p) => existsSync21(p));
|
|
6563
6932
|
if (!yamlPath) {
|
|
6933
|
+
const searched = candidates.join(", ");
|
|
6564
6934
|
manifest.warnings.push(
|
|
6565
|
-
`Theme '${theme2}' not found
|
|
6935
|
+
`Theme '${theme2}' not found (searched: ${searched}) \u2014 sandbox uses placeholder globals.css. Run 'npx visor theme apply <path-to-theme.visor.yaml> --adapter nextjs -o ${join22(sandboxDir, "app", "globals.css")}' manually, or re-run with --theme-file <path>, or set VISOR_THEMES_PRIVATE_PATH to a directory containing themes/${theme2}/theme.visor.yaml.`
|
|
6566
6936
|
);
|
|
6567
6937
|
if (!json) {
|
|
6568
6938
|
logger.warn(`Theme '${theme2}' not found \u2014 leaving placeholder globals.css.`);
|
|
6939
|
+
logger.item(
|
|
6940
|
+
`Re-run with --theme-file <path>, or set VISOR_THEMES_PRIVATE_PATH=<dir-containing-themes/${theme2}/theme.visor.yaml>.`
|
|
6941
|
+
);
|
|
6942
|
+
logger.item(
|
|
6943
|
+
`Or apply manually: npx visor theme apply <path> --adapter nextjs -o ${join22(sandboxDir, "app", "globals.css")}`
|
|
6944
|
+
);
|
|
6569
6945
|
}
|
|
6570
6946
|
return;
|
|
6571
6947
|
}
|
|
6572
|
-
const globalsOut =
|
|
6948
|
+
const globalsOut = join22(sandboxDir, "app", "globals.css");
|
|
6573
6949
|
try {
|
|
6574
6950
|
themeApplyCommand(yamlPath, sandboxDir, {
|
|
6575
6951
|
output: globalsOut,
|
|
@@ -6625,9 +7001,9 @@ function readCliVersion() {
|
|
|
6625
7001
|
const here = dirname9(fileURLToPath3(import.meta.url));
|
|
6626
7002
|
for (let i = 0; i < 6; i++) {
|
|
6627
7003
|
const segments = new Array(i).fill("..");
|
|
6628
|
-
const candidate =
|
|
7004
|
+
const candidate = join22(here, ...segments, "package.json");
|
|
6629
7005
|
try {
|
|
6630
|
-
const pkg2 = JSON.parse(
|
|
7006
|
+
const pkg2 = JSON.parse(readFileSync24(candidate, "utf-8"));
|
|
6631
7007
|
if (pkg2.name === "@loworbitstudio/visor" && pkg2.version) return pkg2.version;
|
|
6632
7008
|
} catch {
|
|
6633
7009
|
}
|
|
@@ -6638,13 +7014,13 @@ function readCliVersion() {
|
|
|
6638
7014
|
}
|
|
6639
7015
|
|
|
6640
7016
|
// src/commands/sandbox/dev.ts
|
|
6641
|
-
import { existsSync as existsSync22, readFileSync as
|
|
6642
|
-
import { join as
|
|
7017
|
+
import { existsSync as existsSync22, readFileSync as readFileSync25 } from "fs";
|
|
7018
|
+
import { join as join23 } from "path";
|
|
6643
7019
|
import * as childProcess3 from "child_process";
|
|
6644
7020
|
function sandboxDevCommand(cwd, options) {
|
|
6645
7021
|
const json = options.json ?? false;
|
|
6646
|
-
const sandboxDir =
|
|
6647
|
-
const configPath =
|
|
7022
|
+
const sandboxDir = join23(cwd, ".lo", "sandbox", options.name);
|
|
7023
|
+
const configPath = join23(sandboxDir, "sandbox.json");
|
|
6648
7024
|
if (!existsSync22(configPath)) {
|
|
6649
7025
|
fail(
|
|
6650
7026
|
json,
|
|
@@ -6654,7 +7030,7 @@ function sandboxDevCommand(cwd, options) {
|
|
|
6654
7030
|
}
|
|
6655
7031
|
let config;
|
|
6656
7032
|
try {
|
|
6657
|
-
config = JSON.parse(
|
|
7033
|
+
config = JSON.parse(readFileSync25(configPath, "utf-8"));
|
|
6658
7034
|
} catch (err) {
|
|
6659
7035
|
fail(json, `Invalid sandbox.json at ${configPath}: ${err.message}`);
|
|
6660
7036
|
return;
|
|
@@ -6710,13 +7086,21 @@ function fail(json, message) {
|
|
|
6710
7086
|
}
|
|
6711
7087
|
|
|
6712
7088
|
// src/commands/sandbox/approve.ts
|
|
6713
|
-
import {
|
|
6714
|
-
|
|
7089
|
+
import {
|
|
7090
|
+
copyFileSync as copyFileSync2,
|
|
7091
|
+
existsSync as existsSync23,
|
|
7092
|
+
mkdirSync as mkdirSync10,
|
|
7093
|
+
readFileSync as readFileSync26,
|
|
7094
|
+
readdirSync as readdirSync14,
|
|
7095
|
+
rmSync as rmSync3,
|
|
7096
|
+
writeFileSync as writeFileSync14
|
|
7097
|
+
} from "fs";
|
|
7098
|
+
import { basename as basename6, join as join24 } from "path";
|
|
6715
7099
|
import * as childProcess4 from "child_process";
|
|
6716
7100
|
function sandboxApproveCommand(cwd, options) {
|
|
6717
7101
|
const json = options.json ?? false;
|
|
6718
|
-
const sandboxDir =
|
|
6719
|
-
const configPath =
|
|
7102
|
+
const sandboxDir = join24(cwd, ".lo", "sandbox", options.name);
|
|
7103
|
+
const configPath = join24(sandboxDir, "sandbox.json");
|
|
6720
7104
|
if (!existsSync23(configPath)) {
|
|
6721
7105
|
fail2(
|
|
6722
7106
|
json,
|
|
@@ -6724,18 +7108,27 @@ function sandboxApproveCommand(cwd, options) {
|
|
|
6724
7108
|
);
|
|
6725
7109
|
return;
|
|
6726
7110
|
}
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
7111
|
+
if (options.approve) {
|
|
7112
|
+
runPromotion(sandboxDir, options.name, json);
|
|
7113
|
+
return;
|
|
7114
|
+
}
|
|
7115
|
+
runCapture(sandboxDir, configPath, options.name, json);
|
|
7116
|
+
}
|
|
7117
|
+
function runCapture(sandboxDir, configPath, sandboxName, json) {
|
|
7118
|
+
const config = JSON.parse(readFileSync26(configPath, "utf-8"));
|
|
7119
|
+
const captureScriptPath = join24(sandboxDir, "playwright.capture.mjs");
|
|
7120
|
+
writeFileSync14(captureScriptPath, captureScriptTemplate(), "utf-8");
|
|
6730
7121
|
ensurePlaywrightInstalled(sandboxDir, json);
|
|
6731
|
-
const
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
7122
|
+
const result = childProcess4.spawnSync(
|
|
7123
|
+
"npx",
|
|
7124
|
+
["--no-install", "node", "playwright.capture.mjs"],
|
|
7125
|
+
{
|
|
7126
|
+
cwd: sandboxDir,
|
|
7127
|
+
stdio: "pipe",
|
|
7128
|
+
env: { ...process.env, SANDBOX_PORT: String(config.port) },
|
|
7129
|
+
encoding: "utf-8"
|
|
7130
|
+
}
|
|
7131
|
+
);
|
|
6739
7132
|
if (result.error) {
|
|
6740
7133
|
fail2(json, `Capture failed to start: ${result.error.message}`);
|
|
6741
7134
|
return;
|
|
@@ -6745,20 +7138,24 @@ function sandboxApproveCommand(cwd, options) {
|
|
|
6745
7138
|
${result.stderr}`);
|
|
6746
7139
|
return;
|
|
6747
7140
|
}
|
|
6748
|
-
const
|
|
6749
|
-
const diffsDir =
|
|
6750
|
-
const
|
|
6751
|
-
const
|
|
7141
|
+
const pendingDir = join24(sandboxDir, "captures", "pending");
|
|
7142
|
+
const diffsDir = join24(sandboxDir, "captures", "diffs");
|
|
7143
|
+
const approvedDir = join24(sandboxDir, "captures", "approved");
|
|
7144
|
+
const pendingFiles = safeListPngs(pendingDir);
|
|
7145
|
+
const diffFiles = safeListPngs(diffsDir);
|
|
7146
|
+
const hasBaseline = safeListPngs(approvedDir).length > 0;
|
|
6752
7147
|
if (json) {
|
|
6753
7148
|
console.log(
|
|
6754
7149
|
JSON.stringify(
|
|
6755
7150
|
{
|
|
6756
7151
|
success: true,
|
|
6757
|
-
mode:
|
|
7152
|
+
mode: "pending",
|
|
7153
|
+
pendingDir,
|
|
7154
|
+
diffsDir,
|
|
6758
7155
|
approvedDir,
|
|
6759
|
-
|
|
6760
|
-
approved: approvedFiles,
|
|
7156
|
+
pending: pendingFiles,
|
|
6761
7157
|
diffs: diffFiles,
|
|
7158
|
+
hasBaseline,
|
|
6762
7159
|
captureOutput: tryParseJson(result.stdout)
|
|
6763
7160
|
},
|
|
6764
7161
|
null,
|
|
@@ -6767,34 +7164,75 @@ ${result.stderr}`);
|
|
|
6767
7164
|
);
|
|
6768
7165
|
} else {
|
|
6769
7166
|
logger.success(
|
|
6770
|
-
|
|
7167
|
+
`Captured ${pendingFiles.length} PNGs in ${pendingDir}` + (hasBaseline ? ` (${diffFiles.length} differ from approved baseline)` : " (no prior baseline \u2014 first capture)")
|
|
6771
7168
|
);
|
|
6772
|
-
|
|
6773
|
-
|
|
7169
|
+
logger.blank();
|
|
7170
|
+
if (diffFiles.length > 0) {
|
|
6774
7171
|
logger.info("Changed routes:");
|
|
6775
7172
|
for (const f of diffFiles) logger.item(f);
|
|
7173
|
+
logger.blank();
|
|
6776
7174
|
}
|
|
7175
|
+
logger.info("Review captures/pending/ (and captures/diffs/), then promote with:");
|
|
7176
|
+
logger.item(`npx visor sandbox approve --name ${sandboxName} --approve`);
|
|
7177
|
+
}
|
|
7178
|
+
}
|
|
7179
|
+
function runPromotion(sandboxDir, sandboxName, json) {
|
|
7180
|
+
const pendingDir = join24(sandboxDir, "captures", "pending");
|
|
7181
|
+
const approvedDir = join24(sandboxDir, "captures", "approved");
|
|
7182
|
+
const diffsDir = join24(sandboxDir, "captures", "diffs");
|
|
7183
|
+
const pendingFiles = safeListPngs(pendingDir);
|
|
7184
|
+
if (pendingFiles.length === 0) {
|
|
7185
|
+
fail2(
|
|
7186
|
+
json,
|
|
7187
|
+
`No pending captures found at ${pendingDir}. Run 'visor sandbox approve --name ${sandboxName}' (without --approve) first to capture and review.`
|
|
7188
|
+
);
|
|
7189
|
+
return;
|
|
7190
|
+
}
|
|
7191
|
+
mkdirSync10(approvedDir, { recursive: true });
|
|
7192
|
+
const promoted = [];
|
|
7193
|
+
for (const src of pendingFiles) {
|
|
7194
|
+
const dest = join24(approvedDir, basename6(src));
|
|
7195
|
+
copyFileSync2(src, dest);
|
|
7196
|
+
promoted.push(dest);
|
|
7197
|
+
}
|
|
7198
|
+
rmSync3(pendingDir, { recursive: true, force: true });
|
|
7199
|
+
rmSync3(diffsDir, { recursive: true, force: true });
|
|
7200
|
+
if (json) {
|
|
7201
|
+
console.log(
|
|
7202
|
+
JSON.stringify(
|
|
7203
|
+
{ success: true, mode: "approve", approvedDir, promoted },
|
|
7204
|
+
null,
|
|
7205
|
+
2
|
|
7206
|
+
)
|
|
7207
|
+
);
|
|
7208
|
+
} else {
|
|
7209
|
+
logger.success(`Promoted ${promoted.length} pending captures \u2192 ${approvedDir}`);
|
|
7210
|
+
logger.info("Pending and diff directories cleared.");
|
|
6777
7211
|
}
|
|
6778
7212
|
}
|
|
6779
7213
|
function ensurePlaywrightInstalled(sandboxDir, json) {
|
|
6780
|
-
const markerPath =
|
|
7214
|
+
const markerPath = join24(sandboxDir, ".playwright-installed");
|
|
6781
7215
|
if (existsSync23(markerPath)) return;
|
|
6782
7216
|
if (!json) logger.info("Installing Playwright Chromium (one-time)...");
|
|
6783
|
-
const result = childProcess4.spawnSync(
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
7217
|
+
const result = childProcess4.spawnSync(
|
|
7218
|
+
"npx",
|
|
7219
|
+
["--no-install", "playwright", "install", "chromium"],
|
|
7220
|
+
{
|
|
7221
|
+
cwd: sandboxDir,
|
|
7222
|
+
stdio: json ? "ignore" : "inherit"
|
|
7223
|
+
}
|
|
7224
|
+
);
|
|
6787
7225
|
if (result.error) {
|
|
6788
7226
|
throw new Error(`playwright install failed to start: ${result.error.message}`);
|
|
6789
7227
|
}
|
|
6790
7228
|
if (typeof result.status === "number" && result.status !== 0) {
|
|
6791
7229
|
throw new Error(`playwright install exited with code ${result.status}`);
|
|
6792
7230
|
}
|
|
6793
|
-
|
|
7231
|
+
writeFileSync14(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
|
|
6794
7232
|
}
|
|
6795
7233
|
function safeListPngs(dir) {
|
|
6796
7234
|
if (!existsSync23(dir)) return [];
|
|
6797
|
-
return
|
|
7235
|
+
return readdirSync14(dir).filter((f) => f.endsWith(".png")).map((f) => join24(dir, f));
|
|
6798
7236
|
}
|
|
6799
7237
|
function tryParseJson(s) {
|
|
6800
7238
|
try {
|
|
@@ -6815,7 +7253,7 @@ function fail2(json, message) {
|
|
|
6815
7253
|
// src/index.ts
|
|
6816
7254
|
var __dirname2 = dirname10(fileURLToPath4(import.meta.url));
|
|
6817
7255
|
var pkg = JSON.parse(
|
|
6818
|
-
|
|
7256
|
+
readFileSync27(join25(__dirname2, "..", "package.json"), "utf-8")
|
|
6819
7257
|
);
|
|
6820
7258
|
var program = new Command2();
|
|
6821
7259
|
program.name("visor").description("CLI for the Visor design system").version(pkg.version);
|
|
@@ -6970,7 +7408,19 @@ migrate.command("token-substitution").description(
|
|
|
6970
7408
|
var sandbox = program.command("sandbox").description("Scaffold and iterate on a Next.js sandbox for new primitives");
|
|
6971
7409
|
sandbox.command("init").description(
|
|
6972
7410
|
"Scaffold a Next.js sandbox at .lo/sandbox/<name>/ from a design-handoff manifest"
|
|
6973
|
-
).argument("<name>", "sandbox name (used as directory and pattern slug)").requiredOption("--handoff <path>", "path to design-handoff.md manifest").requiredOption("--theme <theme>", "theme slug (e.g. 'space') or path to .visor.yaml").option(
|
|
7411
|
+
).argument("<name>", "sandbox name (used as directory and pattern slug)").requiredOption("--handoff <path>", "path to design-handoff.md manifest").requiredOption("--theme <theme>", "theme slug (e.g. 'space') or path to .visor.yaml").option(
|
|
7412
|
+
"--theme-file <path>",
|
|
7413
|
+
"explicit path to a theme.visor.yaml \u2014 bypasses name resolution and the VISOR_THEMES_PRIVATE_PATH env var"
|
|
7414
|
+
).option(
|
|
7415
|
+
"--from-html-prototype <path>",
|
|
7416
|
+
"import a Phase 1.5 HTML prototype directory (its screen-N-*.html files become screen routes via iframe)"
|
|
7417
|
+
).option(
|
|
7418
|
+
"--strip-chrome [selectors]",
|
|
7419
|
+
"strip Phase 1.5 documentary chrome from imported prototype HTML (no arg = defaults; comma list = replace defaults)"
|
|
7420
|
+
).option(
|
|
7421
|
+
"--strip-chrome-additional <selectors>",
|
|
7422
|
+
"extra selectors to merge with the strip-chrome base list (comma-separated)"
|
|
7423
|
+
).option("--overwrite", "replace an existing sandbox at this name", false).option("--skip-install", "skip npm install (test fixture mode)", false).option("--json", "output structured JSON (for AI agents)").action(
|
|
6974
7424
|
async (name, options) => {
|
|
6975
7425
|
await sandboxInitCommand(name, process.cwd(), options);
|
|
6976
7426
|
}
|
|
@@ -6978,7 +7428,19 @@ sandbox.command("init").description(
|
|
|
6978
7428
|
sandbox.command("dev").description("Boot the Next.js dev server for a sandbox on its allocated port").requiredOption("--name <name>", "sandbox name (created via 'sandbox init')").option("--json", "output structured JSON (for AI agents)").action((options) => {
|
|
6979
7429
|
sandboxDevCommand(process.cwd(), options);
|
|
6980
7430
|
});
|
|
6981
|
-
sandbox.command("approve").description(
|
|
6982
|
-
|
|
6983
|
-
|
|
7431
|
+
sandbox.command("approve").description(
|
|
7432
|
+
"Capture sandbox routes into captures/pending/ (with auto-diff vs the approved baseline). Pass --approve to promote pending \u2192 approved."
|
|
7433
|
+
).requiredOption("--name <name>", "sandbox name (created via 'sandbox init')").option(
|
|
7434
|
+
"--approve",
|
|
7435
|
+
"promote captures/pending/ \u2192 captures/approved/ (skips capture; assumes prior run populated pending)",
|
|
7436
|
+
false
|
|
7437
|
+
).option(
|
|
7438
|
+
"--diff",
|
|
7439
|
+
"[deprecated, no-op] default behavior already captures to pending and diffs against approved",
|
|
7440
|
+
false
|
|
7441
|
+
).option("--json", "output structured JSON (for AI agents)").action(
|
|
7442
|
+
(options) => {
|
|
7443
|
+
sandboxApproveCommand(process.cwd(), options);
|
|
7444
|
+
}
|
|
7445
|
+
);
|
|
6984
7446
|
program.parse();
|
package/dist/visor-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loworbitstudio/visor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "CLI for the Visor design system — add components, hooks, and utilities to your project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@aws-sdk/client-s3": "^3.1021.0",
|
|
47
47
|
"@babel/parser": "^7.26.0",
|
|
48
|
-
"@loworbitstudio/visor-theme-engine": "^0.
|
|
48
|
+
"@loworbitstudio/visor-theme-engine": "^0.9.0",
|
|
49
49
|
"commander": "^13.1.0",
|
|
50
50
|
"diff": "^8.0.4",
|
|
51
51
|
"picocolors": "^1.1.1",
|