@loworbitstudio/visor 0.7.0 → 0.9.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/README.md +99 -0
- package/dist/CHANGELOG.json +37 -1
- package/dist/index.js +1365 -300
- package/dist/registry.json +183 -2
- package/dist/visor-manifest.json +741 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { Command as Command2 } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/check.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
+
import { statSync as statSync3 } from "fs";
|
|
9
|
+
import { resolve as resolve3, dirname as dirname2 } from "path";
|
|
8
10
|
|
|
9
11
|
// src/registry/resolve.ts
|
|
10
12
|
import { readFileSync } from "fs";
|
|
@@ -310,7 +312,7 @@ function collectJsxFindings(source, filePath, parse) {
|
|
|
310
312
|
}
|
|
311
313
|
const mapping = NATIVE_TO_VISOR[tagName];
|
|
312
314
|
if (!mapping) return;
|
|
313
|
-
const
|
|
315
|
+
const finding2 = {
|
|
314
316
|
file: filePath,
|
|
315
317
|
line,
|
|
316
318
|
column,
|
|
@@ -318,8 +320,8 @@ function collectJsxFindings(source, filePath, parse) {
|
|
|
318
320
|
suggestedPrimitive: mapping.displayName,
|
|
319
321
|
installCmd: `npx visor add ${mapping.visorName}`
|
|
320
322
|
};
|
|
321
|
-
if (mapping.notes)
|
|
322
|
-
findings.push(
|
|
323
|
+
if (mapping.notes) finding2.rationale = mapping.notes;
|
|
324
|
+
findings.push(finding2);
|
|
323
325
|
});
|
|
324
326
|
return findings;
|
|
325
327
|
}
|
|
@@ -377,6 +379,491 @@ async function scanJsx(pathArg) {
|
|
|
377
379
|
};
|
|
378
380
|
}
|
|
379
381
|
|
|
382
|
+
// src/check/design.ts
|
|
383
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2, existsSync } from "fs";
|
|
384
|
+
import { resolve as resolve2, extname as extname2, join as join3, basename } from "path";
|
|
385
|
+
function loadVisorRc(dir) {
|
|
386
|
+
const rcPath = join3(dir, ".visorrc.json");
|
|
387
|
+
if (!existsSync(rcPath)) return {};
|
|
388
|
+
try {
|
|
389
|
+
const raw = readFileSync3(rcPath, "utf-8");
|
|
390
|
+
return JSON.parse(raw);
|
|
391
|
+
} catch {
|
|
392
|
+
return {};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
var CODE_EXTS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js"]);
|
|
396
|
+
var STYLE_EXTS = /* @__PURE__ */ new Set([".css", ".module.css"]);
|
|
397
|
+
var ALL_EXTS = /* @__PURE__ */ new Set([...CODE_EXTS, ...STYLE_EXTS]);
|
|
398
|
+
function collectFiles2(pathArg) {
|
|
399
|
+
const abs = resolve2(pathArg);
|
|
400
|
+
try {
|
|
401
|
+
const s = statSync2(abs);
|
|
402
|
+
if (s.isDirectory()) {
|
|
403
|
+
let recurse2 = function(dir) {
|
|
404
|
+
for (const entry of readdirSync2(dir)) {
|
|
405
|
+
if (entry.startsWith(".") || entry === "node_modules" || entry === "dist") continue;
|
|
406
|
+
const full = join3(dir, entry);
|
|
407
|
+
const es = statSync2(full);
|
|
408
|
+
if (es.isDirectory()) recurse2(full);
|
|
409
|
+
else if (ALL_EXTS.has(extname2(full))) files.push(full);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
var recurse = recurse2;
|
|
413
|
+
const files = [];
|
|
414
|
+
recurse2(abs);
|
|
415
|
+
return files;
|
|
416
|
+
}
|
|
417
|
+
if (ALL_EXTS.has(extname2(abs))) return [abs];
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
function lines(source) {
|
|
423
|
+
return source.split("\n");
|
|
424
|
+
}
|
|
425
|
+
function finding(file, line, rule, severity, message, fix) {
|
|
426
|
+
return { file, line, rule, severity, message, ...fix ? { fix } : {} };
|
|
427
|
+
}
|
|
428
|
+
var TIER1_PREFIXES = [
|
|
429
|
+
"--primitive-",
|
|
430
|
+
"--raw-",
|
|
431
|
+
"--base-color-",
|
|
432
|
+
"--palette-"
|
|
433
|
+
];
|
|
434
|
+
function ruleTier1TokenDirectUsage(source, filePath) {
|
|
435
|
+
const found = [];
|
|
436
|
+
const ext = extname2(filePath);
|
|
437
|
+
if (STYLE_EXTS.has(ext)) return found;
|
|
438
|
+
const src = lines(source);
|
|
439
|
+
for (let i = 0; i < src.length; i++) {
|
|
440
|
+
const l = src[i];
|
|
441
|
+
for (const prefix of TIER1_PREFIXES) {
|
|
442
|
+
if (l.includes(prefix)) {
|
|
443
|
+
found.push(finding(
|
|
444
|
+
filePath,
|
|
445
|
+
i + 1,
|
|
446
|
+
"tier-1-token-direct-usage",
|
|
447
|
+
"error",
|
|
448
|
+
`Direct use of Tier-1 primitive token "${prefix}..." detected. Use a Tier-2 semantic token instead.`,
|
|
449
|
+
"Replace with the equivalent semantic token from the Borealis token registry."
|
|
450
|
+
));
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return found;
|
|
456
|
+
}
|
|
457
|
+
function ruleHardcodedHex(source, filePath) {
|
|
458
|
+
const found = [];
|
|
459
|
+
const HEX_RE = /#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6,8})\b/g;
|
|
460
|
+
const src = lines(source);
|
|
461
|
+
for (let i = 0; i < src.length; i++) {
|
|
462
|
+
const l = src[i];
|
|
463
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*") || l.trim().startsWith("/*")) continue;
|
|
464
|
+
if (basename(filePath).startsWith(".")) continue;
|
|
465
|
+
let m;
|
|
466
|
+
HEX_RE.lastIndex = 0;
|
|
467
|
+
while ((m = HEX_RE.exec(l)) !== null) {
|
|
468
|
+
found.push(finding(
|
|
469
|
+
filePath,
|
|
470
|
+
i + 1,
|
|
471
|
+
"hardcoded-hex",
|
|
472
|
+
"error",
|
|
473
|
+
`Hardcoded hex color "${m[0]}" bypasses the Borealis token system.`,
|
|
474
|
+
"Replace with the appropriate semantic token: var(--color-surface), var(--color-text-primary), etc."
|
|
475
|
+
));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return found;
|
|
479
|
+
}
|
|
480
|
+
var PX_WHITELIST = /* @__PURE__ */ new Set(["0px", "1px", "2px", "3px"]);
|
|
481
|
+
function ruleHardcodedPx(source, filePath) {
|
|
482
|
+
const found = [];
|
|
483
|
+
const PX_RE = /\b(\d+(?:\.\d+)?)px\b/g;
|
|
484
|
+
const src = lines(source);
|
|
485
|
+
for (let i = 0; i < src.length; i++) {
|
|
486
|
+
const l = src[i];
|
|
487
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*") || l.trim().startsWith("/*")) continue;
|
|
488
|
+
if (!/margin|padding|width|height|gap|top:|left:|right:|bottom:|font-size|line-height|min-width|max-width|min-height|max-height/.test(l)) continue;
|
|
489
|
+
let m;
|
|
490
|
+
PX_RE.lastIndex = 0;
|
|
491
|
+
while ((m = PX_RE.exec(l)) !== null) {
|
|
492
|
+
const full = m[0];
|
|
493
|
+
if (PX_WHITELIST.has(full)) continue;
|
|
494
|
+
found.push(finding(
|
|
495
|
+
filePath,
|
|
496
|
+
i + 1,
|
|
497
|
+
"hardcoded-px",
|
|
498
|
+
"error",
|
|
499
|
+
`Hardcoded pixel value "${full}" in spacing/sizing bypasses the Borealis spacing token system.`,
|
|
500
|
+
"Replace with a semantic spacing token: var(--space-1), var(--space-2), var(--space-4), etc."
|
|
501
|
+
));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return found;
|
|
505
|
+
}
|
|
506
|
+
function ruleMissingDarkModeBlock(source, filePath) {
|
|
507
|
+
const ext = extname2(filePath);
|
|
508
|
+
if (!STYLE_EXTS.has(ext)) return [];
|
|
509
|
+
if (source.trim().length < 20) return [];
|
|
510
|
+
const hasDarkMediaQuery = /@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)/.test(source);
|
|
511
|
+
const hasDarkAttribute = /\[data-theme\s*=\s*["']?dark["']?\]/.test(source);
|
|
512
|
+
const hasDarkClass = /\.dark\b/.test(source);
|
|
513
|
+
if (!hasDarkMediaQuery && !hasDarkAttribute && !hasDarkClass) {
|
|
514
|
+
return [finding(
|
|
515
|
+
filePath,
|
|
516
|
+
1,
|
|
517
|
+
"missing-dark-mode-block",
|
|
518
|
+
"error",
|
|
519
|
+
"CSS file has no dark mode block. Borealis requires dark + light support from day one.",
|
|
520
|
+
'Add @media (prefers-color-scheme: dark) { ... } or [data-theme="dark"] { ... } with dark-mode token overrides.'
|
|
521
|
+
)];
|
|
522
|
+
}
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
function ruleMissingHoverTransition(source, filePath) {
|
|
526
|
+
const ext = extname2(filePath);
|
|
527
|
+
if (!STYLE_EXTS.has(ext)) return [];
|
|
528
|
+
const hasHover = /:hover/.test(source);
|
|
529
|
+
if (!hasHover) return [];
|
|
530
|
+
const hasTransition = /\btransition\b/.test(source);
|
|
531
|
+
if (!hasTransition) {
|
|
532
|
+
const hoverLine = source.split("\n").findIndex((l) => /:hover/.test(l));
|
|
533
|
+
return [finding(
|
|
534
|
+
filePath,
|
|
535
|
+
hoverLine + 1,
|
|
536
|
+
"missing-hover-transition",
|
|
537
|
+
"error",
|
|
538
|
+
":hover selector found but no transition property in this file. Borealis requires CSS transitions for hover states.",
|
|
539
|
+
"Add a transition property to the element's base styles, e.g. transition: color 150ms ease, background 150ms ease."
|
|
540
|
+
)];
|
|
541
|
+
}
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
function ruleDivAsInput(source, filePath) {
|
|
545
|
+
const ext = extname2(filePath);
|
|
546
|
+
if (!CODE_EXTS.has(ext)) return [];
|
|
547
|
+
const found = [];
|
|
548
|
+
const src = lines(source);
|
|
549
|
+
for (let i = 0; i < src.length; i++) {
|
|
550
|
+
const l = src[i];
|
|
551
|
+
if (/<div\b[^>]*onClick/.test(l) && !/role=|type=/.test(l)) {
|
|
552
|
+
found.push(finding(
|
|
553
|
+
filePath,
|
|
554
|
+
i + 1,
|
|
555
|
+
"div-as-input",
|
|
556
|
+
"error",
|
|
557
|
+
"<div onClick> used without role= \u2014 this is a div masquerading as an interactive element.",
|
|
558
|
+
'Use a <button> element or add role="button" and tabIndex={0} with keyboard handlers.'
|
|
559
|
+
));
|
|
560
|
+
}
|
|
561
|
+
if (/<div\b[^>]*onChange/.test(l)) {
|
|
562
|
+
found.push(finding(
|
|
563
|
+
filePath,
|
|
564
|
+
i + 1,
|
|
565
|
+
"div-as-input",
|
|
566
|
+
"error",
|
|
567
|
+
"<div onChange> detected. Real form elements only \u2014 no div-as-input.",
|
|
568
|
+
"Use <input>, <select>, or <textarea> with appropriate Visor wrapper components."
|
|
569
|
+
));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return found;
|
|
573
|
+
}
|
|
574
|
+
function ruleSetStateHover(source, filePath) {
|
|
575
|
+
const ext = extname2(filePath);
|
|
576
|
+
if (!CODE_EXTS.has(ext)) return [];
|
|
577
|
+
const found = [];
|
|
578
|
+
const src = lines(source);
|
|
579
|
+
for (let i = 0; i < src.length; i++) {
|
|
580
|
+
const l = src[i];
|
|
581
|
+
if (/onMouseEnter|onMouseLeave/.test(l) && /set[A-Z]/.test(l)) {
|
|
582
|
+
found.push(finding(
|
|
583
|
+
filePath,
|
|
584
|
+
i + 1,
|
|
585
|
+
"setstate-hover",
|
|
586
|
+
"error",
|
|
587
|
+
"onMouseEnter/onMouseLeave used to manage hover state via setState. Use CSS :hover instead.",
|
|
588
|
+
"Remove the mouse event handlers and hover state variable. Apply hover styles via CSS :hover."
|
|
589
|
+
));
|
|
590
|
+
}
|
|
591
|
+
if (/\buse[Ss]tate\b/.test(l) && /[Hh]overed|hover[Ss]tate|isHover/.test(l)) {
|
|
592
|
+
found.push(finding(
|
|
593
|
+
filePath,
|
|
594
|
+
i + 1,
|
|
595
|
+
"setstate-hover",
|
|
596
|
+
"error",
|
|
597
|
+
"useState used to track hover state. CSS :hover is zero-cost and more correct.",
|
|
598
|
+
"Delete this state variable and replace with CSS :hover selector."
|
|
599
|
+
));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return found;
|
|
603
|
+
}
|
|
604
|
+
function ruleMissingAriaPressed(source, filePath) {
|
|
605
|
+
const ext = extname2(filePath);
|
|
606
|
+
if (!CODE_EXTS.has(ext)) return [];
|
|
607
|
+
const found = [];
|
|
608
|
+
const src = lines(source);
|
|
609
|
+
for (let i = 0; i < src.length; i++) {
|
|
610
|
+
const l = src[i];
|
|
611
|
+
if (/<button\b[^>]*(isActive|isOpen|isSelected|isToggled|active=|selected=|toggled=)/.test(l) && !/aria-pressed/.test(l)) {
|
|
612
|
+
found.push(finding(
|
|
613
|
+
filePath,
|
|
614
|
+
i + 1,
|
|
615
|
+
"missing-aria-pressed",
|
|
616
|
+
"error",
|
|
617
|
+
"Toggle button appears to be missing aria-pressed. Toggleable buttons must expose their state to assistive technology.",
|
|
618
|
+
"Add aria-pressed={isActive} (or equivalent) to the button element."
|
|
619
|
+
));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return found;
|
|
623
|
+
}
|
|
624
|
+
var BANNED_FONT_LIST = ["Inter", "Roboto", "Arial", "system-ui", "'Arial'", '"Arial"', "'Roboto'", '"Roboto"', "'Inter'", '"Inter"'];
|
|
625
|
+
function ruleBannedFonts(source, filePath) {
|
|
626
|
+
const found = [];
|
|
627
|
+
const src = lines(source);
|
|
628
|
+
for (let i = 0; i < src.length; i++) {
|
|
629
|
+
const l = src[i];
|
|
630
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*")) continue;
|
|
631
|
+
for (const font of BANNED_FONT_LIST) {
|
|
632
|
+
if (l.includes(font)) {
|
|
633
|
+
found.push(finding(
|
|
634
|
+
filePath,
|
|
635
|
+
i + 1,
|
|
636
|
+
"banned-fonts",
|
|
637
|
+
"warn",
|
|
638
|
+
`Banned font "${font}" detected. Borealis projects use Satoshi (or the project's designated typeface).`,
|
|
639
|
+
"Remove this font reference and use the Borealis font stack via var(--font-sans)."
|
|
640
|
+
));
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return found;
|
|
646
|
+
}
|
|
647
|
+
function rulePurpleGradientOnWhite(source, filePath) {
|
|
648
|
+
const found = [];
|
|
649
|
+
const src = lines(source);
|
|
650
|
+
const PURPLE_RE = /gradient.*?(?:purple|violet|#[89abcde][0-9a-f]|#[6-9][0-9a-f]{5})/i;
|
|
651
|
+
for (let i = 0; i < src.length; i++) {
|
|
652
|
+
const l = src[i];
|
|
653
|
+
if (PURPLE_RE.test(l)) {
|
|
654
|
+
found.push(finding(
|
|
655
|
+
filePath,
|
|
656
|
+
i + 1,
|
|
657
|
+
"purple-gradient-on-white",
|
|
658
|
+
"warn",
|
|
659
|
+
"Purple gradient detected \u2014 this is a generic SaaS visual cliche.",
|
|
660
|
+
"Replace with a gradient using your project's actual brand tokens."
|
|
661
|
+
));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return found;
|
|
665
|
+
}
|
|
666
|
+
function rulePureBlackUntinted(source, filePath) {
|
|
667
|
+
const found = [];
|
|
668
|
+
const src = lines(source);
|
|
669
|
+
for (let i = 0; i < src.length; i++) {
|
|
670
|
+
const l = src[i];
|
|
671
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*")) continue;
|
|
672
|
+
if (/#000000\b|#000\b|rgb\(\s*0\s*,\s*0\s*,\s*0\s*\)/.test(l) || /:\s*black\b/.test(l) || /color:\s*["']?black["']?/.test(l)) {
|
|
673
|
+
found.push(finding(
|
|
674
|
+
filePath,
|
|
675
|
+
i + 1,
|
|
676
|
+
"pure-black-untinted",
|
|
677
|
+
"warn",
|
|
678
|
+
"Pure black (#000) detected. Use a near-black tinted token for softer, more intentional contrast.",
|
|
679
|
+
"Replace with var(--color-text-primary) or the project's near-black token."
|
|
680
|
+
));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return found;
|
|
684
|
+
}
|
|
685
|
+
function ruleBounceEasing(source, filePath) {
|
|
686
|
+
const found = [];
|
|
687
|
+
const src = lines(source);
|
|
688
|
+
const BOUNCE_RE = /cubic-bezier\s*\([^)]*(?:1\.[1-9]|[-]0\.\d+)[^)]*\)/;
|
|
689
|
+
const KEYWORD_RE = /\bease-in-back\b|\bease-out-back\b|\bease-in-out-back\b|\bbounce\b/;
|
|
690
|
+
for (let i = 0; i < src.length; i++) {
|
|
691
|
+
const l = src[i];
|
|
692
|
+
if (BOUNCE_RE.test(l) || KEYWORD_RE.test(l)) {
|
|
693
|
+
found.push(finding(
|
|
694
|
+
filePath,
|
|
695
|
+
i + 1,
|
|
696
|
+
"bounce-easing",
|
|
697
|
+
"warn",
|
|
698
|
+
"Bounce/overshoot easing detected. Bouncy transitions feel playful/cheap in most UI contexts.",
|
|
699
|
+
"Use ease, ease-out, or a subtle cubic-bezier like cubic-bezier(0.4, 0, 0.2, 1)."
|
|
700
|
+
));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return found;
|
|
704
|
+
}
|
|
705
|
+
function ruleSub44pxTouchTarget(source, filePath) {
|
|
706
|
+
const found = [];
|
|
707
|
+
const src = lines(source);
|
|
708
|
+
const SMALL_RE = /(?:width|height|min-width|min-height)\s*:\s*(?:[1-9]|[1-3][0-9]|4[0-3])px\b/;
|
|
709
|
+
const INTERACTIVE_SELECTOR_RE = /button|btn|icon|touch|tap/i;
|
|
710
|
+
let currentSelector = "";
|
|
711
|
+
for (let i = 0; i < src.length; i++) {
|
|
712
|
+
const l = src[i];
|
|
713
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*")) continue;
|
|
714
|
+
if (/\{/.test(l) && !l.trim().startsWith("@")) {
|
|
715
|
+
currentSelector = l;
|
|
716
|
+
}
|
|
717
|
+
const interactiveContext = INTERACTIVE_SELECTOR_RE.test(l) || INTERACTIVE_SELECTOR_RE.test(currentSelector);
|
|
718
|
+
if (SMALL_RE.test(l) && interactiveContext) {
|
|
719
|
+
found.push(finding(
|
|
720
|
+
filePath,
|
|
721
|
+
i + 1,
|
|
722
|
+
"sub-44px-touch-target",
|
|
723
|
+
"warn",
|
|
724
|
+
"Potential sub-44px touch target detected on an interactive element.",
|
|
725
|
+
"Ensure all interactive elements have a minimum 44x44px touch target (WCAG 2.5.5)."
|
|
726
|
+
));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return found;
|
|
730
|
+
}
|
|
731
|
+
function ruleLineLengthOver75ch(source, filePath) {
|
|
732
|
+
const found = [];
|
|
733
|
+
const src = lines(source);
|
|
734
|
+
const CH_RE = /max-width\s*:\s*(\d+)ch/;
|
|
735
|
+
for (let i = 0; i < src.length; i++) {
|
|
736
|
+
const l = src[i];
|
|
737
|
+
const m = CH_RE.exec(l);
|
|
738
|
+
if (m && parseInt(m[1], 10) > 75) {
|
|
739
|
+
found.push(finding(
|
|
740
|
+
filePath,
|
|
741
|
+
i + 1,
|
|
742
|
+
"line-length-over-75ch",
|
|
743
|
+
"warn",
|
|
744
|
+
`Text container max-width of ${m[1]}ch exceeds the 75ch readability limit.`,
|
|
745
|
+
"Cap text container max-width at 65-75ch for optimal reading comfort."
|
|
746
|
+
));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return found;
|
|
750
|
+
}
|
|
751
|
+
function ruleGradientText(source, filePath) {
|
|
752
|
+
const found = [];
|
|
753
|
+
const src = lines(source);
|
|
754
|
+
for (let i = 0; i < src.length; i++) {
|
|
755
|
+
const l = src[i];
|
|
756
|
+
if (/background-clip\s*:\s*text/.test(l) && /(?:transparent|-webkit-text-fill-color)/.test(source)) {
|
|
757
|
+
found.push(finding(
|
|
758
|
+
filePath,
|
|
759
|
+
i + 1,
|
|
760
|
+
"gradient-text",
|
|
761
|
+
"warn",
|
|
762
|
+
"Gradient text (background-clip: text) detected. Often illegible at small sizes.",
|
|
763
|
+
"Use a solid semantic text color token. Reserve gradient text for display/hero headings only."
|
|
764
|
+
));
|
|
765
|
+
}
|
|
766
|
+
if (/-webkit-text-fill-color\s*:\s*transparent/.test(l) && /gradient/.test(source)) {
|
|
767
|
+
found.push(finding(
|
|
768
|
+
filePath,
|
|
769
|
+
i + 1,
|
|
770
|
+
"gradient-text",
|
|
771
|
+
"warn",
|
|
772
|
+
"Gradient text via -webkit-text-fill-color: transparent detected.",
|
|
773
|
+
"Use a solid semantic text color token. Reserve gradient text for display/hero headings only."
|
|
774
|
+
));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const seen = /* @__PURE__ */ new Set();
|
|
778
|
+
return found.filter((f) => {
|
|
779
|
+
if (seen.has(f.line)) return false;
|
|
780
|
+
seen.add(f.line);
|
|
781
|
+
return true;
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function ruleExcessiveCardNesting(source, filePath) {
|
|
785
|
+
const ext = extname2(filePath);
|
|
786
|
+
if (!CODE_EXTS.has(ext)) return [];
|
|
787
|
+
const found = [];
|
|
788
|
+
const src = lines(source);
|
|
789
|
+
let depth = 0;
|
|
790
|
+
const CARD_OPEN_RE = /<(?:Card|Panel|Box|Surface|Tile|Widget)\b/;
|
|
791
|
+
const CARD_CLOSE_RE = /<\/(?:Card|Panel|Box|Surface|Tile|Widget)>/;
|
|
792
|
+
for (let i = 0; i < src.length; i++) {
|
|
793
|
+
const l = src[i];
|
|
794
|
+
if (CARD_OPEN_RE.test(l)) {
|
|
795
|
+
depth++;
|
|
796
|
+
if (depth >= 3) {
|
|
797
|
+
found.push(finding(
|
|
798
|
+
filePath,
|
|
799
|
+
i + 1,
|
|
800
|
+
"excessive-card-nesting",
|
|
801
|
+
"warn",
|
|
802
|
+
`Card/Panel nested ${depth} levels deep. Deep nesting creates visual noise and unclear hierarchy.`,
|
|
803
|
+
"Flatten the layout. Use spacing, dividers, or type scale to create hierarchy instead of nested containers."
|
|
804
|
+
));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (CARD_CLOSE_RE.test(l)) depth = Math.max(0, depth - 1);
|
|
808
|
+
}
|
|
809
|
+
return found;
|
|
810
|
+
}
|
|
811
|
+
var RULES = [
|
|
812
|
+
// Errors — Borealis non-negotiables
|
|
813
|
+
{ name: "tier-1-token-direct-usage", severity: "error", fn: ruleTier1TokenDirectUsage },
|
|
814
|
+
{ name: "hardcoded-hex", severity: "error", fn: ruleHardcodedHex },
|
|
815
|
+
{ name: "hardcoded-px", severity: "error", fn: ruleHardcodedPx },
|
|
816
|
+
{ name: "missing-dark-mode-block", severity: "error", fn: ruleMissingDarkModeBlock },
|
|
817
|
+
{ name: "missing-hover-transition", severity: "error", fn: ruleMissingHoverTransition },
|
|
818
|
+
{ name: "div-as-input", severity: "error", fn: ruleDivAsInput },
|
|
819
|
+
{ name: "setstate-hover", severity: "error", fn: ruleSetStateHover },
|
|
820
|
+
{ name: "missing-aria-pressed", severity: "error", fn: ruleMissingAriaPressed },
|
|
821
|
+
// Warns — general anti-patterns
|
|
822
|
+
{ name: "banned-fonts", severity: "warn", fn: ruleBannedFonts },
|
|
823
|
+
{ name: "purple-gradient-on-white", severity: "warn", fn: rulePurpleGradientOnWhite },
|
|
824
|
+
{ name: "pure-black-untinted", severity: "warn", fn: rulePureBlackUntinted },
|
|
825
|
+
{ name: "bounce-easing", severity: "warn", fn: ruleBounceEasing },
|
|
826
|
+
{ name: "sub-44px-touch-target", severity: "warn", fn: ruleSub44pxTouchTarget },
|
|
827
|
+
{ name: "line-length-over-75ch", severity: "warn", fn: ruleLineLengthOver75ch },
|
|
828
|
+
{ name: "gradient-text", severity: "warn", fn: ruleGradientText },
|
|
829
|
+
{ name: "excessive-card-nesting", severity: "warn", fn: ruleExcessiveCardNesting }
|
|
830
|
+
];
|
|
831
|
+
function scanDesign(pathArg, options = {}) {
|
|
832
|
+
const files = collectFiles2(pathArg);
|
|
833
|
+
const { disabledRules = [], errorsOnly = false } = options;
|
|
834
|
+
const activeRules = RULES.filter((r) => {
|
|
835
|
+
if (disabledRules.includes(r.name)) return false;
|
|
836
|
+
if (errorsOnly && r.severity !== "error") return false;
|
|
837
|
+
return true;
|
|
838
|
+
});
|
|
839
|
+
const errors = [];
|
|
840
|
+
const warnings = [];
|
|
841
|
+
for (const file of files) {
|
|
842
|
+
let source;
|
|
843
|
+
try {
|
|
844
|
+
source = readFileSync3(file, "utf-8");
|
|
845
|
+
} catch {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
for (const rule of activeRules) {
|
|
849
|
+
const ruleFindings = rule.fn(source, file);
|
|
850
|
+
for (const f of ruleFindings) {
|
|
851
|
+
if (f.severity === "error") errors.push(f);
|
|
852
|
+
else warnings.push(f);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
errors,
|
|
858
|
+
warnings,
|
|
859
|
+
summary: {
|
|
860
|
+
errorCount: errors.length,
|
|
861
|
+
warningCount: warnings.length,
|
|
862
|
+
filesScanned: files.length
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
380
867
|
// src/utils/logger.ts
|
|
381
868
|
import pc from "picocolors";
|
|
382
869
|
var logger = {
|
|
@@ -404,6 +891,7 @@ var logger = {
|
|
|
404
891
|
};
|
|
405
892
|
|
|
406
893
|
// src/commands/check.ts
|
|
894
|
+
import pc2 from "picocolors";
|
|
407
895
|
var TYPE_FILTER = {
|
|
408
896
|
ui: "component",
|
|
409
897
|
blocks: "block",
|
|
@@ -534,6 +1022,58 @@ async function checkDiffCommand(pathArg, options) {
|
|
|
534
1022
|
logger.blank();
|
|
535
1023
|
if (options.failOnHits) process.exit(1);
|
|
536
1024
|
}
|
|
1025
|
+
function checkDesignCommand(pathArg, options) {
|
|
1026
|
+
const absPath = resolve3(pathArg);
|
|
1027
|
+
const rcDir = statSync3(absPath).isDirectory() ? absPath : dirname2(absPath);
|
|
1028
|
+
const rc = loadVisorRc(rcDir);
|
|
1029
|
+
const result = scanDesign(absPath, {
|
|
1030
|
+
disabledRules: rc.disabledRules ?? [],
|
|
1031
|
+
errorsOnly: options.errorsOnly ?? false
|
|
1032
|
+
});
|
|
1033
|
+
const useJson = options.json || options.format === "json";
|
|
1034
|
+
if (useJson) {
|
|
1035
|
+
console.log(JSON.stringify({ success: true, ...result }, null, 2));
|
|
1036
|
+
if (!options.noFail && result.summary.errorCount > 0) process.exit(1);
|
|
1037
|
+
process.exit(0);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const { errors, warnings, summary } = result;
|
|
1041
|
+
if (summary.errorCount === 0 && summary.warningCount === 0) {
|
|
1042
|
+
logger.success(`No design anti-patterns found \u2014 ${summary.filesScanned} file(s) scanned.`);
|
|
1043
|
+
process.exit(0);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
logger.blank();
|
|
1047
|
+
logger.heading(`visor check design \u2014 ${summary.filesScanned} file(s) scanned`);
|
|
1048
|
+
logger.blank();
|
|
1049
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1050
|
+
const allFindings = [...errors, ...warnings];
|
|
1051
|
+
for (const f of allFindings) {
|
|
1052
|
+
if (!byFile.has(f.file)) byFile.set(f.file, []);
|
|
1053
|
+
byFile.get(f.file).push(f);
|
|
1054
|
+
}
|
|
1055
|
+
for (const [file, fileFindings] of byFile) {
|
|
1056
|
+
logger.heading(` ${file}`);
|
|
1057
|
+
for (const f of fileFindings) {
|
|
1058
|
+
const loc = pc2.dim(`${f.line}:`);
|
|
1059
|
+
const badge = f.severity === "error" ? pc2.red("error") : pc2.yellow("warn ");
|
|
1060
|
+
const ruleName = pc2.dim(`[${f.rule}]`);
|
|
1061
|
+
console.log(` ${loc} ${badge} ${f.message} ${ruleName}`);
|
|
1062
|
+
if (f.fix) {
|
|
1063
|
+
console.log(` ${pc2.dim("fix:")} ${pc2.cyan(f.fix)}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
logger.blank();
|
|
1067
|
+
}
|
|
1068
|
+
logger.blank();
|
|
1069
|
+
if (summary.errorCount > 0) {
|
|
1070
|
+
logger.error(`${summary.errorCount} error(s), ${summary.warningCount} warning(s)`);
|
|
1071
|
+
} else {
|
|
1072
|
+
logger.warn(`${summary.warningCount} warning(s) (0 errors)`);
|
|
1073
|
+
}
|
|
1074
|
+
logger.blank();
|
|
1075
|
+
if (!options.noFail && summary.errorCount > 0) process.exit(1);
|
|
1076
|
+
}
|
|
537
1077
|
function checkCommand() {
|
|
538
1078
|
const check = new Command("check").description("Check Visor catalog \u2014 list items, test existence, scan JSX for native HTML");
|
|
539
1079
|
check.command("list").description("List all catalog items (components, blocks, hooks, patterns)").option("--type <type>", "filter by type: ui, blocks, hooks, patterns, all (default: all)").option("--json", "output structured JSON (for AI agents)").action((options) => {
|
|
@@ -545,18 +1085,21 @@ function checkCommand() {
|
|
|
545
1085
|
check.command("diff").description("Scan JSX/TSX for native HTML elements that have Visor equivalents").argument("<path>", "file path, directory, or - for stdin").option("--fail-on-hits", "exit 1 if any native HTML usages are found (for CI use)").option("--json", "output structured JSON (for AI agents)").action(async (pathArg, options) => {
|
|
546
1086
|
await checkDiffCommand(pathArg, options);
|
|
547
1087
|
});
|
|
1088
|
+
check.command("design").description("Scan frontend code for Borealis design anti-patterns (deterministic, no LLM)").argument("<path>", "file path or directory to scan").option("--format <format>", "output format: json or human (default: human when TTY, json otherwise)").option("--errors-only", "report only error-severity rules (skip warnings)").option("--no-fail", "do not exit 1 on errors (advisory mode)").option("--json", "shorthand for --format json").action((pathArg, options) => {
|
|
1089
|
+
checkDesignCommand(pathArg, options);
|
|
1090
|
+
});
|
|
548
1091
|
return check;
|
|
549
1092
|
}
|
|
550
1093
|
|
|
551
1094
|
// src/commands/init.ts
|
|
552
|
-
import { existsSync as
|
|
553
|
-
import { join as
|
|
1095
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync, readFileSync as readFileSync6 } from "fs";
|
|
1096
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
554
1097
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
555
1098
|
import * as childProcess from "child_process";
|
|
556
1099
|
|
|
557
1100
|
// src/config/config.ts
|
|
558
|
-
import { readFileSync as
|
|
559
|
-
import { join as
|
|
1101
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
1102
|
+
import { join as join4 } from "path";
|
|
560
1103
|
|
|
561
1104
|
// src/config/defaults.ts
|
|
562
1105
|
var DEFAULT_CONFIG = {
|
|
@@ -573,19 +1116,19 @@ var CONFIG_FILE = "visor.json";
|
|
|
573
1116
|
|
|
574
1117
|
// src/config/config.ts
|
|
575
1118
|
function getConfigPath(cwd) {
|
|
576
|
-
return
|
|
1119
|
+
return join4(cwd, CONFIG_FILE);
|
|
577
1120
|
}
|
|
578
1121
|
function configExists(cwd) {
|
|
579
|
-
return
|
|
1122
|
+
return existsSync2(getConfigPath(cwd));
|
|
580
1123
|
}
|
|
581
1124
|
function loadConfig(cwd) {
|
|
582
1125
|
const configPath = getConfigPath(cwd);
|
|
583
|
-
if (!
|
|
1126
|
+
if (!existsSync2(configPath)) {
|
|
584
1127
|
throw new Error(
|
|
585
1128
|
`No ${CONFIG_FILE} found. Run "visor init" first.`
|
|
586
1129
|
);
|
|
587
1130
|
}
|
|
588
|
-
const raw =
|
|
1131
|
+
const raw = readFileSync4(configPath, "utf-8");
|
|
589
1132
|
const parsed = JSON.parse(raw);
|
|
590
1133
|
const knownKeys = /* @__PURE__ */ new Set(["paths"]);
|
|
591
1134
|
for (const key of Object.keys(parsed)) {
|
|
@@ -622,12 +1165,12 @@ function writeConfig(cwd, config) {
|
|
|
622
1165
|
|
|
623
1166
|
// src/utils/packages.ts
|
|
624
1167
|
import { execFileSync } from "child_process";
|
|
625
|
-
import { existsSync as
|
|
626
|
-
import { join as
|
|
1168
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
1169
|
+
import { join as join5 } from "path";
|
|
627
1170
|
function readPackageJson(cwd) {
|
|
628
|
-
const pkgPath =
|
|
629
|
-
if (!
|
|
630
|
-
return JSON.parse(
|
|
1171
|
+
const pkgPath = join5(cwd, "package.json");
|
|
1172
|
+
if (!existsSync3(pkgPath)) return null;
|
|
1173
|
+
return JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
631
1174
|
}
|
|
632
1175
|
function isPackageInstalled(packageName, cwd) {
|
|
633
1176
|
const pkg = readPackageJson(cwd);
|
|
@@ -704,7 +1247,7 @@ function initCommand(cwd, options) {
|
|
|
704
1247
|
emitError(json, `Unknown template: ${options.template}. Available templates: nextjs`);
|
|
705
1248
|
process.exit(1);
|
|
706
1249
|
}
|
|
707
|
-
if (options?.template === "nextjs" &&
|
|
1250
|
+
if (options?.template === "nextjs" && existsSync4(join6(cwd, "package.json"))) {
|
|
708
1251
|
emitError(
|
|
709
1252
|
json,
|
|
710
1253
|
"package.json already exists in this directory. visor init --template nextjs only scaffolds into empty directories. For an existing app, see the retrofit flow: https://visor.loworbit.studio/docs/guides/migration"
|
|
@@ -787,8 +1330,8 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
|
|
|
787
1330
|
}
|
|
788
1331
|
runCreateNextApp(cwd, json);
|
|
789
1332
|
runInstallVisorDeps(cwd, json);
|
|
790
|
-
const yamlPath =
|
|
791
|
-
if (
|
|
1333
|
+
const yamlPath = join6(cwd, ".visor.yaml");
|
|
1334
|
+
if (existsSync4(yamlPath)) {
|
|
792
1335
|
filesSkipped.push(".visor.yaml");
|
|
793
1336
|
if (!json) {
|
|
794
1337
|
logger.warn(".visor.yaml already exists. Skipping.");
|
|
@@ -806,10 +1349,10 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
|
|
|
806
1349
|
tokens: data.tokens,
|
|
807
1350
|
config: data.config
|
|
808
1351
|
});
|
|
809
|
-
const globalsPath =
|
|
810
|
-
const globalsDir =
|
|
1352
|
+
const globalsPath = join6(cwd, "app", "globals.css");
|
|
1353
|
+
const globalsDir = dirname3(globalsPath);
|
|
811
1354
|
mkdirSync(globalsDir, { recursive: true });
|
|
812
|
-
if (
|
|
1355
|
+
if (existsSync4(globalsPath)) {
|
|
813
1356
|
writeFileSync2(globalsPath, css, "utf-8");
|
|
814
1357
|
filesCreated.push("app/globals.css");
|
|
815
1358
|
} else {
|
|
@@ -819,15 +1362,15 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
|
|
|
819
1362
|
if (!json) {
|
|
820
1363
|
logger.success("Created app/globals.css with theme tokens");
|
|
821
1364
|
}
|
|
822
|
-
const layoutPath =
|
|
1365
|
+
const layoutPath = join6(cwd, "app", "layout.tsx");
|
|
823
1366
|
writeFileSync2(layoutPath, generateNextjsLayout(), "utf-8");
|
|
824
1367
|
filesCreated.push("app/layout.tsx");
|
|
825
1368
|
if (!json) {
|
|
826
1369
|
logger.success("Wired app/layout.tsx with FOWT prevention and theme tokens");
|
|
827
1370
|
}
|
|
828
|
-
const stampDir =
|
|
829
|
-
const stampPath =
|
|
830
|
-
if (
|
|
1371
|
+
const stampDir = join6(cwd, ".lo");
|
|
1372
|
+
const stampPath = join6(stampDir, "borealis.json");
|
|
1373
|
+
if (existsSync4(stampPath)) {
|
|
831
1374
|
filesSkipped.push(".lo/borealis.json");
|
|
832
1375
|
if (!json) {
|
|
833
1376
|
logger.warn(".lo/borealis.json already exists. Skipping.");
|
|
@@ -891,12 +1434,12 @@ function assertSpawnSuccess(result, label) {
|
|
|
891
1434
|
}
|
|
892
1435
|
function readVisorCliVersion() {
|
|
893
1436
|
try {
|
|
894
|
-
const here =
|
|
1437
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
895
1438
|
for (let i = 0; i < 5; i++) {
|
|
896
1439
|
const segments = new Array(i).fill("..");
|
|
897
|
-
const candidate =
|
|
1440
|
+
const candidate = join6(here, ...segments, "package.json");
|
|
898
1441
|
try {
|
|
899
|
-
const pkg = JSON.parse(
|
|
1442
|
+
const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
|
|
900
1443
|
if (pkg.name === "@loworbitstudio/visor" && pkg.version) {
|
|
901
1444
|
return pkg.version;
|
|
902
1445
|
}
|
|
@@ -911,52 +1454,52 @@ function readVisorCliVersion() {
|
|
|
911
1454
|
// src/utils/fs.ts
|
|
912
1455
|
import {
|
|
913
1456
|
writeFileSync as writeFileSync3,
|
|
914
|
-
readFileSync as
|
|
915
|
-
existsSync as
|
|
1457
|
+
readFileSync as readFileSync7,
|
|
1458
|
+
existsSync as existsSync5,
|
|
916
1459
|
mkdirSync as mkdirSync2
|
|
917
1460
|
} from "fs";
|
|
918
|
-
import { dirname as
|
|
1461
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
919
1462
|
function resolveOutputPath(registryPath, type, config, cwd) {
|
|
920
1463
|
let relativePath;
|
|
921
1464
|
if (type === "registry:block") {
|
|
922
1465
|
relativePath = registryPath.replace(/^blocks\//, "");
|
|
923
|
-
return
|
|
1466
|
+
return join7(cwd, config.paths.blocks, relativePath);
|
|
924
1467
|
}
|
|
925
1468
|
if (type === "registry:ui") {
|
|
926
1469
|
if (registryPath.startsWith("components/deck/")) {
|
|
927
1470
|
relativePath = registryPath.replace(/^components\/deck\//, "");
|
|
928
|
-
return
|
|
1471
|
+
return join7(cwd, config.paths.deckComponents, relativePath);
|
|
929
1472
|
}
|
|
930
1473
|
if (registryPath.startsWith("components/flutter/")) {
|
|
931
1474
|
relativePath = registryPath.replace(/^components\/flutter\//, "");
|
|
932
|
-
return
|
|
1475
|
+
return join7(cwd, config.paths.flutterComponents, relativePath);
|
|
933
1476
|
}
|
|
934
1477
|
relativePath = registryPath.replace(/^components\/ui\//, "");
|
|
935
|
-
return
|
|
1478
|
+
return join7(cwd, config.paths.components, relativePath);
|
|
936
1479
|
}
|
|
937
1480
|
if (type === "registry:hook") {
|
|
938
1481
|
relativePath = registryPath.replace(/^hooks\//, "");
|
|
939
|
-
return
|
|
1482
|
+
return join7(cwd, config.paths.hooks, relativePath);
|
|
940
1483
|
}
|
|
941
1484
|
if (type === "registry:lib") {
|
|
942
1485
|
relativePath = registryPath.replace(/^lib\//, "");
|
|
943
|
-
return
|
|
1486
|
+
return join7(cwd, config.paths.lib, relativePath);
|
|
944
1487
|
}
|
|
945
|
-
return
|
|
1488
|
+
return join7(cwd, registryPath);
|
|
946
1489
|
}
|
|
947
1490
|
function writeFile(filePath, content) {
|
|
948
|
-
const dir =
|
|
949
|
-
if (!
|
|
1491
|
+
const dir = dirname4(filePath);
|
|
1492
|
+
if (!existsSync5(dir)) {
|
|
950
1493
|
mkdirSync2(dir, { recursive: true });
|
|
951
1494
|
}
|
|
952
1495
|
writeFileSync3(filePath, content, "utf-8");
|
|
953
1496
|
}
|
|
954
1497
|
function readFile(filePath) {
|
|
955
|
-
if (!
|
|
956
|
-
return
|
|
1498
|
+
if (!existsSync5(filePath)) return null;
|
|
1499
|
+
return readFileSync7(filePath, "utf-8");
|
|
957
1500
|
}
|
|
958
1501
|
function fileExists(filePath) {
|
|
959
|
-
return
|
|
1502
|
+
return existsSync5(filePath);
|
|
960
1503
|
}
|
|
961
1504
|
|
|
962
1505
|
// src/commands/list.ts
|
|
@@ -1097,8 +1640,8 @@ function listCommand(cwd, options = {}) {
|
|
|
1097
1640
|
}
|
|
1098
1641
|
|
|
1099
1642
|
// src/utils/pubspec.ts
|
|
1100
|
-
import { existsSync as
|
|
1101
|
-
import { join as
|
|
1643
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
1644
|
+
import { join as join8 } from "path";
|
|
1102
1645
|
import { parseDocument, YAMLMap } from "yaml";
|
|
1103
1646
|
function mergePubspec(pubspecText, deps) {
|
|
1104
1647
|
const doc = parseDocument(pubspecText);
|
|
@@ -1120,14 +1663,14 @@ function mergePubspec(pubspecText, deps) {
|
|
|
1120
1663
|
return { text: doc.toString(), added, skipped };
|
|
1121
1664
|
}
|
|
1122
1665
|
function pubspecPath(cwd) {
|
|
1123
|
-
return
|
|
1666
|
+
return join8(cwd, "pubspec.yaml");
|
|
1124
1667
|
}
|
|
1125
1668
|
function pubspecExists(cwd) {
|
|
1126
|
-
return
|
|
1669
|
+
return existsSync6(pubspecPath(cwd));
|
|
1127
1670
|
}
|
|
1128
1671
|
function isPubPackageInstalled(packageName, cwd) {
|
|
1129
1672
|
if (!pubspecExists(cwd)) return false;
|
|
1130
|
-
const text =
|
|
1673
|
+
const text = readFileSync8(pubspecPath(cwd), "utf-8");
|
|
1131
1674
|
const doc = parseDocument(text);
|
|
1132
1675
|
const depsNode = doc.get("dependencies");
|
|
1133
1676
|
if (!(depsNode instanceof YAMLMap)) return false;
|
|
@@ -1138,12 +1681,12 @@ function getUninstalledPubDeps(deps, cwd) {
|
|
|
1138
1681
|
}
|
|
1139
1682
|
function addPubDependencies(deps, cwd) {
|
|
1140
1683
|
const path2 = pubspecPath(cwd);
|
|
1141
|
-
if (!
|
|
1684
|
+
if (!existsSync6(path2)) {
|
|
1142
1685
|
throw new Error(
|
|
1143
1686
|
`No pubspec.yaml found at ${path2}. Run this command from a Flutter project root.`
|
|
1144
1687
|
);
|
|
1145
1688
|
}
|
|
1146
|
-
const text =
|
|
1689
|
+
const text = readFileSync8(path2, "utf-8");
|
|
1147
1690
|
const result = mergePubspec(text, deps);
|
|
1148
1691
|
if (result.added.length > 0) {
|
|
1149
1692
|
writeFileSync4(path2, result.text, "utf-8");
|
|
@@ -1153,12 +1696,12 @@ function addPubDependencies(deps, cwd) {
|
|
|
1153
1696
|
|
|
1154
1697
|
// src/utils/flutter.ts
|
|
1155
1698
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1156
|
-
import { existsSync as
|
|
1699
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
1157
1700
|
import { homedir } from "os";
|
|
1158
|
-
import { join as
|
|
1701
|
+
import { join as join9 } from "path";
|
|
1159
1702
|
function isExecutable(path2) {
|
|
1160
1703
|
try {
|
|
1161
|
-
const s =
|
|
1704
|
+
const s = statSync4(path2);
|
|
1162
1705
|
return s.isFile();
|
|
1163
1706
|
} catch {
|
|
1164
1707
|
return false;
|
|
@@ -1166,24 +1709,24 @@ function isExecutable(path2) {
|
|
|
1166
1709
|
}
|
|
1167
1710
|
function fromPath(env) {
|
|
1168
1711
|
const pathVar = env.PATH ?? "";
|
|
1169
|
-
const
|
|
1712
|
+
const sep2 = process.platform === "win32" ? ";" : ":";
|
|
1170
1713
|
const bin = process.platform === "win32" ? "flutter.bat" : "flutter";
|
|
1171
|
-
for (const dir of pathVar.split(
|
|
1714
|
+
for (const dir of pathVar.split(sep2)) {
|
|
1172
1715
|
if (!dir) continue;
|
|
1173
|
-
const candidate =
|
|
1716
|
+
const candidate = join9(dir, bin);
|
|
1174
1717
|
if (isExecutable(candidate)) return candidate;
|
|
1175
1718
|
}
|
|
1176
1719
|
return null;
|
|
1177
1720
|
}
|
|
1178
1721
|
function fromFvm(home) {
|
|
1179
|
-
const fvmDefault =
|
|
1722
|
+
const fvmDefault = join9(home, "fvm", "default", "bin", "flutter");
|
|
1180
1723
|
if (isExecutable(fvmDefault)) return fvmDefault;
|
|
1181
|
-
const versionsDir =
|
|
1182
|
-
if (!
|
|
1724
|
+
const versionsDir = join9(home, "fvm", "versions");
|
|
1725
|
+
if (!existsSync7(versionsDir)) return null;
|
|
1183
1726
|
let best = null;
|
|
1184
1727
|
try {
|
|
1185
|
-
for (const name of
|
|
1186
|
-
const candidate =
|
|
1728
|
+
for (const name of readdirSync3(versionsDir)) {
|
|
1729
|
+
const candidate = join9(versionsDir, name, "bin", "flutter");
|
|
1187
1730
|
if (!isExecutable(candidate)) continue;
|
|
1188
1731
|
if (!best || compareSemver(name, best.version) > 0) {
|
|
1189
1732
|
best = { version: name, path: candidate };
|
|
@@ -1209,7 +1752,7 @@ function findFlutterBin(options = {}) {
|
|
|
1209
1752
|
const home = options.home ?? homedir();
|
|
1210
1753
|
const envRoot = env.FLUTTER_ROOT;
|
|
1211
1754
|
if (envRoot) {
|
|
1212
|
-
const bin =
|
|
1755
|
+
const bin = join9(envRoot, "bin", "flutter");
|
|
1213
1756
|
if (isExecutable(bin)) return bin;
|
|
1214
1757
|
}
|
|
1215
1758
|
const fromPathBin = fromPath(env);
|
|
@@ -1852,8 +2395,8 @@ function infoCommand(name, cwd, options = {}) {
|
|
|
1852
2395
|
}
|
|
1853
2396
|
|
|
1854
2397
|
// src/commands/theme-apply.ts
|
|
1855
|
-
import { readFileSync as
|
|
1856
|
-
import { resolve as
|
|
2398
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
2399
|
+
import { resolve as resolve4, dirname as dirname5, join as join10 } from "path";
|
|
1857
2400
|
import { generateTheme, generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
|
|
1858
2401
|
import {
|
|
1859
2402
|
nextjsAdapter as nextjsAdapter2,
|
|
@@ -1883,10 +2426,10 @@ function defaultOutputPath(adapter, themeName) {
|
|
|
1883
2426
|
}
|
|
1884
2427
|
}
|
|
1885
2428
|
function themeApplyCommand(file, cwd, options) {
|
|
1886
|
-
const filePath =
|
|
2429
|
+
const filePath = resolve4(cwd, file);
|
|
1887
2430
|
let yamlContent;
|
|
1888
2431
|
try {
|
|
1889
|
-
yamlContent =
|
|
2432
|
+
yamlContent = readFileSync9(filePath, "utf-8");
|
|
1890
2433
|
} catch {
|
|
1891
2434
|
if (options.json) {
|
|
1892
2435
|
console.log(
|
|
@@ -1963,14 +2506,14 @@ function themeApplyCommand(file, cwd, options) {
|
|
|
1963
2506
|
process.exit(1);
|
|
1964
2507
|
}
|
|
1965
2508
|
const outputTarget = options.output ?? defaultOutputPath(options.adapter, themeName);
|
|
1966
|
-
const outputPath =
|
|
2509
|
+
const outputPath = resolve4(cwd, outputTarget);
|
|
1967
2510
|
if (fileMap) {
|
|
1968
2511
|
try {
|
|
1969
2512
|
mkdirSync3(outputPath, { recursive: true });
|
|
1970
2513
|
let totalBytes = 0;
|
|
1971
2514
|
for (const [relPath, content] of Object.entries(fileMap.files)) {
|
|
1972
|
-
const filePath2 =
|
|
1973
|
-
mkdirSync3(
|
|
2515
|
+
const filePath2 = join10(outputPath, relPath);
|
|
2516
|
+
mkdirSync3(dirname5(filePath2), { recursive: true });
|
|
1974
2517
|
writeFileSync5(filePath2, content, "utf-8");
|
|
1975
2518
|
totalBytes += content.length;
|
|
1976
2519
|
}
|
|
@@ -2008,7 +2551,7 @@ function themeApplyCommand(file, cwd, options) {
|
|
|
2008
2551
|
if (css === null) {
|
|
2009
2552
|
process.exit(1);
|
|
2010
2553
|
}
|
|
2011
|
-
const outputDir =
|
|
2554
|
+
const outputDir = dirname5(outputPath);
|
|
2012
2555
|
try {
|
|
2013
2556
|
mkdirSync3(outputDir, { recursive: true });
|
|
2014
2557
|
writeFileSync5(outputPath, css, "utf-8");
|
|
@@ -2052,8 +2595,8 @@ function formatSize(bytes) {
|
|
|
2052
2595
|
}
|
|
2053
2596
|
|
|
2054
2597
|
// src/commands/theme-export.ts
|
|
2055
|
-
import { readFileSync as
|
|
2056
|
-
import { resolve as
|
|
2598
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2599
|
+
import { resolve as resolve5 } from "path";
|
|
2057
2600
|
import {
|
|
2058
2601
|
parseConfig,
|
|
2059
2602
|
resolveConfig,
|
|
@@ -2061,10 +2604,10 @@ import {
|
|
|
2061
2604
|
exportTheme
|
|
2062
2605
|
} from "@loworbitstudio/visor-theme-engine";
|
|
2063
2606
|
function themeExportCommand(file, cwd, options) {
|
|
2064
|
-
const filePath =
|
|
2607
|
+
const filePath = resolve5(cwd, file ?? ".visor.yaml");
|
|
2065
2608
|
let yamlContent;
|
|
2066
2609
|
try {
|
|
2067
|
-
yamlContent =
|
|
2610
|
+
yamlContent = readFileSync10(filePath, "utf-8");
|
|
2068
2611
|
} catch {
|
|
2069
2612
|
if (options.json) {
|
|
2070
2613
|
console.log(
|
|
@@ -2116,16 +2659,16 @@ function themeExportCommand(file, cwd, options) {
|
|
|
2116
2659
|
}
|
|
2117
2660
|
|
|
2118
2661
|
// src/commands/theme-validate.ts
|
|
2119
|
-
import { readFileSync as
|
|
2120
|
-
import { resolve as
|
|
2662
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
2663
|
+
import { resolve as resolve6 } from "path";
|
|
2121
2664
|
import { parse as parseYaml } from "yaml";
|
|
2122
2665
|
import { validate } from "@loworbitstudio/visor-theme-engine";
|
|
2123
|
-
import
|
|
2666
|
+
import pc3 from "picocolors";
|
|
2124
2667
|
function themeValidateCommand(file, cwd, options) {
|
|
2125
|
-
const filePath =
|
|
2668
|
+
const filePath = resolve6(cwd, file);
|
|
2126
2669
|
let fileContent;
|
|
2127
2670
|
try {
|
|
2128
|
-
fileContent =
|
|
2671
|
+
fileContent = readFileSync11(filePath, "utf-8");
|
|
2129
2672
|
} catch {
|
|
2130
2673
|
if (options.json) {
|
|
2131
2674
|
console.log(
|
|
@@ -2205,16 +2748,16 @@ function themeValidateCommand(file, cwd, options) {
|
|
|
2205
2748
|
}
|
|
2206
2749
|
}
|
|
2207
2750
|
function printIssue(issue) {
|
|
2208
|
-
const prefix = issue.severity === "error" ?
|
|
2209
|
-
const code =
|
|
2210
|
-
const path2 = issue.path ?
|
|
2751
|
+
const prefix = issue.severity === "error" ? pc3.red(" ERROR") : pc3.yellow(" WARN ");
|
|
2752
|
+
const code = pc3.dim(`[${issue.code}]`);
|
|
2753
|
+
const path2 = issue.path ? pc3.dim(` (${issue.path})`) : "";
|
|
2211
2754
|
console.log(`${prefix} ${code} ${issue.message}${path2}`);
|
|
2212
2755
|
}
|
|
2213
2756
|
|
|
2214
2757
|
// src/commands/theme-verify.ts
|
|
2215
2758
|
import { spawnSync as _spawnSync } from "child_process";
|
|
2216
|
-
import { existsSync as
|
|
2217
|
-
import { resolve as
|
|
2759
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2760
|
+
import { resolve as resolve7 } from "path";
|
|
2218
2761
|
function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
|
|
2219
2762
|
const target = options.target ?? "flutter";
|
|
2220
2763
|
if (target !== "flutter") {
|
|
@@ -2236,8 +2779,8 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
|
|
|
2236
2779
|
}
|
|
2237
2780
|
process.exit(1);
|
|
2238
2781
|
}
|
|
2239
|
-
const dirPath =
|
|
2240
|
-
if (!
|
|
2782
|
+
const dirPath = resolve7(cwd, dir);
|
|
2783
|
+
if (!existsSync8(dirPath)) {
|
|
2241
2784
|
if (options.json) {
|
|
2242
2785
|
console.log(
|
|
2243
2786
|
JSON.stringify({
|
|
@@ -2324,8 +2867,8 @@ function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
|
|
|
2324
2867
|
}
|
|
2325
2868
|
|
|
2326
2869
|
// src/commands/theme-extract.ts
|
|
2327
|
-
import { readFileSync as
|
|
2328
|
-
import { resolve as
|
|
2870
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
2871
|
+
import { resolve as resolve8, join as join11, basename as basename2, extname as extname3, relative } from "path";
|
|
2329
2872
|
import { stringify as stringifyYaml } from "yaml";
|
|
2330
2873
|
import {
|
|
2331
2874
|
extractFromCSS,
|
|
@@ -2354,8 +2897,8 @@ var CSS_DIRS = [
|
|
|
2354
2897
|
"packages/design-tokens"
|
|
2355
2898
|
];
|
|
2356
2899
|
function themeExtractCommand(cwd, options) {
|
|
2357
|
-
const targetDir =
|
|
2358
|
-
if (!
|
|
2900
|
+
const targetDir = resolve8(cwd, options.from ?? ".");
|
|
2901
|
+
if (!existsSync9(targetDir)) {
|
|
2359
2902
|
if (options.json) {
|
|
2360
2903
|
console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
|
|
2361
2904
|
} else {
|
|
@@ -2409,16 +2952,16 @@ function collectCSSFiles(targetDir) {
|
|
|
2409
2952
|
const files = [];
|
|
2410
2953
|
const seen = /* @__PURE__ */ new Set();
|
|
2411
2954
|
for (const pattern2 of CSS_FILE_PATTERNS) {
|
|
2412
|
-
const rootPath =
|
|
2955
|
+
const rootPath = join11(targetDir, pattern2);
|
|
2413
2956
|
addFileIfExists(rootPath, files, seen);
|
|
2414
2957
|
for (const dir of CSS_DIRS) {
|
|
2415
|
-
const dirPath =
|
|
2958
|
+
const dirPath = join11(targetDir, dir, pattern2);
|
|
2416
2959
|
addFileIfExists(dirPath, files, seen);
|
|
2417
2960
|
}
|
|
2418
2961
|
}
|
|
2419
2962
|
for (const dir of CSS_DIRS) {
|
|
2420
|
-
const dirPath =
|
|
2421
|
-
if (
|
|
2963
|
+
const dirPath = join11(targetDir, dir);
|
|
2964
|
+
if (existsSync9(dirPath) && statSync5(dirPath).isDirectory()) {
|
|
2422
2965
|
scanDirForCSS(dirPath, files, seen, 2);
|
|
2423
2966
|
}
|
|
2424
2967
|
}
|
|
@@ -2426,11 +2969,11 @@ function collectCSSFiles(targetDir) {
|
|
|
2426
2969
|
return files;
|
|
2427
2970
|
}
|
|
2428
2971
|
function addFileIfExists(filePath, files, seen) {
|
|
2429
|
-
const resolved =
|
|
2972
|
+
const resolved = resolve8(filePath);
|
|
2430
2973
|
if (seen.has(resolved)) return;
|
|
2431
|
-
if (!
|
|
2974
|
+
if (!existsSync9(resolved)) return;
|
|
2432
2975
|
try {
|
|
2433
|
-
const content =
|
|
2976
|
+
const content = readFileSync12(resolved, "utf-8");
|
|
2434
2977
|
if (content.includes("--")) {
|
|
2435
2978
|
files.push({ path: resolved, content });
|
|
2436
2979
|
seen.add(resolved);
|
|
@@ -2439,7 +2982,7 @@ function addFileIfExists(filePath, files, seen) {
|
|
|
2439
2982
|
}
|
|
2440
2983
|
}
|
|
2441
2984
|
function scanDirForCSS(dir, files, seen, maxDepth) {
|
|
2442
|
-
if (!
|
|
2985
|
+
if (!existsSync9(dir)) return;
|
|
2443
2986
|
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2444
2987
|
"node_modules",
|
|
2445
2988
|
".next",
|
|
@@ -2453,15 +2996,15 @@ function scanDirForCSS(dir, files, seen, maxDepth) {
|
|
|
2453
2996
|
".vercel"
|
|
2454
2997
|
]);
|
|
2455
2998
|
try {
|
|
2456
|
-
const entries =
|
|
2999
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
2457
3000
|
for (const entry of entries) {
|
|
2458
3001
|
if (entry.isDirectory()) {
|
|
2459
3002
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
2460
3003
|
if (maxDepth > 0) {
|
|
2461
|
-
scanDirForCSS(
|
|
3004
|
+
scanDirForCSS(join11(dir, entry.name), files, seen, maxDepth - 1);
|
|
2462
3005
|
}
|
|
2463
|
-
} else if (entry.isFile() &&
|
|
2464
|
-
addFileIfExists(
|
|
3006
|
+
} else if (entry.isFile() && extname3(entry.name) === ".css") {
|
|
3007
|
+
addFileIfExists(join11(dir, entry.name), files, seen);
|
|
2465
3008
|
}
|
|
2466
3009
|
}
|
|
2467
3010
|
} catch {
|
|
@@ -2543,10 +3086,10 @@ function extractVarName(varExpr) {
|
|
|
2543
3086
|
function parseNextFontFromLayouts(targetDir) {
|
|
2544
3087
|
const fontMap = /* @__PURE__ */ new Map();
|
|
2545
3088
|
for (const relPath of LAYOUT_FILE_PATHS) {
|
|
2546
|
-
const fullPath =
|
|
2547
|
-
if (!
|
|
3089
|
+
const fullPath = join11(targetDir, relPath);
|
|
3090
|
+
if (!existsSync9(fullPath)) continue;
|
|
2548
3091
|
try {
|
|
2549
|
-
const content =
|
|
3092
|
+
const content = readFileSync12(fullPath, "utf-8");
|
|
2550
3093
|
parseNextFontDeclarations(content, fontMap);
|
|
2551
3094
|
} catch {
|
|
2552
3095
|
}
|
|
@@ -2593,7 +3136,7 @@ function parseNextFontDeclarations(content, fontMap) {
|
|
|
2593
3136
|
const srcMatch = block.match(/src\s*:\s*["']([^"']+)["']/);
|
|
2594
3137
|
if (srcMatch) {
|
|
2595
3138
|
const srcPath = srcMatch[1];
|
|
2596
|
-
const fileName =
|
|
3139
|
+
const fileName = basename2(srcPath, extname3(srcPath));
|
|
2597
3140
|
const fontBaseName = fileName.replace(/[-_](Variable|Regular|Bold|Light|Medium|SemiBold|ExtraBold|Thin|Black|Italic).*$/i, "").replace(/[-_]/g, " ").trim();
|
|
2598
3141
|
if (fontBaseName) {
|
|
2599
3142
|
fontMap.set(varName, fontBaseName);
|
|
@@ -2631,10 +3174,10 @@ var MONO_FONT_NAMES = /* @__PURE__ */ new Set([
|
|
|
2631
3174
|
"IBM Plex Mono"
|
|
2632
3175
|
]);
|
|
2633
3176
|
function extractFontHints(targetDir) {
|
|
2634
|
-
const pkgPath =
|
|
2635
|
-
if (!
|
|
3177
|
+
const pkgPath = join11(targetDir, "package.json");
|
|
3178
|
+
if (!existsSync9(pkgPath)) return void 0;
|
|
2636
3179
|
try {
|
|
2637
|
-
const pkg = JSON.parse(
|
|
3180
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
2638
3181
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2639
3182
|
const fonts2 = [];
|
|
2640
3183
|
for (const [dep, _] of Object.entries(allDeps)) {
|
|
@@ -2670,10 +3213,10 @@ function extractFontHints(targetDir) {
|
|
|
2670
3213
|
}
|
|
2671
3214
|
}
|
|
2672
3215
|
function inferThemeName(targetDir) {
|
|
2673
|
-
const pkgPath =
|
|
2674
|
-
if (
|
|
3216
|
+
const pkgPath = join11(targetDir, "package.json");
|
|
3217
|
+
if (existsSync9(pkgPath)) {
|
|
2675
3218
|
try {
|
|
2676
|
-
const pkg = JSON.parse(
|
|
3219
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
2677
3220
|
if (pkg.name) {
|
|
2678
3221
|
const name = pkg.name.replace(/^@[\w-]+\//, "");
|
|
2679
3222
|
return `${name}-theme`;
|
|
@@ -2681,7 +3224,7 @@ function inferThemeName(targetDir) {
|
|
|
2681
3224
|
} catch {
|
|
2682
3225
|
}
|
|
2683
3226
|
}
|
|
2684
|
-
return `${
|
|
3227
|
+
return `${basename2(targetDir)}-theme`;
|
|
2685
3228
|
}
|
|
2686
3229
|
function confidenceComment(confidence) {
|
|
2687
3230
|
return `# confidence: ${confidence}`;
|
|
@@ -2710,7 +3253,7 @@ function outputJSON(result, validationResult) {
|
|
|
2710
3253
|
}
|
|
2711
3254
|
function outputYAML(result, outputPath, cwd, validationResult) {
|
|
2712
3255
|
const yamlStr = buildAnnotatedYAML(result);
|
|
2713
|
-
const outFile =
|
|
3256
|
+
const outFile = resolve8(cwd, outputPath ?? ".visor.yaml");
|
|
2714
3257
|
const high = result.tokens.filter((t) => t.confidence === "high").length;
|
|
2715
3258
|
const med = result.tokens.filter((t) => t.confidence === "medium").length;
|
|
2716
3259
|
const low = result.tokens.filter((t) => t.confidence === "low").length;
|
|
@@ -2763,11 +3306,11 @@ function buildAnnotatedYAML(result) {
|
|
|
2763
3306
|
for (const token of result.tokens) {
|
|
2764
3307
|
confidenceMap.set(token.name, token.confidence);
|
|
2765
3308
|
}
|
|
2766
|
-
const
|
|
3309
|
+
const lines2 = baseYaml.split("\n");
|
|
2767
3310
|
const annotated = [];
|
|
2768
3311
|
let inColors = false;
|
|
2769
3312
|
let inColorsDark = false;
|
|
2770
|
-
for (const line of
|
|
3313
|
+
for (const line of lines2) {
|
|
2771
3314
|
if (/^colors:/.test(line)) {
|
|
2772
3315
|
inColors = true;
|
|
2773
3316
|
inColorsDark = false;
|
|
@@ -2796,14 +3339,15 @@ function buildAnnotatedYAML(result) {
|
|
|
2796
3339
|
}
|
|
2797
3340
|
|
|
2798
3341
|
// src/commands/theme-register.ts
|
|
2799
|
-
import { readFileSync as
|
|
2800
|
-
import { resolve as
|
|
3342
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
|
|
3343
|
+
import { resolve as resolve10, join as join13 } from "path";
|
|
2801
3344
|
import { generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
|
|
2802
3345
|
import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
2803
3346
|
|
|
2804
3347
|
// src/utils/theme-helpers.ts
|
|
2805
|
-
import { existsSync as
|
|
2806
|
-
import { resolve as
|
|
3348
|
+
import { existsSync as existsSync10, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync13, readlinkSync, realpathSync, statSync as statSync6 } from "fs";
|
|
3349
|
+
import { resolve as resolve9, dirname as dirname6, join as join12 } from "path";
|
|
3350
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2807
3351
|
function toSlug(name) {
|
|
2808
3352
|
return name.toLowerCase().replace(/\s+/g, "-");
|
|
2809
3353
|
}
|
|
@@ -2811,17 +3355,132 @@ function toLabel(name) {
|
|
|
2811
3355
|
return name.split(/[\s-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2812
3356
|
}
|
|
2813
3357
|
function findRepoRoot(startDir) {
|
|
2814
|
-
let current =
|
|
3358
|
+
let current = resolve9(startDir);
|
|
2815
3359
|
while (true) {
|
|
2816
|
-
if (
|
|
3360
|
+
if (existsSync10(join12(current, "packages", "docs"))) {
|
|
2817
3361
|
return current;
|
|
2818
3362
|
}
|
|
2819
|
-
const parent =
|
|
3363
|
+
const parent = dirname6(current);
|
|
2820
3364
|
if (parent === current) break;
|
|
2821
3365
|
current = parent;
|
|
2822
3366
|
}
|
|
2823
3367
|
return null;
|
|
2824
3368
|
}
|
|
3369
|
+
function findMainRepoRoot(startDir) {
|
|
3370
|
+
try {
|
|
3371
|
+
const commonDir = execFileSync3("git", ["rev-parse", "--git-common-dir"], {
|
|
3372
|
+
cwd: startDir,
|
|
3373
|
+
encoding: "utf-8",
|
|
3374
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3375
|
+
}).trim();
|
|
3376
|
+
if (commonDir) {
|
|
3377
|
+
const absoluteCommonDir = resolve9(startDir, commonDir);
|
|
3378
|
+
const candidate = dirname6(absoluteCommonDir);
|
|
3379
|
+
if (existsSync10(join12(candidate, "packages", "docs"))) {
|
|
3380
|
+
return candidate;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
} catch {
|
|
3384
|
+
}
|
|
3385
|
+
return findRepoRoot(startDir);
|
|
3386
|
+
}
|
|
3387
|
+
var BrokenSymlinkError = class extends Error {
|
|
3388
|
+
constructor(path2, target) {
|
|
3389
|
+
super(`Broken symlink: ${path2} \u2192 ${target}`);
|
|
3390
|
+
this.path = path2;
|
|
3391
|
+
this.target = target;
|
|
3392
|
+
}
|
|
3393
|
+
path;
|
|
3394
|
+
target;
|
|
3395
|
+
code = "BROKEN_SYMLINK";
|
|
3396
|
+
};
|
|
3397
|
+
function assertNoBrokenSymlinks(dir) {
|
|
3398
|
+
if (!existsSync10(dir)) return;
|
|
3399
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
3400
|
+
for (const entry of entries) {
|
|
3401
|
+
const entryPath = join12(dir, entry.name);
|
|
3402
|
+
let lst;
|
|
3403
|
+
try {
|
|
3404
|
+
lst = lstatSync(entryPath);
|
|
3405
|
+
} catch {
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
if (lst.isSymbolicLink()) {
|
|
3409
|
+
const target = readlinkSync(entryPath);
|
|
3410
|
+
try {
|
|
3411
|
+
statSync6(entryPath);
|
|
3412
|
+
} catch {
|
|
3413
|
+
throw new BrokenSymlinkError(entryPath, target);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
function scanParentForPrivateThemes(parentDir) {
|
|
3419
|
+
if (!existsSync10(parentDir)) return [];
|
|
3420
|
+
const entries = readdirSync5(parentDir, { withFileTypes: true });
|
|
3421
|
+
const matches = [];
|
|
3422
|
+
for (const entry of entries) {
|
|
3423
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
3424
|
+
const candidate = join12(parentDir, entry.name, "visor-themes-private", "themes");
|
|
3425
|
+
if (existsSync10(candidate)) {
|
|
3426
|
+
matches.push(candidate);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
return matches.sort();
|
|
3430
|
+
}
|
|
3431
|
+
function scanNestedThemeDir(dir) {
|
|
3432
|
+
if (!existsSync10(dir)) return [];
|
|
3433
|
+
assertNoBrokenSymlinks(dir);
|
|
3434
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
3435
|
+
const out = [];
|
|
3436
|
+
for (const entry of entries) {
|
|
3437
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
3438
|
+
const themeFile = join12(dir, entry.name, "theme.visor.yaml");
|
|
3439
|
+
if (existsSync10(themeFile)) {
|
|
3440
|
+
out.push({ filePath: themeFile, slug: entry.name });
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return out;
|
|
3444
|
+
}
|
|
3445
|
+
function detectVisorWorkspace(cwd) {
|
|
3446
|
+
let current = resolve9(cwd);
|
|
3447
|
+
while (true) {
|
|
3448
|
+
const pkgPath = join12(current, "package.json");
|
|
3449
|
+
if (existsSync10(pkgPath)) {
|
|
3450
|
+
try {
|
|
3451
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
3452
|
+
const workspaces = pkg.workspaces ?? [];
|
|
3453
|
+
const hasCli = workspaces.some((w) => w.includes("packages/cli"));
|
|
3454
|
+
const hasEngine = workspaces.some((w) => w.includes("packages/theme-engine"));
|
|
3455
|
+
if (pkg.name === "visor" && hasCli && hasEngine) {
|
|
3456
|
+
return current;
|
|
3457
|
+
}
|
|
3458
|
+
} catch {
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
const parent = dirname6(current);
|
|
3462
|
+
if (parent === current) break;
|
|
3463
|
+
current = parent;
|
|
3464
|
+
}
|
|
3465
|
+
return null;
|
|
3466
|
+
}
|
|
3467
|
+
function isLocalVisorBinary(workspaceRoot, scriptPath) {
|
|
3468
|
+
if (!scriptPath) return false;
|
|
3469
|
+
const expectedPrefix = join12(workspaceRoot, "packages", "cli", "dist");
|
|
3470
|
+
let resolvedScript;
|
|
3471
|
+
let resolvedPrefix;
|
|
3472
|
+
try {
|
|
3473
|
+
resolvedScript = realpathSync(scriptPath);
|
|
3474
|
+
} catch {
|
|
3475
|
+
resolvedScript = resolve9(scriptPath);
|
|
3476
|
+
}
|
|
3477
|
+
try {
|
|
3478
|
+
resolvedPrefix = realpathSync(expectedPrefix);
|
|
3479
|
+
} catch {
|
|
3480
|
+
resolvedPrefix = resolve9(expectedPrefix);
|
|
3481
|
+
}
|
|
3482
|
+
return resolvedScript.startsWith(resolvedPrefix + "/") || resolvedScript === resolvedPrefix;
|
|
3483
|
+
}
|
|
2825
3484
|
|
|
2826
3485
|
// src/commands/theme-register.ts
|
|
2827
3486
|
function insertGlobalsImport(content, slug2) {
|
|
@@ -2829,32 +3488,32 @@ function insertGlobalsImport(content, slug2) {
|
|
|
2829
3488
|
if (content.includes(importLine)) {
|
|
2830
3489
|
return { updated: content, changed: false };
|
|
2831
3490
|
}
|
|
2832
|
-
const
|
|
3491
|
+
const lines2 = content.split("\n");
|
|
2833
3492
|
const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';/;
|
|
2834
3493
|
const themeImportIndices = [];
|
|
2835
|
-
for (let i = 0; i <
|
|
2836
|
-
if (themeImportPattern.test(
|
|
3494
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
3495
|
+
if (themeImportPattern.test(lines2[i])) {
|
|
2837
3496
|
themeImportIndices.push(i);
|
|
2838
3497
|
}
|
|
2839
3498
|
}
|
|
2840
3499
|
if (themeImportIndices.length === 0) {
|
|
2841
|
-
const lastImportIdx =
|
|
3500
|
+
const lastImportIdx = lines2.reduce(
|
|
2842
3501
|
(last, line, i) => line.startsWith("@import") ? i : last,
|
|
2843
3502
|
-1
|
|
2844
3503
|
);
|
|
2845
3504
|
const insertAt2 = lastImportIdx + 1;
|
|
2846
|
-
|
|
2847
|
-
return { updated:
|
|
3505
|
+
lines2.splice(insertAt2, 0, importLine);
|
|
3506
|
+
return { updated: lines2.join("\n"), changed: true };
|
|
2848
3507
|
}
|
|
2849
3508
|
let insertAt = themeImportIndices[themeImportIndices.length - 1] + 1;
|
|
2850
3509
|
for (const idx of themeImportIndices) {
|
|
2851
|
-
if (importLine <
|
|
3510
|
+
if (importLine < lines2[idx]) {
|
|
2852
3511
|
insertAt = idx;
|
|
2853
3512
|
break;
|
|
2854
3513
|
}
|
|
2855
3514
|
}
|
|
2856
|
-
|
|
2857
|
-
return { updated:
|
|
3515
|
+
lines2.splice(insertAt, 0, importLine);
|
|
3516
|
+
return { updated: lines2.join("\n"), changed: true };
|
|
2858
3517
|
}
|
|
2859
3518
|
function insertThemeConfig(content, slug2, label, group) {
|
|
2860
3519
|
if (content.includes(`value: "${slug2}"`)) {
|
|
@@ -2908,10 +3567,10 @@ ${indent}${newEntry},
|
|
|
2908
3567
|
return { updated, changed: true };
|
|
2909
3568
|
}
|
|
2910
3569
|
function themeRegisterCommand(file, cwd, options) {
|
|
2911
|
-
const filePath =
|
|
3570
|
+
const filePath = resolve10(cwd, file);
|
|
2912
3571
|
let yamlContent;
|
|
2913
3572
|
try {
|
|
2914
|
-
yamlContent =
|
|
3573
|
+
yamlContent = readFileSync14(filePath, "utf-8");
|
|
2915
3574
|
} catch {
|
|
2916
3575
|
if (options.json) {
|
|
2917
3576
|
console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
|
|
@@ -2954,11 +3613,11 @@ function themeRegisterCommand(file, cwd, options) {
|
|
|
2954
3613
|
process.exit(1);
|
|
2955
3614
|
return;
|
|
2956
3615
|
}
|
|
2957
|
-
const docsAppDir =
|
|
2958
|
-
const cssFilePath =
|
|
2959
|
-
const globalsPath =
|
|
2960
|
-
const themeConfigPath =
|
|
2961
|
-
if (!
|
|
3616
|
+
const docsAppDir = join13(repoRoot, "packages", "docs", "app");
|
|
3617
|
+
const cssFilePath = join13(docsAppDir, `${slug2}-theme.css`);
|
|
3618
|
+
const globalsPath = join13(docsAppDir, "globals.css");
|
|
3619
|
+
const themeConfigPath = join13(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
3620
|
+
if (!existsSync11(docsAppDir)) {
|
|
2962
3621
|
const msg = `Docs app directory not found: ${docsAppDir}`;
|
|
2963
3622
|
if (options.json) {
|
|
2964
3623
|
console.log(JSON.stringify({ success: false, error: msg }));
|
|
@@ -2971,8 +3630,8 @@ function themeRegisterCommand(file, cwd, options) {
|
|
|
2971
3630
|
let globalsContent = "";
|
|
2972
3631
|
let themeConfigContent = "";
|
|
2973
3632
|
try {
|
|
2974
|
-
globalsContent =
|
|
2975
|
-
themeConfigContent =
|
|
3633
|
+
globalsContent = readFileSync14(globalsPath, "utf-8");
|
|
3634
|
+
themeConfigContent = readFileSync14(themeConfigPath, "utf-8");
|
|
2976
3635
|
} catch (err) {
|
|
2977
3636
|
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
2978
3637
|
if (options.json) {
|
|
@@ -2983,8 +3642,8 @@ function themeRegisterCommand(file, cwd, options) {
|
|
|
2983
3642
|
process.exit(1);
|
|
2984
3643
|
return;
|
|
2985
3644
|
}
|
|
2986
|
-
const cssExists =
|
|
2987
|
-
const cssChanged = !cssExists ||
|
|
3645
|
+
const cssExists = existsSync11(cssFilePath);
|
|
3646
|
+
const cssChanged = !cssExists || readFileSync14(cssFilePath, "utf-8") !== css;
|
|
2988
3647
|
const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug2);
|
|
2989
3648
|
const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
|
|
2990
3649
|
themeConfigContent,
|
|
@@ -3068,8 +3727,8 @@ function themeRegisterCommand(file, cwd, options) {
|
|
|
3068
3727
|
}
|
|
3069
3728
|
|
|
3070
3729
|
// src/commands/theme-unregister.ts
|
|
3071
|
-
import { readFileSync as
|
|
3072
|
-
import { join as
|
|
3730
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, existsSync as existsSync12, unlinkSync } from "fs";
|
|
3731
|
+
import { join as join14 } from "path";
|
|
3073
3732
|
function removeGlobalsImport(content, slug2) {
|
|
3074
3733
|
const importLine = `@import './${slug2}-theme.css';`;
|
|
3075
3734
|
if (!content.includes(importLine)) {
|
|
@@ -3101,11 +3760,11 @@ function themeUnregisterCommand(slug2, cwd, options) {
|
|
|
3101
3760
|
process.exit(1);
|
|
3102
3761
|
return;
|
|
3103
3762
|
}
|
|
3104
|
-
const docsAppDir =
|
|
3105
|
-
const cssFilePath =
|
|
3106
|
-
const globalsPath =
|
|
3107
|
-
const themeConfigPath =
|
|
3108
|
-
if (!
|
|
3763
|
+
const docsAppDir = join14(repoRoot, "packages", "docs", "app");
|
|
3764
|
+
const cssFilePath = join14(docsAppDir, `${slug2}-theme.css`);
|
|
3765
|
+
const globalsPath = join14(docsAppDir, "globals.css");
|
|
3766
|
+
const themeConfigPath = join14(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
3767
|
+
if (!existsSync12(docsAppDir)) {
|
|
3109
3768
|
const msg = `Docs app directory not found: ${docsAppDir}`;
|
|
3110
3769
|
if (options.json) {
|
|
3111
3770
|
console.log(JSON.stringify({ success: false, error: msg }));
|
|
@@ -3118,8 +3777,8 @@ function themeUnregisterCommand(slug2, cwd, options) {
|
|
|
3118
3777
|
let globalsContent = "";
|
|
3119
3778
|
let themeConfigContent = "";
|
|
3120
3779
|
try {
|
|
3121
|
-
globalsContent =
|
|
3122
|
-
themeConfigContent =
|
|
3780
|
+
globalsContent = readFileSync15(globalsPath, "utf-8");
|
|
3781
|
+
themeConfigContent = readFileSync15(themeConfigPath, "utf-8");
|
|
3123
3782
|
} catch (err) {
|
|
3124
3783
|
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
3125
3784
|
if (options.json) {
|
|
@@ -3130,7 +3789,7 @@ function themeUnregisterCommand(slug2, cwd, options) {
|
|
|
3130
3789
|
process.exit(1);
|
|
3131
3790
|
return;
|
|
3132
3791
|
}
|
|
3133
|
-
const cssExists =
|
|
3792
|
+
const cssExists = existsSync12(cssFilePath);
|
|
3134
3793
|
const { updated: newGlobals, changed: globalsChanged } = removeGlobalsImport(globalsContent, slug2);
|
|
3135
3794
|
const { updated: newThemeConfig, changed: themeConfigChanged } = removeThemeConfigEntry(themeConfigContent, slug2);
|
|
3136
3795
|
if (!cssExists && !globalsChanged && !themeConfigChanged) {
|
|
@@ -3171,30 +3830,150 @@ function themeUnregisterCommand(slug2, cwd, options) {
|
|
|
3171
3830
|
|
|
3172
3831
|
// src/commands/theme-sync.ts
|
|
3173
3832
|
import {
|
|
3174
|
-
readFileSync as
|
|
3833
|
+
readFileSync as readFileSync16,
|
|
3175
3834
|
writeFileSync as writeFileSync9,
|
|
3176
3835
|
mkdirSync as mkdirSync5,
|
|
3177
|
-
existsSync as
|
|
3178
|
-
readdirSync as
|
|
3836
|
+
existsSync as existsSync13,
|
|
3837
|
+
readdirSync as readdirSync6,
|
|
3179
3838
|
unlinkSync as unlinkSync2,
|
|
3180
3839
|
copyFileSync
|
|
3181
3840
|
} from "fs";
|
|
3182
|
-
import { join as
|
|
3841
|
+
import { join as join15, basename as basename3, resolve as resolve11, sep } from "path";
|
|
3183
3842
|
import { parse as parseYaml2 } from "yaml";
|
|
3184
3843
|
import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
|
|
3185
3844
|
import { docsAdapter as docsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
3845
|
+
var PRIVATE_THEMES_REPO_URL = "git@github.com:low-orbit-studio/visor-themes-private.git";
|
|
3846
|
+
var PRIVATE_THEMES_ENV_VAR = "VISOR_THEMES_PRIVATE_PATH";
|
|
3186
3847
|
var GLOBALS_BEGIN_MARKER = "/* BEGIN visor-theme-imports \u2014 managed by `visor theme sync` */";
|
|
3187
3848
|
var GLOBALS_END_MARKER = "/* END visor-theme-imports */";
|
|
3188
3849
|
var STOCK_GROUPS_BEGIN_MARKER = "/* BEGIN visor-stock-themes \u2014 managed by `visor theme sync` */";
|
|
3189
3850
|
var STOCK_GROUPS_END_MARKER = "/* END visor-stock-themes */";
|
|
3190
|
-
var GITIGNORE_BEGIN_MARKER = "# BEGIN visor-custom-theme-css (managed by `visor theme sync` \u2014 do not edit manually)";
|
|
3191
|
-
var GITIGNORE_END_MARKER = "# END visor-custom-theme-css";
|
|
3192
3851
|
var CUSTOM_OVERLAY_CSS_PATH = "packages/docs/app/custom-themes.generated.css";
|
|
3193
3852
|
var CUSTOM_OVERLAY_TS_PATH = "packages/docs/lib/theme-config.custom.generated.ts";
|
|
3194
3853
|
var CUSTOM_OVERLAY_IMPORT_LINE = "@import './custom-themes.generated.css';";
|
|
3195
3854
|
function scanThemeDir(dir) {
|
|
3196
|
-
if (!
|
|
3197
|
-
|
|
3855
|
+
if (!existsSync13(dir)) return [];
|
|
3856
|
+
assertNoBrokenSymlinks(dir);
|
|
3857
|
+
return readdirSync6(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join15(dir, f));
|
|
3858
|
+
}
|
|
3859
|
+
function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
|
|
3860
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3861
|
+
const deprecationWarnings = [];
|
|
3862
|
+
const addNested = (dir, origin) => {
|
|
3863
|
+
const entries = scanNestedThemeDir(dir);
|
|
3864
|
+
for (const entry of entries) {
|
|
3865
|
+
const existing = merged.get(entry.slug);
|
|
3866
|
+
if (existing) {
|
|
3867
|
+
warn(
|
|
3868
|
+
`Duplicate theme slug "${entry.slug}" \u2014 keeping ${existing.origin} source (${existing.filePath}); ignoring ${origin} source (${entry.filePath}).`
|
|
3869
|
+
);
|
|
3870
|
+
continue;
|
|
3871
|
+
}
|
|
3872
|
+
merged.set(entry.slug, {
|
|
3873
|
+
filePath: entry.filePath,
|
|
3874
|
+
slug: entry.slug,
|
|
3875
|
+
origin
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
const envPath = process.env[PRIVATE_THEMES_ENV_VAR];
|
|
3880
|
+
if (envPath && envPath.trim() !== "") {
|
|
3881
|
+
const resolved = resolve11(envPath);
|
|
3882
|
+
if (!existsSync13(resolved)) {
|
|
3883
|
+
throw new Error(
|
|
3884
|
+
`${PRIVATE_THEMES_ENV_VAR} is set to "${envPath}" but the path does not exist. Expected a directory containing {slug}/theme.visor.yaml entries.`
|
|
3885
|
+
);
|
|
3886
|
+
}
|
|
3887
|
+
addNested(resolved, "env");
|
|
3888
|
+
}
|
|
3889
|
+
const siblingPath = join15(mainRepoRoot, "..", "visor-themes-private", "themes");
|
|
3890
|
+
const siblingExists = existsSync13(siblingPath);
|
|
3891
|
+
if (siblingExists) {
|
|
3892
|
+
addNested(siblingPath, "sibling");
|
|
3893
|
+
}
|
|
3894
|
+
const parentDir = join15(mainRepoRoot, "..");
|
|
3895
|
+
const parentGlobMatches = scanParentForPrivateThemes(parentDir).filter(
|
|
3896
|
+
(path2) => !path2.startsWith(mainRepoRoot + sep)
|
|
3897
|
+
);
|
|
3898
|
+
if (parentGlobMatches.length > 0) {
|
|
3899
|
+
if (siblingExists) {
|
|
3900
|
+
for (const path2 of parentGlobMatches) {
|
|
3901
|
+
warn(
|
|
3902
|
+
`Found one-level-deeper theme source ${path2} but using true-sibling at ${siblingPath} (preferred per BO-29 D2).`
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
} else {
|
|
3906
|
+
const [first, ...rest] = parentGlobMatches;
|
|
3907
|
+
addNested(first, "parent-glob");
|
|
3908
|
+
for (const other of rest) {
|
|
3909
|
+
warn(
|
|
3910
|
+
`Multiple one-level-deeper theme sources found; using ${first} and ignoring ${other}. Set ${PRIVATE_THEMES_ENV_VAR} to override.`
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
const legacyDir = join15(repoRoot, "custom-themes");
|
|
3916
|
+
const legacyFiles = scanThemeDir(legacyDir);
|
|
3917
|
+
for (const legacyFile of legacyFiles) {
|
|
3918
|
+
const slug2 = basename3(legacyFile).replace(/\.visor\.yaml$/, "");
|
|
3919
|
+
const existing = merged.get(slug2);
|
|
3920
|
+
if (existing) {
|
|
3921
|
+
warn(
|
|
3922
|
+
`Duplicate theme slug "${slug2}" \u2014 keeping ${existing.origin} source (${existing.filePath}); ignoring legacy source (${legacyFile}).`
|
|
3923
|
+
);
|
|
3924
|
+
continue;
|
|
3925
|
+
}
|
|
3926
|
+
deprecationWarnings.push(
|
|
3927
|
+
`Deprecated legacy custom-themes/ source: ${legacyFile} \u2014 migrate to visor-themes-private (see docs).`
|
|
3928
|
+
);
|
|
3929
|
+
merged.set(slug2, {
|
|
3930
|
+
filePath: legacyFile,
|
|
3931
|
+
slug: slug2,
|
|
3932
|
+
origin: "legacy"
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
return { files: [...merged.values()], deprecationWarnings };
|
|
3936
|
+
}
|
|
3937
|
+
function reportBrokenSymlink(err, options) {
|
|
3938
|
+
const msg = `Broken symlink in theme source: ${err.path} \u2192 ${err.target}`;
|
|
3939
|
+
if (options.json) {
|
|
3940
|
+
console.log(JSON.stringify({ success: false, error: msg, path: err.path, target: err.target }));
|
|
3941
|
+
} else {
|
|
3942
|
+
logger.error(msg);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
function buildEmptySourcesMessage(mainRepoRoot) {
|
|
3946
|
+
const expectedSibling = join15(mainRepoRoot, "..", "visor-themes-private");
|
|
3947
|
+
return [
|
|
3948
|
+
"No theme sources discovered. Cannot proceed \u2014 refusing to wipe generated CSS.",
|
|
3949
|
+
"",
|
|
3950
|
+
"Resolution order checked:",
|
|
3951
|
+
` 1. Env var ${PRIVATE_THEMES_ENV_VAR} (unset or empty)`,
|
|
3952
|
+
` 2. Sibling checkout at ${expectedSibling}/themes/ (not found)`,
|
|
3953
|
+
` 3. One-level-deeper at ${join15(mainRepoRoot, "..")}/<parent>/visor-themes-private/themes/ (not found)`,
|
|
3954
|
+
" 4. Legacy custom-themes/ (no .visor.yaml files)",
|
|
3955
|
+
"",
|
|
3956
|
+
"To fix, clone the private themes repo as a sibling:",
|
|
3957
|
+
` git clone ${PRIVATE_THEMES_REPO_URL} ${expectedSibling}`,
|
|
3958
|
+
"",
|
|
3959
|
+
`Or set ${PRIVATE_THEMES_ENV_VAR} to a directory containing {slug}/theme.visor.yaml entries.`
|
|
3960
|
+
].join("\n");
|
|
3961
|
+
}
|
|
3962
|
+
function enforceWorkspaceGuard(cwd) {
|
|
3963
|
+
if (process.env.VITEST) return null;
|
|
3964
|
+
if (process.env.VISOR_SKIP_WORKSPACE_GUARD) return null;
|
|
3965
|
+
const workspaceRoot = detectVisorWorkspace(cwd);
|
|
3966
|
+
if (!workspaceRoot) return null;
|
|
3967
|
+
if (isLocalVisorBinary(workspaceRoot, process.argv[1])) return null;
|
|
3968
|
+
return [
|
|
3969
|
+
`Detected Visor workspace at ${workspaceRoot}.`,
|
|
3970
|
+
"The global `visor` CLI bundles a published theme-engine that lags HEAD and",
|
|
3971
|
+
"can regress stock theme CSS files. Run the workspace command instead:",
|
|
3972
|
+
"",
|
|
3973
|
+
" npm run theme:sync",
|
|
3974
|
+
"",
|
|
3975
|
+
`(Override with VISOR_SKIP_WORKSPACE_GUARD=1 if you really know what you're doing.)`
|
|
3976
|
+
].join("\n");
|
|
3198
3977
|
}
|
|
3199
3978
|
function extractGroup(yamlContent) {
|
|
3200
3979
|
const parsed = parseYaml2(yamlContent);
|
|
@@ -3231,28 +4010,28 @@ ${GLOBALS_END_MARKER}`;
|
|
|
3231
4010
|
updated = content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GLOBALS_END_MARKER.length);
|
|
3232
4011
|
} else {
|
|
3233
4012
|
const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';\n?/gm;
|
|
3234
|
-
const
|
|
4013
|
+
const lines2 = content.split("\n");
|
|
3235
4014
|
let firstThemeIdx = -1;
|
|
3236
4015
|
let lastThemeIdx = -1;
|
|
3237
|
-
for (let i = 0; i <
|
|
3238
|
-
if (/^@import '\.\/[\w-]+-theme\.css';/.test(
|
|
4016
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
4017
|
+
if (/^@import '\.\/[\w-]+-theme\.css';/.test(lines2[i])) {
|
|
3239
4018
|
if (firstThemeIdx === -1) firstThemeIdx = i;
|
|
3240
4019
|
lastThemeIdx = i;
|
|
3241
4020
|
}
|
|
3242
4021
|
}
|
|
3243
4022
|
if (firstThemeIdx !== -1) {
|
|
3244
|
-
const before =
|
|
3245
|
-
const after =
|
|
4023
|
+
const before = lines2.slice(0, firstThemeIdx);
|
|
4024
|
+
const after = lines2.slice(lastThemeIdx + 1);
|
|
3246
4025
|
updated = [...before, newBlock, ...after].join("\n");
|
|
3247
4026
|
} else {
|
|
3248
4027
|
void themeImportPattern;
|
|
3249
|
-
const lastImportIdx =
|
|
4028
|
+
const lastImportIdx = lines2.reduce(
|
|
3250
4029
|
(last, line, i) => line.startsWith("@import") ? i : last,
|
|
3251
4030
|
-1
|
|
3252
4031
|
);
|
|
3253
4032
|
const insertAt = lastImportIdx + 1;
|
|
3254
|
-
|
|
3255
|
-
updated =
|
|
4033
|
+
lines2.splice(insertAt, 0, newBlock);
|
|
4034
|
+
updated = lines2.join("\n");
|
|
3256
4035
|
}
|
|
3257
4036
|
}
|
|
3258
4037
|
updated = ensureCustomOverlayImport(updated);
|
|
@@ -3353,19 +4132,17 @@ ${groupsTs}
|
|
|
3353
4132
|
];
|
|
3354
4133
|
`;
|
|
3355
4134
|
}
|
|
3356
|
-
function updateGitignoreBlock(content, customSlugs) {
|
|
3357
|
-
const cssLines = customSlugs.sort().map((slug2) => `packages/docs/app/${slug2}-theme.css`).join("\n");
|
|
3358
|
-
const newBlock = `${GITIGNORE_BEGIN_MARKER}
|
|
3359
|
-
${cssLines}
|
|
3360
|
-
${GITIGNORE_END_MARKER}`;
|
|
3361
|
-
const beginIdx = content.indexOf(GITIGNORE_BEGIN_MARKER);
|
|
3362
|
-
const endIdx = content.indexOf(GITIGNORE_END_MARKER);
|
|
3363
|
-
if (beginIdx !== -1 && endIdx !== -1) {
|
|
3364
|
-
return content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GITIGNORE_END_MARKER.length);
|
|
3365
|
-
}
|
|
3366
|
-
return content.trimEnd() + "\n\n" + newBlock + "\n";
|
|
3367
|
-
}
|
|
3368
4135
|
function themeSyncCommand(cwd, options) {
|
|
4136
|
+
const guardError = enforceWorkspaceGuard(cwd);
|
|
4137
|
+
if (guardError) {
|
|
4138
|
+
if (options.json) {
|
|
4139
|
+
console.log(JSON.stringify({ success: false, error: guardError }));
|
|
4140
|
+
} else {
|
|
4141
|
+
logger.error(guardError);
|
|
4142
|
+
}
|
|
4143
|
+
process.exit(1);
|
|
4144
|
+
return;
|
|
4145
|
+
}
|
|
3369
4146
|
const repoRoot = findRepoRoot(cwd);
|
|
3370
4147
|
if (!repoRoot) {
|
|
3371
4148
|
const msg = "Could not locate repo root (packages/docs/ not found). Run from within the visor repo.";
|
|
@@ -3377,33 +4154,72 @@ function themeSyncCommand(cwd, options) {
|
|
|
3377
4154
|
process.exit(1);
|
|
3378
4155
|
return;
|
|
3379
4156
|
}
|
|
3380
|
-
const
|
|
3381
|
-
const
|
|
3382
|
-
const docsAppDir =
|
|
3383
|
-
const docsLibDir =
|
|
3384
|
-
const docsPublicThemesDir =
|
|
3385
|
-
const themeConfigPath =
|
|
3386
|
-
const globalsPath =
|
|
3387
|
-
const
|
|
3388
|
-
const
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
4157
|
+
const mainRepoRoot = findMainRepoRoot(cwd) ?? repoRoot;
|
|
4158
|
+
const themesDir = join15(repoRoot, "themes");
|
|
4159
|
+
const docsAppDir = join15(repoRoot, "packages", "docs", "app");
|
|
4160
|
+
const docsLibDir = join15(repoRoot, "packages", "docs", "lib");
|
|
4161
|
+
const docsPublicThemesDir = join15(repoRoot, "packages", "docs", "public", "themes");
|
|
4162
|
+
const themeConfigPath = join15(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
4163
|
+
const globalsPath = join15(docsAppDir, "globals.css");
|
|
4164
|
+
const customOverlayCssPath = join15(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
|
|
4165
|
+
const customOverlayTsPath = join15(repoRoot, CUSTOM_OVERLAY_TS_PATH);
|
|
4166
|
+
let stockFiles;
|
|
4167
|
+
try {
|
|
4168
|
+
stockFiles = scanThemeDir(themesDir);
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
if (err instanceof BrokenSymlinkError) {
|
|
4171
|
+
reportBrokenSymlink(err, options);
|
|
4172
|
+
process.exit(1);
|
|
4173
|
+
return;
|
|
4174
|
+
}
|
|
4175
|
+
throw err;
|
|
4176
|
+
}
|
|
4177
|
+
let customSources = [];
|
|
4178
|
+
let deprecationWarnings = [];
|
|
4179
|
+
const discoveryWarnings = [];
|
|
4180
|
+
try {
|
|
4181
|
+
const result = resolveCustomSources(
|
|
4182
|
+
repoRoot,
|
|
4183
|
+
mainRepoRoot,
|
|
4184
|
+
(msg) => discoveryWarnings.push(msg)
|
|
4185
|
+
);
|
|
4186
|
+
customSources = result.files;
|
|
4187
|
+
deprecationWarnings = result.deprecationWarnings;
|
|
4188
|
+
} catch (err) {
|
|
4189
|
+
if (err instanceof BrokenSymlinkError) {
|
|
4190
|
+
reportBrokenSymlink(err, options);
|
|
4191
|
+
process.exit(1);
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
const msg = err instanceof Error ? err.message : "Custom theme discovery failed";
|
|
3394
4195
|
if (options.json) {
|
|
3395
4196
|
console.log(JSON.stringify({ success: false, error: msg }));
|
|
3396
4197
|
} else {
|
|
3397
|
-
logger.
|
|
4198
|
+
logger.error(msg);
|
|
3398
4199
|
}
|
|
4200
|
+
process.exit(1);
|
|
3399
4201
|
return;
|
|
3400
4202
|
}
|
|
4203
|
+
if (stockFiles.length === 0 && customSources.length === 0) {
|
|
4204
|
+
const msg = buildEmptySourcesMessage(mainRepoRoot);
|
|
4205
|
+
if (options.json) {
|
|
4206
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
4207
|
+
} else {
|
|
4208
|
+
logger.error(msg);
|
|
4209
|
+
}
|
|
4210
|
+
process.exit(1);
|
|
4211
|
+
return;
|
|
4212
|
+
}
|
|
4213
|
+
if (!options.json) {
|
|
4214
|
+
for (const w of deprecationWarnings) logger.warn(w);
|
|
4215
|
+
for (const w of discoveryWarnings) logger.warn(w);
|
|
4216
|
+
}
|
|
3401
4217
|
const manifest = [];
|
|
3402
4218
|
const errors = [];
|
|
3403
|
-
const processFile = (filePath, isCustom) => {
|
|
4219
|
+
const processFile = (filePath, isCustom, slugOverride) => {
|
|
3404
4220
|
let yamlContent;
|
|
3405
4221
|
try {
|
|
3406
|
-
yamlContent =
|
|
4222
|
+
yamlContent = readFileSync16(filePath, "utf-8");
|
|
3407
4223
|
} catch {
|
|
3408
4224
|
errors.push(`Could not read: ${filePath}`);
|
|
3409
4225
|
return;
|
|
@@ -3412,19 +4228,22 @@ function themeSyncCommand(cwd, options) {
|
|
|
3412
4228
|
try {
|
|
3413
4229
|
data = generateThemeData4(yamlContent);
|
|
3414
4230
|
} catch (err) {
|
|
3415
|
-
errors.push(`Failed to parse ${
|
|
4231
|
+
errors.push(`Failed to parse ${basename3(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
3416
4232
|
return;
|
|
3417
4233
|
}
|
|
3418
|
-
const slug2 = toSlug(data.config.name);
|
|
4234
|
+
const slug2 = slugOverride ?? toSlug(data.config.name);
|
|
3419
4235
|
const label = extractLabel(yamlContent) ?? toLabel(data.config.name);
|
|
3420
4236
|
const group = extractGroup(yamlContent) ?? (isCustom ? "Custom" : "Visor");
|
|
3421
4237
|
const defaultMode = extractDefaultMode(yamlContent);
|
|
3422
4238
|
const css = docsAdapter3({ primitives: data.primitives, tokens: data.tokens, config: data.config });
|
|
3423
|
-
const yamlFilename =
|
|
4239
|
+
const yamlFilename = slugOverride ?? basename3(filePath).replace(/\.visor\.yaml$/, "");
|
|
3424
4240
|
manifest.push({ slug: slug2, label, group, defaultMode, css, yamlFilename, isCustom });
|
|
3425
4241
|
};
|
|
3426
4242
|
for (const f of stockFiles) processFile(f, false);
|
|
3427
|
-
for (const
|
|
4243
|
+
for (const c of customSources) {
|
|
4244
|
+
const isNested = c.origin !== "legacy";
|
|
4245
|
+
processFile(c.filePath, true, isNested ? c.slug : void 0);
|
|
4246
|
+
}
|
|
3428
4247
|
if (errors.length > 0) {
|
|
3429
4248
|
if (options.json) {
|
|
3430
4249
|
console.log(JSON.stringify({ success: false, errors }));
|
|
@@ -3437,15 +4256,12 @@ function themeSyncCommand(cwd, options) {
|
|
|
3437
4256
|
const stockManifest = manifest.filter((e) => !e.isCustom);
|
|
3438
4257
|
const customManifest = manifest.filter((e) => e.isCustom);
|
|
3439
4258
|
const stockSlugs = stockManifest.map((e) => e.slug);
|
|
3440
|
-
const customSlugs = customManifest.map((e) => e.slug);
|
|
3441
4259
|
const allSlugs = manifest.map((e) => e.slug);
|
|
3442
4260
|
let globalsContent;
|
|
3443
4261
|
let themeConfigContent;
|
|
3444
|
-
let gitignoreContent;
|
|
3445
4262
|
try {
|
|
3446
|
-
globalsContent =
|
|
3447
|
-
themeConfigContent =
|
|
3448
|
-
gitignoreContent = existsSync12(gitignorePath) ? readFileSync14(gitignorePath, "utf-8") : "";
|
|
4263
|
+
globalsContent = readFileSync16(globalsPath, "utf-8");
|
|
4264
|
+
themeConfigContent = readFileSync16(themeConfigPath, "utf-8");
|
|
3449
4265
|
} catch (err) {
|
|
3450
4266
|
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
3451
4267
|
if (options.json) {
|
|
@@ -3458,15 +4274,14 @@ function themeSyncCommand(cwd, options) {
|
|
|
3458
4274
|
}
|
|
3459
4275
|
const newGlobals = updateGlobalsImports(globalsContent, stockSlugs);
|
|
3460
4276
|
const newThemeConfig = updateStockThemeConfigBlock(themeConfigContent, stockManifest);
|
|
3461
|
-
const newGitignore = customSlugs.length > 0 ? updateGitignoreBlock(gitignoreContent, customSlugs) : gitignoreContent;
|
|
3462
4277
|
const newCustomOverlayCss = generateCustomOverlayCss(customManifest);
|
|
3463
4278
|
const newCustomOverlayTs = generateCustomOverlayTs(customManifest);
|
|
3464
|
-
const existingCssFiles =
|
|
4279
|
+
const existingCssFiles = existsSync13(docsAppDir) ? readdirSync6(docsAppDir).filter(
|
|
3465
4280
|
(f) => f.endsWith("-theme.css") && f !== "custom-themes.generated.css"
|
|
3466
4281
|
) : [];
|
|
3467
4282
|
const newCssSet = new Set(allSlugs.map((s) => `${s}-theme.css`));
|
|
3468
4283
|
const staleCssFiles = existingCssFiles.filter((f) => !newCssSet.has(f));
|
|
3469
|
-
const existingPublicYamls =
|
|
4284
|
+
const existingPublicYamls = existsSync13(docsPublicThemesDir) ? readdirSync6(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
|
|
3470
4285
|
const newPublicYamlSet = new Set(manifest.map((e) => `${e.yamlFilename}.visor.yaml`));
|
|
3471
4286
|
const stalePublicYamls = existingPublicYamls.filter((f) => !newPublicYamlSet.has(f));
|
|
3472
4287
|
if (options.dryRun) {
|
|
@@ -3478,7 +4293,6 @@ function themeSyncCommand(cwd, options) {
|
|
|
3478
4293
|
globalsCSS: globalsPath,
|
|
3479
4294
|
customOverlayCss: CUSTOM_OVERLAY_CSS_PATH,
|
|
3480
4295
|
customOverlayTs: CUSTOM_OVERLAY_TS_PATH,
|
|
3481
|
-
gitignore: gitignorePath,
|
|
3482
4296
|
publicYamlsCopied: manifest.map((e) => `packages/docs/public/themes/${e.yamlFilename}.visor.yaml`),
|
|
3483
4297
|
publicYamlsDeleted: stalePublicYamls.map((f) => `packages/docs/public/themes/${f}`)
|
|
3484
4298
|
};
|
|
@@ -3498,25 +4312,24 @@ function themeSyncCommand(cwd, options) {
|
|
|
3498
4312
|
mkdirSync5(docsLibDir, { recursive: true });
|
|
3499
4313
|
mkdirSync5(docsPublicThemesDir, { recursive: true });
|
|
3500
4314
|
for (const entry of manifest) {
|
|
3501
|
-
writeFileSync9(
|
|
4315
|
+
writeFileSync9(join15(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
|
|
3502
4316
|
}
|
|
3503
4317
|
for (const stale of staleCssFiles) {
|
|
3504
|
-
unlinkSync2(
|
|
4318
|
+
unlinkSync2(join15(docsAppDir, stale));
|
|
3505
4319
|
}
|
|
3506
4320
|
writeFileSync9(customOverlayCssPath, newCustomOverlayCss, "utf-8");
|
|
3507
4321
|
writeFileSync9(customOverlayTsPath, newCustomOverlayTs, "utf-8");
|
|
3508
4322
|
writeFileSync9(themeConfigPath, newThemeConfig, "utf-8");
|
|
3509
4323
|
writeFileSync9(globalsPath, newGlobals, "utf-8");
|
|
3510
|
-
|
|
3511
|
-
|
|
4324
|
+
for (const srcFile of stockFiles) {
|
|
4325
|
+
copyFileSync(srcFile, join15(docsPublicThemesDir, basename3(srcFile)));
|
|
3512
4326
|
}
|
|
3513
|
-
const
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
copyFileSync(srcFile, join14(docsPublicThemesDir, filename));
|
|
4327
|
+
for (const c of customSources) {
|
|
4328
|
+
const targetName = c.origin === "legacy" ? basename3(c.filePath) : `${c.slug}.visor.yaml`;
|
|
4329
|
+
copyFileSync(c.filePath, join15(docsPublicThemesDir, targetName));
|
|
3517
4330
|
}
|
|
3518
4331
|
for (const stale of stalePublicYamls) {
|
|
3519
|
-
unlinkSync2(
|
|
4332
|
+
unlinkSync2(join15(docsPublicThemesDir, stale));
|
|
3520
4333
|
}
|
|
3521
4334
|
} catch (err) {
|
|
3522
4335
|
const msg = err instanceof Error ? err.message : "Write failed";
|
|
@@ -3529,6 +4342,7 @@ function themeSyncCommand(cwd, options) {
|
|
|
3529
4342
|
return;
|
|
3530
4343
|
}
|
|
3531
4344
|
if (options.json) {
|
|
4345
|
+
const warnings = [...deprecationWarnings, ...discoveryWarnings];
|
|
3532
4346
|
console.log(JSON.stringify({
|
|
3533
4347
|
success: true,
|
|
3534
4348
|
themes: manifest.length,
|
|
@@ -3536,7 +4350,8 @@ function themeSyncCommand(cwd, options) {
|
|
|
3536
4350
|
custom: customManifest.length,
|
|
3537
4351
|
staleCssDeleted: staleCssFiles.length,
|
|
3538
4352
|
staleYamlsDeleted: stalePublicYamls.length,
|
|
3539
|
-
slugs: allSlugs
|
|
4353
|
+
slugs: allSlugs,
|
|
4354
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
3540
4355
|
}));
|
|
3541
4356
|
} else {
|
|
3542
4357
|
logger.success(`Theme sync complete \u2014 ${manifest.length} themes registered`);
|
|
@@ -3552,19 +4367,19 @@ function themeSyncCommand(cwd, options) {
|
|
|
3552
4367
|
|
|
3553
4368
|
// src/commands/theme-batch-apply-flutter.ts
|
|
3554
4369
|
import {
|
|
3555
|
-
readFileSync as
|
|
4370
|
+
readFileSync as readFileSync17,
|
|
3556
4371
|
writeFileSync as writeFileSync10,
|
|
3557
4372
|
mkdirSync as mkdirSync6,
|
|
3558
|
-
existsSync as
|
|
3559
|
-
readdirSync as
|
|
4373
|
+
existsSync as existsSync14,
|
|
4374
|
+
readdirSync as readdirSync7,
|
|
3560
4375
|
rmSync
|
|
3561
4376
|
} from "fs";
|
|
3562
|
-
import { join as
|
|
4377
|
+
import { join as join16, basename as basename4, dirname as dirname7 } from "path";
|
|
3563
4378
|
import { generateThemeData as generateThemeData5 } from "@loworbitstudio/visor-theme-engine";
|
|
3564
4379
|
import { flutterAdapter as flutterAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
3565
4380
|
function scanThemeDir2(dir) {
|
|
3566
|
-
if (!
|
|
3567
|
-
return
|
|
4381
|
+
if (!existsSync14(dir)) return [];
|
|
4382
|
+
return readdirSync7(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join16(dir, f)).sort();
|
|
3568
4383
|
}
|
|
3569
4384
|
function slugToCamel(slug2) {
|
|
3570
4385
|
return slug2.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -3627,7 +4442,7 @@ function slugToDartPrefix(slug2) {
|
|
|
3627
4442
|
return slug2.replace(/-/g, "_") + "_t";
|
|
3628
4443
|
}
|
|
3629
4444
|
function emitMetaBarrel(slugs) {
|
|
3630
|
-
const
|
|
4445
|
+
const lines2 = [
|
|
3631
4446
|
`// GENERATED BY visor \u2014 DO NOT EDIT.`,
|
|
3632
4447
|
`// Regenerate with \`npm run themes:apply-flutter\`.`,
|
|
3633
4448
|
`//`,
|
|
@@ -3643,37 +4458,37 @@ function emitMetaBarrel(slugs) {
|
|
|
3643
4458
|
];
|
|
3644
4459
|
for (const slug2 of slugs) {
|
|
3645
4460
|
const prefix = slugToDartPrefix(slug2);
|
|
3646
|
-
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
4461
|
+
lines2.push(`import 'src/${slug2}/theme/visor_theme.dart' as ${prefix};`);
|
|
4462
|
+
}
|
|
4463
|
+
lines2.push(``);
|
|
4464
|
+
lines2.push(`/// A light/dark [ThemeData] pair for a single Visor theme.`);
|
|
4465
|
+
lines2.push(`class VisorThemePair {`);
|
|
4466
|
+
lines2.push(` final ThemeData light;`);
|
|
4467
|
+
lines2.push(` final ThemeData dark;`);
|
|
4468
|
+
lines2.push(` const VisorThemePair({required this.light, required this.dark});`);
|
|
4469
|
+
lines2.push(`}`);
|
|
4470
|
+
lines2.push(``);
|
|
4471
|
+
lines2.push(`/// Static access to all Visor-generated Flutter themes.`);
|
|
4472
|
+
lines2.push(`///`);
|
|
4473
|
+
lines2.push(`/// Usage:`);
|
|
4474
|
+
lines2.push(`/// \`\`\`dart`);
|
|
4475
|
+
lines2.push(`/// MaterialApp(`);
|
|
4476
|
+
lines2.push(`/// theme: VisorThemes.blackout.light,`);
|
|
4477
|
+
lines2.push(`/// darkTheme: VisorThemes.blackout.dark,`);
|
|
4478
|
+
lines2.push(`/// );`);
|
|
4479
|
+
lines2.push(`/// \`\`\``);
|
|
4480
|
+
lines2.push(`sealed class VisorThemes {`);
|
|
3666
4481
|
for (const slug2 of slugs) {
|
|
3667
4482
|
const camel = slugToCamel(slug2);
|
|
3668
4483
|
const prefix = slugToDartPrefix(slug2);
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
4484
|
+
lines2.push(` static VisorThemePair get ${camel} => VisorThemePair(`);
|
|
4485
|
+
lines2.push(` light: ${prefix}.VisorAppTheme.light,`);
|
|
4486
|
+
lines2.push(` dark: ${prefix}.VisorAppTheme.dark,`);
|
|
4487
|
+
lines2.push(` );`);
|
|
3673
4488
|
}
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
return
|
|
4489
|
+
lines2.push(`}`);
|
|
4490
|
+
lines2.push(``);
|
|
4491
|
+
return lines2.join("\n");
|
|
3677
4492
|
}
|
|
3678
4493
|
function emitGitignore() {
|
|
3679
4494
|
return [
|
|
@@ -3697,9 +4512,9 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3697
4512
|
process.exit(1);
|
|
3698
4513
|
return;
|
|
3699
4514
|
}
|
|
3700
|
-
const themesDir =
|
|
3701
|
-
const customThemesDir =
|
|
3702
|
-
const outputDir =
|
|
4515
|
+
const themesDir = join16(repoRoot, "themes");
|
|
4516
|
+
const customThemesDir = join16(repoRoot, "custom-themes");
|
|
4517
|
+
const outputDir = join16(repoRoot, "packages", "visor_themes");
|
|
3703
4518
|
const stockFiles = scanThemeDir2(themesDir);
|
|
3704
4519
|
const customFiles = scanThemeDir2(customThemesDir);
|
|
3705
4520
|
const allFiles = [...stockFiles, ...customFiles];
|
|
@@ -3720,7 +4535,7 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3720
4535
|
for (const filePath of allFiles) {
|
|
3721
4536
|
let yamlContent;
|
|
3722
4537
|
try {
|
|
3723
|
-
yamlContent =
|
|
4538
|
+
yamlContent = readFileSync17(filePath, "utf-8");
|
|
3724
4539
|
} catch {
|
|
3725
4540
|
errors.push(`Could not read: ${filePath}`);
|
|
3726
4541
|
continue;
|
|
@@ -3730,7 +4545,7 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3730
4545
|
data = generateThemeData5(yamlContent);
|
|
3731
4546
|
} catch (err) {
|
|
3732
4547
|
errors.push(
|
|
3733
|
-
`Failed to parse ${
|
|
4548
|
+
`Failed to parse ${basename4(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
3734
4549
|
);
|
|
3735
4550
|
continue;
|
|
3736
4551
|
}
|
|
@@ -3799,8 +4614,8 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3799
4614
|
return;
|
|
3800
4615
|
}
|
|
3801
4616
|
const slugs = processed.map((p) => p.slug);
|
|
3802
|
-
const libSrcDir =
|
|
3803
|
-
if (
|
|
4617
|
+
const libSrcDir = join16(outputDir, "lib", "src");
|
|
4618
|
+
if (existsSync14(libSrcDir)) {
|
|
3804
4619
|
rmSync(libSrcDir, { recursive: true, force: true });
|
|
3805
4620
|
}
|
|
3806
4621
|
const packageFiles = {
|
|
@@ -3812,16 +4627,16 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3812
4627
|
};
|
|
3813
4628
|
let totalFiles = 0;
|
|
3814
4629
|
for (const [relPath, content] of Object.entries(packageFiles)) {
|
|
3815
|
-
const absPath =
|
|
3816
|
-
mkdirSync6(
|
|
4630
|
+
const absPath = join16(outputDir, relPath);
|
|
4631
|
+
mkdirSync6(dirname7(absPath), { recursive: true });
|
|
3817
4632
|
writeFileSync10(absPath, content, "utf-8");
|
|
3818
4633
|
totalFiles++;
|
|
3819
4634
|
}
|
|
3820
4635
|
for (const { slug: slug2, tokenFiles } of processed) {
|
|
3821
|
-
const themeBaseDir =
|
|
4636
|
+
const themeBaseDir = join16(outputDir, "lib", "src", slug2);
|
|
3822
4637
|
for (const [relPath, content] of Object.entries(tokenFiles)) {
|
|
3823
|
-
const absPath =
|
|
3824
|
-
mkdirSync6(
|
|
4638
|
+
const absPath = join16(themeBaseDir, relPath);
|
|
4639
|
+
mkdirSync6(dirname7(absPath), { recursive: true });
|
|
3825
4640
|
writeFileSync10(absPath, content, "utf-8");
|
|
3826
4641
|
totalFiles++;
|
|
3827
4642
|
}
|
|
@@ -3843,11 +4658,11 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3843
4658
|
}
|
|
3844
4659
|
|
|
3845
4660
|
// src/commands/fonts-add.ts
|
|
3846
|
-
import { existsSync as
|
|
3847
|
-
import { resolve as
|
|
4661
|
+
import { existsSync as existsSync15, statSync as statSync7, readdirSync as readdirSync8, readFileSync as readFileSync18 } from "fs";
|
|
4662
|
+
import { resolve as resolve12, basename as basename5, extname as extname4 } from "path";
|
|
3848
4663
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
3849
4664
|
function deriveFamilySlug(filename) {
|
|
3850
|
-
const name =
|
|
4665
|
+
const name = basename5(filename, extname4(filename));
|
|
3851
4666
|
const WEIGHT_STYLE_SUFFIXES = /* @__PURE__ */ new Set([
|
|
3852
4667
|
"thin",
|
|
3853
4668
|
"hairline",
|
|
@@ -3888,28 +4703,28 @@ function deriveFamilySlug(filename) {
|
|
|
3888
4703
|
return parts.join("-").toLowerCase();
|
|
3889
4704
|
}
|
|
3890
4705
|
function collectWoff2Files(inputPath) {
|
|
3891
|
-
const resolved =
|
|
3892
|
-
if (!
|
|
4706
|
+
const resolved = resolve12(inputPath);
|
|
4707
|
+
if (!existsSync15(resolved)) {
|
|
3893
4708
|
throw new Error(`Path not found: ${resolved}`);
|
|
3894
4709
|
}
|
|
3895
|
-
const stat =
|
|
4710
|
+
const stat = statSync7(resolved);
|
|
3896
4711
|
if (stat.isFile()) {
|
|
3897
|
-
if (
|
|
4712
|
+
if (extname4(resolved).toLowerCase() !== ".woff2") {
|
|
3898
4713
|
throw new Error(
|
|
3899
|
-
`Invalid file format: ${
|
|
4714
|
+
`Invalid file format: ${basename5(resolved)}. Only .woff2 files are accepted.`
|
|
3900
4715
|
);
|
|
3901
4716
|
}
|
|
3902
4717
|
return [resolved];
|
|
3903
4718
|
}
|
|
3904
4719
|
if (stat.isDirectory()) {
|
|
3905
|
-
const files =
|
|
4720
|
+
const files = readdirSync8(resolved).filter((f) => extname4(f).toLowerCase() === ".woff2").map((f) => resolve12(resolved, f));
|
|
3906
4721
|
if (files.length === 0) {
|
|
3907
4722
|
throw new Error(
|
|
3908
4723
|
`No .woff2 files found in directory: ${resolved}`
|
|
3909
4724
|
);
|
|
3910
4725
|
}
|
|
3911
|
-
const nonWoff2Fonts =
|
|
3912
|
-
const ext =
|
|
4726
|
+
const nonWoff2Fonts = readdirSync8(resolved).filter((f) => {
|
|
4727
|
+
const ext = extname4(f).toLowerCase();
|
|
3913
4728
|
return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
|
|
3914
4729
|
});
|
|
3915
4730
|
return files.sort();
|
|
@@ -3917,12 +4732,12 @@ function collectWoff2Files(inputPath) {
|
|
|
3917
4732
|
throw new Error(`Path is neither a file nor a directory: ${resolved}`);
|
|
3918
4733
|
}
|
|
3919
4734
|
function getNonWoff2Fonts(inputPath) {
|
|
3920
|
-
const resolved =
|
|
3921
|
-
if (!
|
|
4735
|
+
const resolved = resolve12(inputPath);
|
|
4736
|
+
if (!existsSync15(resolved) || !statSync7(resolved).isDirectory()) {
|
|
3922
4737
|
return [];
|
|
3923
4738
|
}
|
|
3924
|
-
return
|
|
3925
|
-
const ext =
|
|
4739
|
+
return readdirSync8(resolved).filter((f) => {
|
|
4740
|
+
const ext = extname4(f).toLowerCase();
|
|
3926
4741
|
return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
|
|
3927
4742
|
});
|
|
3928
4743
|
}
|
|
@@ -3955,7 +4770,7 @@ function createR2Client(config) {
|
|
|
3955
4770
|
});
|
|
3956
4771
|
}
|
|
3957
4772
|
async function uploadFile(client, bucket, key, filePath) {
|
|
3958
|
-
const body =
|
|
4773
|
+
const body = readFileSync18(filePath);
|
|
3959
4774
|
await client.send(
|
|
3960
4775
|
new PutObjectCommand({
|
|
3961
4776
|
Bucket: bucket,
|
|
@@ -3970,9 +4785,9 @@ async function fontsAddCommand(inputPath, options) {
|
|
|
3970
4785
|
try {
|
|
3971
4786
|
const r2Config = getR2Config();
|
|
3972
4787
|
const files = collectWoff2Files(inputPath);
|
|
3973
|
-
const familySlug = options.family ?? deriveFamilySlug(
|
|
3974
|
-
const resolved =
|
|
3975
|
-
const nonWoff2 =
|
|
4788
|
+
const familySlug = options.family ?? deriveFamilySlug(basename5(files[0]));
|
|
4789
|
+
const resolved = resolve12(inputPath);
|
|
4790
|
+
const nonWoff2 = statSync7(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
|
|
3976
4791
|
if (!json) {
|
|
3977
4792
|
logger.heading("Visor Font Upload");
|
|
3978
4793
|
logger.info(`Organization: ${org}`);
|
|
@@ -3990,13 +4805,13 @@ async function fontsAddCommand(inputPath, options) {
|
|
|
3990
4805
|
const bucket = "visor-fonts";
|
|
3991
4806
|
const results = [];
|
|
3992
4807
|
for (const filePath of files) {
|
|
3993
|
-
const filename =
|
|
4808
|
+
const filename = basename5(filePath);
|
|
3994
4809
|
const key = buildS3Key(org, familySlug, filename);
|
|
3995
4810
|
if (!json) {
|
|
3996
4811
|
logger.info(`Uploading ${filename}...`);
|
|
3997
4812
|
}
|
|
3998
4813
|
await uploadFile(client, bucket, key, filePath);
|
|
3999
|
-
const size =
|
|
4814
|
+
const size = statSync7(filePath).size;
|
|
4000
4815
|
results.push({ file: filename, key, size });
|
|
4001
4816
|
if (!json) {
|
|
4002
4817
|
logger.success(`Uploaded: ${key} (${formatBytes(size)})`);
|
|
@@ -4046,7 +4861,7 @@ function formatBytes(bytes) {
|
|
|
4046
4861
|
// src/commands/doctor.ts
|
|
4047
4862
|
import * as fs from "fs";
|
|
4048
4863
|
import * as path from "path";
|
|
4049
|
-
import { execFileSync as
|
|
4864
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
4050
4865
|
async function doctorCommand(cwd, options, cliVersion) {
|
|
4051
4866
|
const checks = [];
|
|
4052
4867
|
const visorJsonPath = path.join(cwd, "visor.json");
|
|
@@ -4181,9 +4996,9 @@ async function doctorCommand(cwd, options, cliVersion) {
|
|
|
4181
4996
|
}
|
|
4182
4997
|
if (process.platform !== "win32") {
|
|
4183
4998
|
try {
|
|
4184
|
-
const globalPath =
|
|
4999
|
+
const globalPath = execFileSync4("which", ["visor"], { encoding: "utf-8" }).trim();
|
|
4185
5000
|
if (globalPath) {
|
|
4186
|
-
const globalVersionRaw =
|
|
5001
|
+
const globalVersionRaw = execFileSync4(globalPath, ["--version"], { encoding: "utf-8" }).trim();
|
|
4187
5002
|
const globalVersion = globalVersionRaw.split(/\s+/).pop() ?? "";
|
|
4188
5003
|
if (isOlder(globalVersion, cliVersion)) {
|
|
4189
5004
|
checks.push({
|
|
@@ -4259,27 +5074,27 @@ function findCssFiles(dir, maxDepth = 3) {
|
|
|
4259
5074
|
}
|
|
4260
5075
|
|
|
4261
5076
|
// src/utils/patterns.ts
|
|
4262
|
-
import { existsSync as
|
|
4263
|
-
import { join as
|
|
5077
|
+
import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync20 } from "fs";
|
|
5078
|
+
import { join as join18 } from "path";
|
|
4264
5079
|
import { parse as parseYAML } from "yaml";
|
|
4265
5080
|
function loadPatternsFromYaml(repoRoot) {
|
|
4266
|
-
const patternsDir =
|
|
4267
|
-
if (!
|
|
4268
|
-
const files =
|
|
5081
|
+
const patternsDir = join18(repoRoot, "patterns");
|
|
5082
|
+
if (!existsSync17(patternsDir)) return [];
|
|
5083
|
+
const files = readdirSync10(patternsDir).filter(
|
|
4269
5084
|
(f) => f.endsWith(".visor-pattern.yaml")
|
|
4270
5085
|
);
|
|
4271
5086
|
return files.map((file) => {
|
|
4272
|
-
const content =
|
|
5087
|
+
const content = readFileSync20(join18(patternsDir, file), "utf-8");
|
|
4273
5088
|
return parseYAML(content);
|
|
4274
5089
|
}).filter(Boolean);
|
|
4275
5090
|
}
|
|
4276
5091
|
function findRepoRoot2(startDir) {
|
|
4277
5092
|
let current = startDir;
|
|
4278
5093
|
while (true) {
|
|
4279
|
-
if (
|
|
5094
|
+
if (existsSync17(join18(current, "patterns"))) {
|
|
4280
5095
|
return current;
|
|
4281
5096
|
}
|
|
4282
|
-
const parent =
|
|
5097
|
+
const parent = join18(current, "..");
|
|
4283
5098
|
if (parent === current) return null;
|
|
4284
5099
|
current = parent;
|
|
4285
5100
|
}
|
|
@@ -4657,6 +5472,244 @@ Visor Tokens (${categoryLabel}) \u2014 ${tokens2.length} tokens
|
|
|
4657
5472
|
);
|
|
4658
5473
|
}
|
|
4659
5474
|
|
|
5475
|
+
// src/commands/migrate-token-substitution.ts
|
|
5476
|
+
import { readFileSync as readFileSync21, writeFileSync as writeFileSync11, readdirSync as readdirSync11, statSync as statSync8, existsSync as existsSync18 } from "fs";
|
|
5477
|
+
import { resolve as resolve13, join as join19, relative as relative3 } from "path";
|
|
5478
|
+
import { parse as parseYaml3 } from "yaml";
|
|
5479
|
+
import pc4 from "picocolors";
|
|
5480
|
+
var V7_ENTR_SUBSTITUTION_MAP = {
|
|
5481
|
+
"--panel": "--surface-card",
|
|
5482
|
+
"--panel-2": "--surface-interactive-default",
|
|
5483
|
+
"--panel-3": "--surface-interactive-active",
|
|
5484
|
+
"--text": "--text-primary",
|
|
5485
|
+
"--text-2": "--text-secondary",
|
|
5486
|
+
"--text-3": "--text-tertiary",
|
|
5487
|
+
"--text-4": "--text-tertiary",
|
|
5488
|
+
"--mint": "--accent-primary",
|
|
5489
|
+
"--mint-soft": "--surface-accent-subtle",
|
|
5490
|
+
"--warn": "--text-warning",
|
|
5491
|
+
"--warn-soft": "--surface-warning-subtle"
|
|
5492
|
+
};
|
|
5493
|
+
var BUILT_IN_SUBSTITUTION_MAPS = {
|
|
5494
|
+
"entr": V7_ENTR_SUBSTITUTION_MAP
|
|
5495
|
+
// future themes: "kaiah": KAIAH_SUBSTITUTION_MAP, etc.
|
|
5496
|
+
};
|
|
5497
|
+
var DEFAULT_THEME_ID = "entr";
|
|
5498
|
+
function readMapFromThemeFile(themeId, cwd) {
|
|
5499
|
+
const candidates = [
|
|
5500
|
+
join19(cwd, "themes", `${themeId}.visor.yaml`),
|
|
5501
|
+
join19(cwd, "custom-themes", `${themeId}.visor.yaml`),
|
|
5502
|
+
join19(cwd, "packages", "docs", "public", "themes", `${themeId}.visor.yaml`)
|
|
5503
|
+
];
|
|
5504
|
+
for (const candidate of candidates) {
|
|
5505
|
+
if (existsSync18(candidate)) {
|
|
5506
|
+
try {
|
|
5507
|
+
const raw = readFileSync21(candidate, "utf-8");
|
|
5508
|
+
const parsed = parseYaml3(raw);
|
|
5509
|
+
if (parsed?.migrate?.["token-substitution"]) {
|
|
5510
|
+
return parsed.migrate["token-substitution"];
|
|
5511
|
+
}
|
|
5512
|
+
} catch {
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
return void 0;
|
|
5517
|
+
}
|
|
5518
|
+
function resolveSubstitutionMap(themeId, cwd) {
|
|
5519
|
+
const fromYaml = readMapFromThemeFile(themeId, cwd);
|
|
5520
|
+
if (fromYaml) return fromYaml;
|
|
5521
|
+
return BUILT_IN_SUBSTITUTION_MAPS[themeId];
|
|
5522
|
+
}
|
|
5523
|
+
function applySubstitutionsToContent(content, map) {
|
|
5524
|
+
const substitutions = [];
|
|
5525
|
+
const lines2 = content.split("\n");
|
|
5526
|
+
const newLines = lines2.map((line, lineIndex) => {
|
|
5527
|
+
let newLine = line;
|
|
5528
|
+
for (const [from, to] of Object.entries(map)) {
|
|
5529
|
+
const pattern2 = new RegExp(`var\\(\\s*${escapeRegex(from)}\\s*(?:,[^)]*)?\\)`, "g");
|
|
5530
|
+
let match;
|
|
5531
|
+
while ((match = pattern2.exec(newLine)) !== null) {
|
|
5532
|
+
const column = match.index;
|
|
5533
|
+
const originalLine = line;
|
|
5534
|
+
const fullMatch = match[0];
|
|
5535
|
+
const replaced = fullMatch.replace(
|
|
5536
|
+
new RegExp(escapeRegex(from)),
|
|
5537
|
+
to
|
|
5538
|
+
);
|
|
5539
|
+
newLine = newLine.slice(0, match.index) + replaced + newLine.slice(match.index + fullMatch.length);
|
|
5540
|
+
substitutions.push({
|
|
5541
|
+
line: lineIndex + 1,
|
|
5542
|
+
column,
|
|
5543
|
+
from,
|
|
5544
|
+
to,
|
|
5545
|
+
originalLine,
|
|
5546
|
+
replacedLine: newLine
|
|
5547
|
+
});
|
|
5548
|
+
pattern2.lastIndex = match.index + replaced.length;
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
return newLine;
|
|
5552
|
+
});
|
|
5553
|
+
return { newContent: newLines.join("\n"), substitutions };
|
|
5554
|
+
}
|
|
5555
|
+
function escapeRegex(s) {
|
|
5556
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5557
|
+
}
|
|
5558
|
+
function collectCssFiles(dirPath) {
|
|
5559
|
+
const results = [];
|
|
5560
|
+
function walk2(current) {
|
|
5561
|
+
const entries = readdirSync11(current, { withFileTypes: true });
|
|
5562
|
+
for (const entry of entries) {
|
|
5563
|
+
const fullPath = join19(current, entry.name);
|
|
5564
|
+
if (entry.isDirectory()) {
|
|
5565
|
+
if (["node_modules", ".git", ".next", "dist", "build", ".cache"].includes(entry.name)) {
|
|
5566
|
+
continue;
|
|
5567
|
+
}
|
|
5568
|
+
walk2(fullPath);
|
|
5569
|
+
} else if (entry.isFile()) {
|
|
5570
|
+
const name = entry.name;
|
|
5571
|
+
if (name.endsWith(".module.css") || name.endsWith(".scss") || name.endsWith(".css")) {
|
|
5572
|
+
results.push(fullPath);
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
walk2(dirPath);
|
|
5578
|
+
return results;
|
|
5579
|
+
}
|
|
5580
|
+
function runSubstitutionPass(targetPath, map, themeId) {
|
|
5581
|
+
let files;
|
|
5582
|
+
const stat = statSync8(targetPath);
|
|
5583
|
+
if (stat.isFile()) {
|
|
5584
|
+
files = [targetPath];
|
|
5585
|
+
} else {
|
|
5586
|
+
files = collectCssFiles(targetPath);
|
|
5587
|
+
}
|
|
5588
|
+
const fileResults = [];
|
|
5589
|
+
let totalSubstitutions = 0;
|
|
5590
|
+
for (const file of files) {
|
|
5591
|
+
const content = readFileSync21(file, "utf-8");
|
|
5592
|
+
const { newContent, substitutions } = applySubstitutionsToContent(content, map);
|
|
5593
|
+
if (substitutions.length > 0) {
|
|
5594
|
+
fileResults.push({ file, substitutions, originalContent: content, newContent });
|
|
5595
|
+
totalSubstitutions += substitutions.length;
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
return {
|
|
5599
|
+
themeId,
|
|
5600
|
+
targetPath,
|
|
5601
|
+
filesScanned: files.length,
|
|
5602
|
+
filesChanged: fileResults.length,
|
|
5603
|
+
totalSubstitutions,
|
|
5604
|
+
files: fileResults
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5607
|
+
function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
|
|
5608
|
+
const themeId = options.themeId ?? DEFAULT_THEME_ID;
|
|
5609
|
+
const apply = options.apply ?? false;
|
|
5610
|
+
const dryRun = !apply;
|
|
5611
|
+
const map = resolveSubstitutionMap(themeId, cwd);
|
|
5612
|
+
if (!map) {
|
|
5613
|
+
const available = Object.keys(BUILT_IN_SUBSTITUTION_MAPS).join(", ");
|
|
5614
|
+
if (options.json) {
|
|
5615
|
+
console.log(JSON.stringify({
|
|
5616
|
+
success: false,
|
|
5617
|
+
error: `Unknown theme-id: "${themeId}". Available: ${available}`
|
|
5618
|
+
}));
|
|
5619
|
+
process.exit(1);
|
|
5620
|
+
}
|
|
5621
|
+
logger.error(`Unknown theme-id: "${themeId}". Available: ${available}`);
|
|
5622
|
+
process.exit(1);
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
const targetPath = resolve13(cwd, targetArg ?? ".");
|
|
5626
|
+
try {
|
|
5627
|
+
statSync8(targetPath);
|
|
5628
|
+
} catch {
|
|
5629
|
+
if (options.json) {
|
|
5630
|
+
console.log(JSON.stringify({
|
|
5631
|
+
success: false,
|
|
5632
|
+
error: `Target path not found: ${targetPath}`
|
|
5633
|
+
}));
|
|
5634
|
+
process.exit(1);
|
|
5635
|
+
}
|
|
5636
|
+
logger.error(`Target path not found: ${targetPath}`);
|
|
5637
|
+
process.exit(1);
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
const result = runSubstitutionPass(targetPath, map, themeId);
|
|
5641
|
+
if (result.filesChanged === 0) {
|
|
5642
|
+
if (options.json) {
|
|
5643
|
+
console.log(JSON.stringify({ success: true, ...result, message: "No V7 primitives found \u2014 already up to date." }));
|
|
5644
|
+
process.exit(0);
|
|
5645
|
+
}
|
|
5646
|
+
logger.success(`No V7 primitives found \u2014 ${result.filesScanned} file(s) scanned. Already up to date.`);
|
|
5647
|
+
process.exit(0);
|
|
5648
|
+
return;
|
|
5649
|
+
}
|
|
5650
|
+
if (options.json) {
|
|
5651
|
+
if (apply) {
|
|
5652
|
+
for (const f of result.files) {
|
|
5653
|
+
writeFileSync11(f.file, f.newContent, "utf-8");
|
|
5654
|
+
}
|
|
5655
|
+
console.log(JSON.stringify({
|
|
5656
|
+
success: true,
|
|
5657
|
+
applied: true,
|
|
5658
|
+
...result
|
|
5659
|
+
}));
|
|
5660
|
+
} else {
|
|
5661
|
+
console.log(JSON.stringify({
|
|
5662
|
+
success: true,
|
|
5663
|
+
dryRun: true,
|
|
5664
|
+
...result
|
|
5665
|
+
}));
|
|
5666
|
+
}
|
|
5667
|
+
process.exit(0);
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
const relTarget = relative3(cwd, targetPath) || ".";
|
|
5671
|
+
if (dryRun) {
|
|
5672
|
+
logger.heading(`visor migrate token-substitution \u2014 dry run`);
|
|
5673
|
+
logger.blank();
|
|
5674
|
+
logger.info(` Theme: ${pc4.bold(themeId)}`);
|
|
5675
|
+
logger.info(` Target: ${pc4.dim(relTarget)}`);
|
|
5676
|
+
logger.info(` Scanned: ${result.filesScanned} file(s)`);
|
|
5677
|
+
logger.blank();
|
|
5678
|
+
logger.heading(`Proposed changes (${result.filesChanged} file(s), ${result.totalSubstitutions} substitution(s)):`);
|
|
5679
|
+
logger.blank();
|
|
5680
|
+
for (const f of result.files) {
|
|
5681
|
+
const relFile = relative3(cwd, f.file);
|
|
5682
|
+
logger.info(pc4.bold(` ${relFile}`));
|
|
5683
|
+
for (const sub of f.substitutions) {
|
|
5684
|
+
logger.info(
|
|
5685
|
+
` line ${String(sub.line).padEnd(4)} ${pc4.red(`var(${sub.from})`)} \u2192 ${pc4.green(`var(${sub.to})`)}`
|
|
5686
|
+
);
|
|
5687
|
+
}
|
|
5688
|
+
logger.blank();
|
|
5689
|
+
}
|
|
5690
|
+
logger.warn(`Dry run \u2014 no files written. Re-run with ${pc4.bold("--apply")} to commit changes.`);
|
|
5691
|
+
} else {
|
|
5692
|
+
for (const f of result.files) {
|
|
5693
|
+
writeFileSync11(f.file, f.newContent, "utf-8");
|
|
5694
|
+
}
|
|
5695
|
+
logger.heading(`visor migrate token-substitution \u2014 applied`);
|
|
5696
|
+
logger.blank();
|
|
5697
|
+
logger.info(` Theme: ${pc4.bold(themeId)}`);
|
|
5698
|
+
logger.info(` Target: ${pc4.dim(relTarget)}`);
|
|
5699
|
+
logger.info(` Scanned: ${result.filesScanned} file(s)`);
|
|
5700
|
+
logger.blank();
|
|
5701
|
+
for (const f of result.files) {
|
|
5702
|
+
const relFile = relative3(cwd, f.file);
|
|
5703
|
+
logger.success(` ${relFile} (${f.substitutions.length} substitution(s))`);
|
|
5704
|
+
}
|
|
5705
|
+
logger.blank();
|
|
5706
|
+
logger.success(
|
|
5707
|
+
`Done \u2014 ${result.filesChanged} file(s) updated, ${result.totalSubstitutions} substitution(s) applied.`
|
|
5708
|
+
);
|
|
5709
|
+
}
|
|
5710
|
+
process.exit(0);
|
|
5711
|
+
}
|
|
5712
|
+
|
|
4660
5713
|
// src/index.ts
|
|
4661
5714
|
var program = new Command2();
|
|
4662
5715
|
program.name("visor").description("CLI for the Visor design system").version("0.3.0");
|
|
@@ -4793,4 +5846,16 @@ var tokens = program.command("tokens").description("Explore design tokens");
|
|
|
4793
5846
|
tokens.command("list").description("List all design tokens").option("--json", "output as JSON (for AI agents)").option("--category <category>", "filter by tier: primitives, semantic, adaptive").action(async (options) => {
|
|
4794
5847
|
await tokensListCommand(process.cwd(), options);
|
|
4795
5848
|
});
|
|
5849
|
+
var migrate = program.command("migrate").description("Migration helpers \u2014 mechanically transform source files during design-system adoption");
|
|
5850
|
+
migrate.command("token-substitution").description(
|
|
5851
|
+
"Apply the \xA73.1 V7-primitive \u2192 Visor-semantic substitution table across a target directory. Dry-run by default; use --apply to commit changes. Idempotent \u2014 running twice is a no-op."
|
|
5852
|
+
).argument("[path]", "path to file or directory to migrate (default: current directory)").option("--theme-id <id>", "theme whose substitution map to apply (default: entr)", "entr").option("--dry-run", "preview proposed changes without writing files (default when --apply is omitted)").option("--apply", "write changes to disk").option("--json", "output structured JSON (for AI agents)").action(
|
|
5853
|
+
(pathArg, options) => {
|
|
5854
|
+
migrateTokenSubstitutionCommand(pathArg, process.cwd(), {
|
|
5855
|
+
themeId: options.themeId,
|
|
5856
|
+
apply: options.apply,
|
|
5857
|
+
json: options.json
|
|
5858
|
+
});
|
|
5859
|
+
}
|
|
5860
|
+
);
|
|
4796
5861
|
program.parse();
|