jig-ui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { readFileSync as readFileSync16 } from "fs";
5
+ import { readFileSync as readFileSync17 } from "fs";
6
6
  import { homedir } from "os";
7
- import { join as join21 } from "path";
7
+ import { join as join22 } from "path";
8
8
 
9
9
  // src/paths.ts
10
10
  import { existsSync } from "fs";
@@ -42,8 +42,8 @@ function isPublishedBuild(packageRoot2) {
42
42
  }
43
43
 
44
44
  // src/commands/install.ts
45
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "fs";
46
- import { dirname as dirname4, join as join6 } from "path";
45
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
46
+ import { join as join7 } from "path";
47
47
 
48
48
  // src/adapters/types.ts
49
49
  var COMMAND_DESCRIPTION = "Run a Jig design-system command: set the project up, review the UI against the rules, or refresh the install.";
@@ -477,6 +477,10 @@ function upsertBlock(existing, block) {
477
477
  return `${text.slice(0, insertAt)}${block}${text.slice(insertAt)}`;
478
478
  }
479
479
 
480
+ // src/install/writer.ts
481
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
482
+ import { dirname as dirname4, join as join6 } from "path";
483
+
480
484
  // src/install/line-endings.ts
481
485
  function dominantEnding(existing) {
482
486
  const crlf = (existing.match(/\r\n/g) ?? []).length;
@@ -490,23 +494,36 @@ function matchLineEndings(content, existing) {
490
494
  return target === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
491
495
  }
492
496
 
493
- // src/commands/install.ts
494
- var ASK_INSTRUCTION = "Ask the user directly in chat, one question at a time, and wait for an answer before continuing.";
497
+ // src/install/writer.ts
495
498
  function relKey(...parts) {
496
499
  return parts.join("/");
497
500
  }
498
- function writeFile(root, key, content, files) {
499
- const abs = join6(root, ...key.split("/"));
500
- mkdirSync3(dirname4(abs), { recursive: true });
501
- const existing = existsSync4(abs) ? readFileSync3(abs, "utf8") : void 0;
502
- const toWrite = matchLineEndings(content, existing);
503
- writeFileSync3(abs, toWrite, "utf8");
504
- files[key] = checksum(toWrite);
501
+ function createWriter(root, seed = {}) {
502
+ const files = { ...seed };
503
+ return {
504
+ files,
505
+ write(key, content) {
506
+ const abs = join6(root, ...key.split("/"));
507
+ mkdirSync3(dirname4(abs), { recursive: true });
508
+ const existing = existsSync4(abs) ? readFileSync3(abs, "utf8") : void 0;
509
+ const toWrite = matchLineEndings(content, existing);
510
+ writeFileAtomic(abs, toWrite);
511
+ files[key] = checksum(toWrite);
512
+ return key;
513
+ }
514
+ };
515
+ }
516
+ function bundleFiles(packageRoot2, dir) {
517
+ const ext = dir === "rules" ? ".md" : ".css";
518
+ return readdirSync2(join6(packageRoot2, dir)).filter((f) => f.endsWith(ext)).sort();
505
519
  }
520
+
521
+ // src/commands/install.ts
522
+ var ASK_INSTRUCTION = "Ask the user directly in chat, one question at a time, and wait for an answer before continuing.";
506
523
  function buildSkillBody(packageRoot2, rulesPath, version2) {
507
- const template = readFileSync3(join6(packageRoot2, "templates", "SKILL.md.tmpl"), "utf8");
524
+ const template = readFileSync4(join7(packageRoot2, "templates", "SKILL.md.tmpl"), "utf8");
508
525
  const metadata = JSON.parse(
509
- readFileSync3(join6(packageRoot2, "templates", "command-metadata.json"), "utf8")
526
+ readFileSync4(join7(packageRoot2, "templates", "command-metadata.json"), "utf8")
510
527
  );
511
528
  return render(template, {
512
529
  command_prefix: "/jig ",
@@ -523,13 +540,13 @@ function buildSkillBody(packageRoot2, rulesPath, version2) {
523
540
  }
524
541
  function buildCommandBody(packageRoot2, rulesPath, version2, argsPlaceholder) {
525
542
  const metadata = JSON.parse(
526
- readFileSync3(join6(packageRoot2, "templates", "command-metadata.json"), "utf8")
543
+ readFileSync4(join7(packageRoot2, "templates", "command-metadata.json"), "utf8")
527
544
  );
528
545
  const subcommands = Object.entries(metadata).filter(([, meta]) => meta.status === "available").map(([name]) => name).sort();
529
- const templatePath = join6(packageRoot2, "templates", "COMMAND.md.tmpl");
530
- if (!existsSync4(templatePath)) return { body: "", subcommands };
531
- const template = readFileSync3(templatePath, "utf8");
532
- const body = template.replace(/\{\{command_prefix\}\}/g, "/jig ").replace(/\{\{args_placeholder\}\}/g, argsPlaceholder).replace(/\{\{subcommand_list\}\}/g, subcommands.join(", ")).replace(/\{\{scripts_path\}\}/g, `npx jig-ui@${version2}`).replace(/\{\{rules_path\}\}/g, rulesPath);
546
+ const templatePath = join7(packageRoot2, "templates", "COMMAND.md.tmpl");
547
+ if (!existsSync5(templatePath)) return { body: "", subcommands };
548
+ const template = readFileSync4(templatePath, "utf8");
549
+ const body = template.replace(/\{\{command_prefix\}\}/g, "/jig ").replace(/\{\{args_placeholder\}\}/g, argsPlaceholder).replace(/\{\{subcommand_list\}\}/g, subcommands.join(", ")).replace(/\{\{scripts_path\}\}/g, `npx jig-ui@${version2}`).replace(/\{\{update_path\}\}/g, "npx jig-ui@latest").replace(/\{\{rules_path\}\}/g, rulesPath);
533
550
  return { body, subcommands };
534
551
  }
535
552
  function rulesPathFor(referenceDir, scope) {
@@ -562,9 +579,9 @@ function install(opts) {
562
579
  existingManifest = null;
563
580
  }
564
581
  const planned = [];
565
- const rulesDir = join6(opts.packageRoot, "rules");
566
- for (const file of readdirSync2(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
567
- const body = readFileSync3(join6(rulesDir, file), "utf8");
582
+ const rulesDir = join7(opts.packageRoot, "rules");
583
+ for (const file of bundleFiles(opts.packageRoot, "rules")) {
584
+ const body = readFileSync4(join7(rulesDir, file), "utf8");
568
585
  planned.push({
569
586
  key: relKey(referenceDir, "rules", file),
570
587
  content: vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + body,
@@ -580,13 +597,13 @@ function install(opts) {
580
597
  }
581
598
  planned.push({
582
599
  key: relKey(referenceDir, "rules.index.json"),
583
- content: readFileSync3(join6(opts.packageRoot, "rules.index.json"), "utf8"),
600
+ content: readFileSync4(join7(opts.packageRoot, "rules.index.json"), "utf8"),
584
601
  checkable: true
585
602
  });
586
603
  for (const src of ["LICENSE", "NOTICE"]) {
587
604
  planned.push({
588
605
  key: relKey(referenceDir, src),
589
- content: readFileSync3(join6(opts.packageRoot, src), "utf8"),
606
+ content: readFileSync4(join7(opts.packageRoot, src), "utf8"),
590
607
  checkable: false
591
608
  });
592
609
  }
@@ -608,12 +625,13 @@ function install(opts) {
608
625
  });
609
626
  for (const file of skillFiles) {
610
627
  const key = file.relPath;
611
- const abs = join6(installRoot, ...key.split("/"));
628
+ const abs = join7(installRoot, ...key.split("/"));
612
629
  const isBlockFile = file.content.includes(BLOCK_START);
613
- const content = isBlockFile && existsSync4(abs) ? upsertBlock(readFileSync3(abs, "utf8"), file.content) : file.content;
630
+ const content = isBlockFile && existsSync5(abs) ? upsertBlock(readFileSync4(abs, "utf8"), file.content) : file.content;
614
631
  planned.push({ key, content, checkable: !isBlockFile });
615
632
  }
616
- const files = {};
633
+ const writer = createWriter(installRoot);
634
+ const files = writer.files;
617
635
  const written = [];
618
636
  const skipped = [];
619
637
  for (const { key, content, checkable } of planned) {
@@ -623,7 +641,7 @@ function install(opts) {
623
641
  if (recorded) files[key] = recorded;
624
642
  continue;
625
643
  }
626
- writeFile(installRoot, key, content, files);
644
+ writer.write(key, content);
627
645
  written.push(key);
628
646
  }
629
647
  const manifest = {
@@ -639,8 +657,8 @@ function install(opts) {
639
657
  }
640
658
 
641
659
  // src/commands/update.ts
642
- import { dirname as dirname6, join as join9 } from "path";
643
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync4, writeFileSync as writeFileSync5 } from "fs";
660
+ import { join as join10 } from "path";
661
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
644
662
 
645
663
  // src/install/target.ts
646
664
  import { resolve } from "path";
@@ -683,14 +701,14 @@ function resolveAllInstalled(projectRoot, homeDir) {
683
701
  }
684
702
 
685
703
  // src/init/state.ts
686
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
687
- import { dirname as dirname5, join as join7 } from "path";
688
- var INIT_MANIFEST_REL = join7(".jig", "state.json");
704
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5 } from "fs";
705
+ import { dirname as dirname5, join as join8 } from "path";
706
+ var INIT_MANIFEST_REL = join8(".jig", "state.json");
689
707
  function readInitManifest(projectRoot) {
690
- const path = join7(projectRoot, INIT_MANIFEST_REL);
691
- if (!existsSync5(path)) return null;
708
+ const path = join8(projectRoot, INIT_MANIFEST_REL);
709
+ if (!existsSync6(path)) return null;
692
710
  try {
693
- const raw = JSON.parse(readFileSync4(path, "utf8"));
711
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
694
712
  if (typeof raw !== "object" || raw === null) return null;
695
713
  const { files, version: version2, modes } = raw;
696
714
  if (typeof files !== "object" || files === null) return null;
@@ -705,7 +723,7 @@ function readInitManifest(projectRoot) {
705
723
  }
706
724
  var MERGE_ATTEMPTS2 = 5;
707
725
  function writeInitManifest(projectRoot, m) {
708
- const path = join7(projectRoot, INIT_MANIFEST_REL);
726
+ const path = join8(projectRoot, INIT_MANIFEST_REL);
709
727
  mkdirSync4(dirname5(path), { recursive: true });
710
728
  for (let attempt = 0; attempt < MERGE_ATTEMPTS2; attempt++) {
711
729
  const onDisk = readInitManifest(projectRoot);
@@ -726,15 +744,15 @@ function isInitFileModified(projectRoot, relPath, m) {
726
744
  }
727
745
 
728
746
  // src/init/migrate.ts
729
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync } from "fs";
730
- import { join as join8 } from "path";
747
+ import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync } from "fs";
748
+ import { join as join9 } from "path";
731
749
  var LEGACY_RULE_RE = /^\d{2}-.*\.md$/;
732
750
  function isLegacyInstallArtifact(name) {
733
751
  return LEGACY_RULE_RE.test(name) || name === "rules.index.json" || name === "LICENSE" || name === "NOTICE" || name === "manifest.json";
734
752
  }
735
753
  function readLegacyManifest(dirAbs) {
736
754
  try {
737
- const raw = JSON.parse(readFileSync5(join8(dirAbs, "manifest.json"), "utf8"));
755
+ const raw = JSON.parse(readFileSync6(join9(dirAbs, "manifest.json"), "utf8"));
738
756
  if (raw && typeof raw === "object" && typeof raw.files === "object") {
739
757
  return raw;
740
758
  }
@@ -746,8 +764,8 @@ function classify(projectRoot, relPath, legacyManifest) {
746
764
  return legacyManifest && relPath in legacyManifest.files ? isModified(projectRoot, relPath, legacyManifest) : "unknown";
747
765
  }
748
766
  function detectLegacyRules(projectRoot) {
749
- const jigDir = join8(projectRoot, ".jig");
750
- if (!existsSync6(jigDir)) return { present: false, files: [] };
767
+ const jigDir = join9(projectRoot, ".jig");
768
+ if (!existsSync7(jigDir)) return { present: false, files: [] };
751
769
  const legacyManifest = readLegacyManifest(jigDir);
752
770
  let entries;
753
771
  try {
@@ -763,12 +781,12 @@ function detectLegacyRules(projectRoot) {
763
781
  }
764
782
  function detectLegacyCursorRules(projectRoot) {
765
783
  const bundleDirRel = ".cursor/rules/jig";
766
- const bundleDir = join8(projectRoot, ...bundleDirRel.split("/"));
784
+ const bundleDir = join9(projectRoot, ...bundleDirRel.split("/"));
767
785
  const mdcRel = ".cursor/rules/jig.mdc";
768
- const mdcAbs = join8(projectRoot, ...mdcRel.split("/"));
786
+ const mdcAbs = join9(projectRoot, ...mdcRel.split("/"));
769
787
  const legacyManifest = readLegacyManifest(bundleDir);
770
788
  const files = [];
771
- if (existsSync6(mdcAbs)) {
789
+ if (existsSync7(mdcAbs)) {
772
790
  files.push({ relPath: mdcRel, modified: classify(projectRoot, mdcRel, legacyManifest) });
773
791
  }
774
792
  let entries;
@@ -822,7 +840,7 @@ function removableLegacyFiles(report) {
822
840
  function removeLegacyFiles(projectRoot, relPaths) {
823
841
  for (const relPath of relPaths) {
824
842
  try {
825
- rmSync(join8(projectRoot, ...relPath.split("/")), { force: true });
843
+ rmSync(join9(projectRoot, ...relPath.split("/")), { force: true });
826
844
  } catch {
827
845
  }
828
846
  }
@@ -868,24 +886,19 @@ function updateTarget(opts, resolved) {
868
886
  const adapter = getAdapter(existing.agent);
869
887
  const updated = [];
870
888
  const skipped = [];
871
- const files = { ...existing.files };
889
+ const writer = createWriter(installRoot, existing.files);
890
+ const files = writer.files;
872
891
  const write = (key, content) => {
873
- const abs = join9(installRoot, ...key.split("/"));
874
- mkdirSync5(dirname6(abs), { recursive: true });
875
- const existing2 = existsSync7(abs) ? readFileSync6(abs, "utf8") : void 0;
876
- const toWrite = matchLineEndings(content, existing2);
877
- writeFileSync5(abs, toWrite, "utf8");
878
- files[key] = checksum(toWrite);
879
- updated.push(key);
892
+ updated.push(writer.write(key, content));
880
893
  };
881
- const rulesDir = join9(opts.packageRoot, "rules");
882
- for (const file of readdirSync4(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
894
+ const rulesDir = join10(opts.packageRoot, "rules");
895
+ for (const file of bundleFiles(opts.packageRoot, "rules")) {
883
896
  const key = relKey(referenceDir, "rules", file);
884
897
  if (isModified(installRoot, key, existing)) {
885
898
  skipped.push(key);
886
899
  continue;
887
900
  }
888
- write(key, vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + readFileSync6(join9(rulesDir, file), "utf8"));
901
+ write(key, vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + readFileSync7(join10(rulesDir, file), "utf8"));
889
902
  }
890
903
  for (const ref of referenceFiles(opts.packageRoot)) {
891
904
  const key = relKey(referenceDir, ...ref.relPath.split("/"));
@@ -899,7 +912,7 @@ function updateTarget(opts, resolved) {
899
912
  if (isModified(installRoot, indexKey, existing)) {
900
913
  skipped.push(indexKey);
901
914
  } else {
902
- write(indexKey, readFileSync6(join9(opts.packageRoot, "rules.index.json"), "utf8"));
915
+ write(indexKey, readFileSync7(join10(opts.packageRoot, "rules.index.json"), "utf8"));
903
916
  }
904
917
  const rulesPath = rulesPathFor(referenceDir, discoveredScope);
905
918
  const skillBody = buildSkillBody(opts.packageRoot, rulesPath, opts.version);
@@ -921,8 +934,8 @@ function updateTarget(opts, resolved) {
921
934
  const key = file.relPath;
922
935
  const isBlockFile = file.content.includes(BLOCK_START);
923
936
  if (isBlockFile) {
924
- const abs = join9(installRoot, ...key.split("/"));
925
- const current = existsSync7(abs) ? readFileSync6(abs, "utf8") : "";
937
+ const abs = join10(installRoot, ...key.split("/"));
938
+ const current = existsSync8(abs) ? readFileSync7(abs, "utf8") : "";
926
939
  write(key, upsertBlock(current, file.content));
927
940
  continue;
928
941
  }
@@ -933,7 +946,7 @@ function updateTarget(opts, resolved) {
933
946
  write(key, file.content);
934
947
  }
935
948
  for (const src of ["LICENSE", "NOTICE"]) {
936
- write(relKey(referenceDir, src), readFileSync6(join9(opts.packageRoot, src), "utf8"));
949
+ write(relKey(referenceDir, src), readFileSync7(join10(opts.packageRoot, src), "utf8"));
937
950
  }
938
951
  const manifest = {
939
952
  ...existing,
@@ -948,37 +961,35 @@ function updateTarget(opts, resolved) {
948
961
  function updateInitFiles(opts) {
949
962
  const updated = [];
950
963
  const skipped = [];
951
- const tokensDir = join9(opts.packageRoot, "tokens");
964
+ const tokensDir = join10(opts.packageRoot, "tokens");
952
965
  const initManifest = readInitManifest(opts.projectRoot);
953
966
  if (initManifest) {
954
967
  const initFiles = { ...initManifest.files };
955
968
  let initChanged = false;
956
- for (const file of readdirSync4(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
969
+ const writer = createWriter(opts.projectRoot, initFiles);
970
+ for (const file of bundleFiles(opts.packageRoot, "tokens")) {
957
971
  const key = relKey(".jig", "tokens", file);
958
972
  if (!(key in initManifest.files)) continue;
959
973
  if (isInitFileModified(opts.projectRoot, key, initManifest)) {
960
974
  skipped.push(key);
961
975
  continue;
962
976
  }
963
- const content = vendorHeader(file, opts.version, "css", null) + readFileSync6(join9(tokensDir, file), "utf8");
964
- const abs = join9(opts.projectRoot, ...key.split("/"));
965
- mkdirSync5(dirname6(abs), { recursive: true });
966
- writeFileSync5(abs, content, "utf8");
967
- initFiles[key] = checksum(content);
977
+ const content = vendorHeader(file, opts.version, "css", null) + readFileSync7(join10(tokensDir, file), "utf8");
978
+ updated.push(writer.write(key, content));
968
979
  initChanged = true;
969
- updated.push(key);
970
980
  }
981
+ Object.assign(initFiles, writer.files);
971
982
  if (initChanged) writeInitManifest(opts.projectRoot, { ...initManifest, version: opts.version, files: initFiles });
972
983
  }
973
984
  return { updated, skipped };
974
985
  }
975
986
 
976
987
  // src/commands/explain.ts
977
- import { join as join12 } from "path";
988
+ import { join as join13 } from "path";
978
989
 
979
990
  // src/rules/load.ts
980
- import { readFileSync as readFileSync7, readdirSync as readdirSync5 } from "fs";
981
- import { join as join10 } from "path";
991
+ import { readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
992
+ import { join as join11 } from "path";
982
993
 
983
994
  // src/rules/parse.ts
984
995
  var HEADING = /^###\s+([A-Z])-(\d+)\s+(.+?)\s*$/;
@@ -1050,12 +1061,12 @@ function validateIndex(entries) {
1050
1061
 
1051
1062
  // src/rules/load.ts
1052
1063
  function loadRules(rulesDir, indexPath) {
1053
- const files = readdirSync5(rulesDir).filter((f) => f.endsWith(".md")).sort();
1064
+ const files = readdirSync4(rulesDir).filter((f) => f.endsWith(".md")).sort();
1054
1065
  const parsed = [];
1055
1066
  for (const file of files) {
1056
- parsed.push(...parseRules(readFileSync7(join10(rulesDir, file), "utf8"), file));
1067
+ parsed.push(...parseRules(readFileSync8(join11(rulesDir, file), "utf8"), file));
1057
1068
  }
1058
- const entries = validateIndex(JSON.parse(readFileSync7(indexPath, "utf8")));
1069
+ const entries = validateIndex(JSON.parse(readFileSync8(indexPath, "utf8")));
1059
1070
  const byId = new Map(entries.map((e) => [e.id, e]));
1060
1071
  const missing = parsed.filter((r) => !byId.has(r.id)).map((r) => r.id);
1061
1072
  if (missing.length) {
@@ -1077,8 +1088,8 @@ function loadRules(rulesDir, indexPath) {
1077
1088
  }
1078
1089
 
1079
1090
  // src/rules/specs.ts
1080
- import { readFileSync as readFileSync8, readdirSync as readdirSync6 } from "fs";
1081
- import { join as join11 } from "path";
1091
+ import { readFileSync as readFileSync9, readdirSync as readdirSync5 } from "fs";
1092
+ import { join as join12 } from "path";
1082
1093
  var HEADING2 = /^##\s+([PM]-\d+)\s*(?:·\s*)?(.*?)\s*$/;
1083
1094
  function parseSpecs(markdown, sourceFile) {
1084
1095
  const lines = markdown.split("\n");
@@ -1110,68 +1121,121 @@ function parseSpecs(markdown, sourceFile) {
1110
1121
  }
1111
1122
  function loadSpecs(rulesDir) {
1112
1123
  const specs = [];
1113
- for (const file of readdirSync6(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
1114
- specs.push(...parseSpecs(readFileSync8(join11(rulesDir, file), "utf8"), file));
1124
+ for (const file of readdirSync5(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
1125
+ specs.push(...parseSpecs(readFileSync9(join12(rulesDir, file), "utf8"), file));
1115
1126
  }
1116
1127
  return specs;
1117
1128
  }
1118
1129
 
1119
1130
  // src/commands/explain.ts
1120
- var ID_SHAPE = /^([A-Z])-(\d+)$/;
1131
+ var ID_SHAPE = /^([A-Z])-?(\d+)$/;
1132
+ var SECTION_SHAPE = /^([A-Z])$/;
1133
+ var matches = (haystack, needle) => haystack.toLowerCase().includes(needle.toLowerCase());
1134
+ var byNumber = (a, b) => {
1135
+ const [as, an] = a.split("-");
1136
+ const [bs, bn] = b.split("-");
1137
+ return as === bs ? Number(an) - Number(bn) : as.localeCompare(bs);
1138
+ };
1139
+ function renderRule(rule) {
1140
+ return [
1141
+ `${rule.id} ${rule.title}`,
1142
+ "",
1143
+ `\u274C ${rule.wrong}`,
1144
+ `\u2705 ${rule.correction}`,
1145
+ "",
1146
+ ` ${rule.bucket} \xB7 ${rule.severity}${rule.detector ? ` \xB7 detector: ${rule.detector}` : ""}`,
1147
+ ` since ${rule.since} \xB7 ${rule.source}`
1148
+ ].join("\n");
1149
+ }
1150
+ function renderSpec(spec) {
1151
+ return [
1152
+ `${spec.id} ${spec.title}`,
1153
+ "",
1154
+ spec.body,
1155
+ "",
1156
+ ` specification \u2014 component anatomy and behaviour, not a single rule,`,
1157
+ ` so it carries no \u274C/\u2705 pair and no detector.`,
1158
+ ` ${spec.source}`
1159
+ ].join("\n");
1160
+ }
1121
1161
  function explain(opts) {
1122
1162
  const raw = opts.ruleId.trim().toUpperCase();
1163
+ const root = opts.packageRoot ?? assetRoot();
1164
+ const rulesDir = join13(root, "rules");
1165
+ const rules = loadRules(rulesDir, join13(root, "rules.index.json"));
1166
+ const specs = loadSpecs(rulesDir);
1167
+ const entries = [
1168
+ ...rules.map((r) => ({ id: r.id, title: r.title, text: `${r.wrong}
1169
+ ${r.correction}` })),
1170
+ ...specs.map((s) => ({ id: s.id, title: s.title, text: s.body }))
1171
+ ];
1172
+ if (opts.list) {
1173
+ const section = SECTION_SHAPE.exec(raw)?.[1];
1174
+ const shown = entries.filter((e) => !section || e.id.startsWith(`${section}-`)).sort((a, b) => byNumber(a.id, b.id));
1175
+ if (shown.length === 0) {
1176
+ throw new Error(`No section '${raw}'. Sections: ${sectionsOf(entries).join(", ")}.`);
1177
+ }
1178
+ return [
1179
+ section ? `Section ${section} \u2014 ${shown.length} entries` : `${shown.length} rules and specs`,
1180
+ "",
1181
+ ...shown.map((e) => `${e.id.padEnd(6)} ${e.title}`)
1182
+ ].join("\n");
1183
+ }
1123
1184
  const shape = ID_SHAPE.exec(raw);
1124
- if (!shape) {
1185
+ if (shape) {
1186
+ const id = `${shape[1]}-${shape[2]}`;
1187
+ const rule = rules.find((r) => r.id === id);
1188
+ if (rule) return renderRule(rule);
1189
+ const spec = specs.find((s) => s.id === id);
1190
+ if (spec) return renderSpec(spec);
1191
+ const siblings = entries.filter((e) => e.id.startsWith(`${shape[1]}-`)).map((e) => e.id).sort(byNumber);
1125
1192
  throw new Error(
1126
- `'${opts.ruleId.trim()}' is not a rule id. Ids look like 'C-19' or 'P-02' \u2014 a section letter, a hyphen, and a number.`
1193
+ siblings.length > 0 ? `No rule '${id}'. Section ${shape[1]} has: ${siblings.join(", ")}.` : `No rule '${id}', and no section '${shape[1]}' exists.`
1127
1194
  );
1128
1195
  }
1129
- const root = opts.packageRoot ?? assetRoot();
1130
- const rulesDir = join12(root, "rules");
1131
- const rules = loadRules(rulesDir, join12(root, "rules.index.json"));
1132
- const rule = rules.find((r) => r.id === raw);
1133
- if (rule) {
1134
- const lines = [
1135
- `${rule.id} ${rule.title}`,
1136
- "",
1137
- `\u274C ${rule.wrong}`,
1138
- `\u2705 ${rule.correction}`,
1139
- "",
1140
- ` ${rule.bucket} \xB7 ${rule.severity}${rule.detector ? ` \xB7 detector: ${rule.detector}` : ""}`,
1141
- ` since ${rule.since} \xB7 ${rule.source}`
1142
- ];
1143
- return lines.join("\n");
1196
+ if (raw === "") {
1197
+ throw new Error(
1198
+ `Nothing to explain. Give a rule id ('C-19'), a word to search for ('contrast'), or --list.`
1199
+ );
1144
1200
  }
1145
- const spec = loadSpecs(rulesDir).find((s) => s.id === raw);
1146
- if (spec) {
1147
- return [
1148
- `${spec.id} ${spec.title}`,
1149
- "",
1150
- spec.body,
1151
- "",
1152
- ` specification \u2014 component anatomy and behaviour, not a single rule,`,
1153
- ` so it carries no \u274C/\u2705 pair and no detector.`,
1154
- ` ${spec.source}`
1155
- ].join("\n");
1201
+ const needle = opts.ruleId.trim();
1202
+ const inTitle = entries.filter((e) => matches(e.title, needle) || matches(e.id, needle));
1203
+ const inBody = entries.filter((e) => !inTitle.includes(e) && matches(e.text, needle));
1204
+ const hits = [
1205
+ ...inTitle.sort((a, b) => byNumber(a.id, b.id)),
1206
+ ...inBody.sort((a, b) => byNumber(a.id, b.id))
1207
+ ];
1208
+ if (hits.length === 0) {
1209
+ throw new Error(
1210
+ `No rule matches '${needle}'. Try --list to see every id, or a broader word.`
1211
+ );
1156
1212
  }
1157
- const section = shape[1];
1158
- const siblings = [
1159
- ...rules.filter((r) => r.id.startsWith(`${section}-`)).map((r) => r.id),
1160
- ...loadSpecs(rulesDir).filter((s) => s.id.startsWith(`${section}-`)).map((s) => s.id)
1161
- ].sort((a, b) => Number(a.split("-")[1]) - Number(b.split("-")[1]));
1162
- throw new Error(
1163
- siblings.length > 0 ? `No rule '${raw}'. Section ${section} has: ${siblings.join(", ")}.` : `No rule '${raw}', and no section '${section}' exists.`
1164
- );
1213
+ if (hits.length === 1) {
1214
+ const rule = rules.find((r) => r.id === hits[0].id);
1215
+ return rule ? renderRule(rule) : renderSpec(specs.find((s) => s.id === hits[0].id));
1216
+ }
1217
+ return [
1218
+ // Title matches lead, so the list is ranked rather than sorted. Say so:
1219
+ // otherwise C-19 sitting above A-02 reads as a sorting bug.
1220
+ `${hits.length} entries match '${needle}' \u2014 title matches first:`,
1221
+ "",
1222
+ ...hits.map((e) => `${e.id.padEnd(6)} ${e.title}`),
1223
+ "",
1224
+ `Run 'jig explain <id>' for any of them.`
1225
+ ].join("\n");
1226
+ }
1227
+ function sectionsOf(entries) {
1228
+ return [...new Set(entries.map((e) => e.id.split("-")[0]))].sort();
1165
1229
  }
1166
1230
 
1167
1231
  // src/commands/check.ts
1168
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
1169
- import { join as join16 } from "path";
1232
+ import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
1233
+ import { join as join17 } from "path";
1170
1234
 
1171
1235
  // src/check/files.ts
1172
1236
  import { execFileSync } from "child_process";
1173
- import { readdirSync as readdirSync7, realpathSync, statSync } from "fs";
1174
- import { join as join13, relative, sep } from "path";
1237
+ import { readdirSync as readdirSync6, realpathSync, statSync } from "fs";
1238
+ import { join as join14, relative, sep } from "path";
1175
1239
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1176
1240
  "node_modules",
1177
1241
  "dist",
@@ -1230,13 +1294,13 @@ function isExcluded(relPath) {
1230
1294
  function walk(root, dir, out, seen) {
1231
1295
  let entries;
1232
1296
  try {
1233
- entries = readdirSync7(dir, { withFileTypes: true });
1297
+ entries = readdirSync6(dir, { withFileTypes: true });
1234
1298
  } catch {
1235
1299
  return;
1236
1300
  }
1237
1301
  for (const entry of entries) {
1238
1302
  if (EXCLUDE_DIRS.has(entry.name)) continue;
1239
- const abs = join13(dir, entry.name);
1303
+ const abs = join14(dir, entry.name);
1240
1304
  let isDir = entry.isDirectory();
1241
1305
  let isFile = entry.isFile();
1242
1306
  if (entry.isSymbolicLink()) {
@@ -1473,8 +1537,8 @@ function isStyleBearing(file) {
1473
1537
  }
1474
1538
 
1475
1539
  // src/check/run.ts
1476
- import { readFileSync as readFileSync9 } from "fs";
1477
- import { join as join14 } from "path";
1540
+ import { readFileSync as readFileSync10 } from "fs";
1541
+ import { join as join15 } from "path";
1478
1542
 
1479
1543
  // src/check/css.ts
1480
1544
  function maskComments(source) {
@@ -1523,7 +1587,12 @@ function splitRuleBlocks(source) {
1523
1587
  const ch = source[i];
1524
1588
  if (ch === "\n") line++;
1525
1589
  if (ch === "{") {
1526
- stack.push({ selectorStart: lastBoundary, bodyStart: i + 1, bodyStartLine: line });
1590
+ stack.push({
1591
+ selectorStart: lastBoundary,
1592
+ bodyStart: i + 1,
1593
+ bodyStartLine: line,
1594
+ isAtRule: cleanSelector(source.slice(lastBoundary, i)).startsWith("@")
1595
+ });
1527
1596
  lastBoundary = i + 1;
1528
1597
  } else if (ch === "}") {
1529
1598
  const top = stack.pop();
@@ -1531,7 +1600,8 @@ function splitRuleBlocks(source) {
1531
1600
  blocks.push({
1532
1601
  selector: cleanSelector(source.slice(top.selectorStart, top.bodyStart - 1)),
1533
1602
  body: source.slice(top.bodyStart, i),
1534
- bodyStartLine: top.bodyStartLine
1603
+ bodyStartLine: top.bodyStartLine,
1604
+ atRuleDepth: stack.filter((f) => f.isAtRule).length
1535
1605
  });
1536
1606
  }
1537
1607
  lastBoundary = i + 1;
@@ -2514,7 +2584,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
2514
2584
  if (applicable.length === 0) continue;
2515
2585
  let raw;
2516
2586
  try {
2517
- raw = readFileSync9(join14(projectRoot, file), "utf8");
2587
+ raw = readFileSync10(join15(projectRoot, file), "utf8");
2518
2588
  } catch {
2519
2589
  continue;
2520
2590
  }
@@ -2528,38 +2598,63 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
2528
2598
  }
2529
2599
 
2530
2600
  // src/check/tokens.ts
2531
- import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "fs";
2532
- import { join as join15 } from "path";
2533
- function loadTokenMap(projectRoot) {
2534
- const tokensDir = join15(projectRoot, ".jig", "tokens");
2535
- const tokens = {};
2536
- if (!existsSync8(tokensDir)) return tokens;
2537
- const files = readdirSync8(tokensDir).filter((f) => f.endsWith(".css")).sort();
2538
- for (const file of files) {
2539
- const source = readFileSync10(join15(tokensDir, file), "utf8");
2540
- for (const block of splitRuleBlocks(source)) {
2541
- if (block.body.includes("{")) continue;
2542
- if (block.selector.trim() !== ":root") continue;
2543
- const re = /--([\w-]+)\s*:\s*([^;]+);/g;
2544
- let m;
2545
- while (m = re.exec(block.body)) {
2546
- tokens[m[1]] = m[2].trim();
2547
- }
2601
+ import { existsSync as existsSync9, readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
2602
+ import { join as join16 } from "path";
2603
+ function rootDeclarations(source) {
2604
+ const out = {};
2605
+ for (const block of splitRuleBlocks(source)) {
2606
+ if (block.atRuleDepth !== 0) continue;
2607
+ if (block.body.includes("{")) continue;
2608
+ if (block.selector.trim() !== ":root") continue;
2609
+ const re = /--([\w-]+)\s*:\s*([^;]+);/g;
2610
+ let m;
2611
+ while (m = re.exec(block.body)) out[m[1]] = m[2].trim();
2612
+ }
2613
+ return out;
2614
+ }
2615
+ function loadTokenMap(projectRoot, consumerFiles = []) {
2616
+ const jig = {};
2617
+ const tokensDir = join16(projectRoot, ".jig", "tokens");
2618
+ if (existsSync9(tokensDir)) {
2619
+ for (const file of readdirSync7(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
2620
+ Object.assign(jig, rootDeclarations(readFileSync11(join16(tokensDir, file), "utf8")));
2548
2621
  }
2549
2622
  }
2623
+ const consumer = {};
2624
+ const ambiguous = /* @__PURE__ */ new Set();
2625
+ const vendored = join16(".jig", "tokens");
2626
+ for (const file of consumerFiles) {
2627
+ if (file.includes(vendored)) continue;
2628
+ let source;
2629
+ try {
2630
+ source = readFileSync11(join16(projectRoot, file), "utf8");
2631
+ } catch {
2632
+ continue;
2633
+ }
2634
+ for (const [name, value] of Object.entries(rootDeclarations(maskNonStyleRegions(source, file)))) {
2635
+ if (name in consumer && consumer[name] !== value) ambiguous.add(name);
2636
+ consumer[name] = value;
2637
+ }
2638
+ }
2639
+ const tokens = { ...consumer };
2640
+ for (const [name, value] of Object.entries(jig)) {
2641
+ if (name in consumer && consumer[name] !== value) ambiguous.add(name);
2642
+ tokens[name] = value;
2643
+ }
2644
+ for (const name of ambiguous) delete tokens[name];
2550
2645
  return tokens;
2551
2646
  }
2552
2647
 
2553
2648
  // src/commands/check.ts
2554
- var LEGACY_INDEX_REL = join16(".jig", "rules.index.json");
2649
+ var LEGACY_INDEX_REL = join17(".jig", "rules.index.json");
2555
2650
  function resolveIndexPath(projectRoot) {
2556
- const legacy = join16(projectRoot, LEGACY_INDEX_REL);
2557
- if (existsSync9(legacy)) return legacy;
2558
- return join16(assetRoot(), "rules.index.json");
2651
+ const legacy = join17(projectRoot, LEGACY_INDEX_REL);
2652
+ if (existsSync10(legacy)) return legacy;
2653
+ return join17(assetRoot(), "rules.index.json");
2559
2654
  }
2560
2655
  function resolveMode(projectRoot) {
2561
2656
  try {
2562
- const config = JSON.parse(readFileSync11(join16(projectRoot, "jig.config.json"), "utf8"));
2657
+ const config = JSON.parse(readFileSync12(join17(projectRoot, "jig.config.json"), "utf8"));
2563
2658
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
2564
2659
  if (modes.length === 1) return modes[0];
2565
2660
  return modes.length > 1 ? "mixed" : "unknown";
@@ -2576,23 +2671,23 @@ function summariseUnscanned(files) {
2576
2671
  }
2577
2672
  function countSpecs() {
2578
2673
  try {
2579
- return loadSpecs(join16(assetRoot(), "rules")).length;
2674
+ return loadSpecs(join17(assetRoot(), "rules")).length;
2580
2675
  } catch {
2581
2676
  return 0;
2582
2677
  }
2583
2678
  }
2584
2679
  function check(opts) {
2585
2680
  const indexPath = resolveIndexPath(opts.projectRoot);
2586
- const index = validateIndex(JSON.parse(readFileSync11(indexPath, "utf8")));
2587
- const tokens = loadTokenMap(opts.projectRoot);
2681
+ const index = validateIndex(JSON.parse(readFileSync12(indexPath, "utf8")));
2588
2682
  const selection = selectFiles(opts.projectRoot, opts.all);
2589
2683
  const { files } = selection;
2590
2684
  const bucketFilter = opts.ci ? (b) => b === "mechanical" : void 0;
2591
2685
  const stylesheets = selection.mode === "all" ? files : selectFiles(opts.projectRoot, true).files;
2686
+ const tokens = loadTokenMap(opts.projectRoot, stylesheets);
2592
2687
  const projectParticipates = stylesheets.some((f) => {
2593
2688
  if (!hasExtension(f, CSS_EXTENSIONS)) return false;
2594
2689
  try {
2595
- return participatesInTokenLayer(readFileSync11(join16(opts.projectRoot, f), "utf8"), tokens);
2690
+ return participatesInTokenLayer(readFileSync12(join17(opts.projectRoot, f), "utf8"), tokens);
2596
2691
  } catch {
2597
2692
  return false;
2598
2693
  }
@@ -2605,7 +2700,7 @@ function check(opts) {
2605
2700
  // told to add a token @import it has nowhere to put.
2606
2701
  cssFiles.length > 0 && cssFiles.every((f) => {
2607
2702
  try {
2608
- return !participatesInTokenLayer(readFileSync11(join16(opts.projectRoot, f), "utf8"), tokens);
2703
+ return !participatesInTokenLayer(readFileSync12(join17(opts.projectRoot, f), "utf8"), tokens);
2609
2704
  } catch {
2610
2705
  return true;
2611
2706
  }
@@ -2626,12 +2721,12 @@ function check(opts) {
2626
2721
  }
2627
2722
 
2628
2723
  // src/commands/init.ts
2629
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync6 } from "fs";
2630
- import { dirname as dirname7, join as join20 } from "path";
2724
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync16, writeFileSync as writeFileSync4 } from "fs";
2725
+ import { dirname as dirname6, join as join21 } from "path";
2631
2726
 
2632
2727
  // src/init/detect.ts
2633
- import { readFileSync as readFileSync12 } from "fs";
2634
- import { join as join17 } from "path";
2728
+ import { readFileSync as readFileSync13 } from "fs";
2729
+ import { join as join18 } from "path";
2635
2730
  var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
2636
2731
  var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
2637
2732
  var TAILWIND_V4_THEME_RE = /@theme\b/;
@@ -2648,7 +2743,7 @@ var FRAMEWORK_DEPS = [
2648
2743
  function detectFramework(projectRoot) {
2649
2744
  let pkg;
2650
2745
  try {
2651
- pkg = JSON.parse(readFileSync12(join17(projectRoot, "package.json"), "utf8"));
2746
+ pkg = JSON.parse(readFileSync13(join18(projectRoot, "package.json"), "utf8"));
2652
2747
  } catch {
2653
2748
  return void 0;
2654
2749
  }
@@ -2666,7 +2761,7 @@ function detect(projectRoot) {
2666
2761
  for (const f of cssFiles) {
2667
2762
  let source;
2668
2763
  try {
2669
- source = readFileSync12(join17(projectRoot, f), "utf8");
2764
+ source = readFileSync13(join18(projectRoot, f), "utf8");
2670
2765
  } catch {
2671
2766
  continue;
2672
2767
  }
@@ -2701,8 +2796,8 @@ function isIgnored(projectRoot, relPath) {
2701
2796
  }
2702
2797
 
2703
2798
  // src/init/derive.ts
2704
- import { readFileSync as readFileSync13 } from "fs";
2705
- import { join as join18 } from "path";
2799
+ import { readFileSync as readFileSync14 } from "fs";
2800
+ import { join as join19 } from "path";
2706
2801
  var DEFAULT_PROPOSAL = {
2707
2802
  h: 264,
2708
2803
  s: 0,
@@ -2735,7 +2830,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
2735
2830
  for (const file of cssFiles) {
2736
2831
  let source;
2737
2832
  try {
2738
- source = readFileSync13(join18(projectRoot, file), "utf8");
2833
+ source = readFileSync14(join19(projectRoot, file), "utf8");
2739
2834
  } catch {
2740
2835
  continue;
2741
2836
  }
@@ -2784,7 +2879,7 @@ function balancedBraceBody(source, openIdx) {
2784
2879
  function fromTailwindConfig(projectRoot, tailwindConfigFile) {
2785
2880
  let source;
2786
2881
  try {
2787
- source = readFileSync13(join18(projectRoot, tailwindConfigFile), "utf8");
2882
+ source = readFileSync14(join19(projectRoot, tailwindConfigFile), "utf8");
2788
2883
  } catch {
2789
2884
  return null;
2790
2885
  }
@@ -2840,7 +2935,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
2840
2935
  for (const file of cssFiles) {
2841
2936
  let source;
2842
2937
  try {
2843
- source = readFileSync13(join18(projectRoot, file), "utf8");
2938
+ source = readFileSync14(join19(projectRoot, file), "utf8");
2844
2939
  } catch {
2845
2940
  continue;
2846
2941
  }
@@ -3000,8 +3095,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
3000
3095
  }
3001
3096
 
3002
3097
  // src/init/project-name.ts
3003
- import { readFileSync as readFileSync14 } from "fs";
3004
- import { basename, join as join19 } from "path";
3098
+ import { readFileSync as readFileSync15 } from "fs";
3099
+ import { basename, join as join20 } from "path";
3005
3100
  function slugify(name) {
3006
3101
  const withoutScope = name.replace(/^@[^/]+\//, "");
3007
3102
  const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -3009,7 +3104,7 @@ function slugify(name) {
3009
3104
  }
3010
3105
  function deriveProjectSlug(projectRoot) {
3011
3106
  try {
3012
- const pkg = JSON.parse(readFileSync14(join19(projectRoot, "package.json"), "utf8"));
3107
+ const pkg = JSON.parse(readFileSync15(join20(projectRoot, "package.json"), "utf8"));
3013
3108
  if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
3014
3109
  } catch {
3015
3110
  }
@@ -3075,7 +3170,7 @@ function parseSurfaces(answer, fallback) {
3075
3170
  return parsed.length > 0 ? parsed : fallback;
3076
3171
  }
3077
3172
  function fileState(projectRoot, absPath, relPath, initManifest) {
3078
- const existsOnDisk = existsSync10(absPath);
3173
+ const existsOnDisk = existsSync11(absPath);
3079
3174
  const tracked = !!initManifest && relPath in initManifest.files;
3080
3175
  const modified = tracked && isInitFileModified(projectRoot, relPath, initManifest);
3081
3176
  return { existsOnDisk, tracked, modified };
@@ -3086,11 +3181,11 @@ function isValidSurfaceArray(v) {
3086
3181
  );
3087
3182
  }
3088
3183
  function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
3089
- if (!existsSync10(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
3184
+ if (!existsSync11(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
3090
3185
  try {
3091
- const parsed = JSON.parse(readFileSync15(configAbsPath, "utf8"));
3186
+ const parsed = JSON.parse(readFileSync16(configAbsPath, "utf8"));
3092
3187
  const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
3093
- const brandResolves = brandCandidate ? existsSync10(join20(projectRoot, ...brandCandidate.split("/"))) : false;
3188
+ const brandResolves = brandCandidate ? existsSync11(join21(projectRoot, ...brandCandidate.split("/"))) : false;
3094
3189
  return {
3095
3190
  brand: brandResolves ? brandCandidate : fallbackBrand,
3096
3191
  surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
@@ -3116,7 +3211,7 @@ function insertAfterCharset(content, insertion) {
3116
3211
  return content.slice(0, charsetLen) + insertion + content.slice(charsetLen);
3117
3212
  }
3118
3213
  function wireImport(absPath, brandImport, modeImport, mode) {
3119
- const content = existsSync10(absPath) ? readFileSync15(absPath, "utf8") : "";
3214
+ const content = existsSync11(absPath) ? readFileSync16(absPath, "utf8") : "";
3120
3215
  const brandImportLine = `@import "${brandImport}";`;
3121
3216
  const modeImportLine = `@import "${modeImport}";`;
3122
3217
  const brandPresent = content.includes(brandImportLine);
@@ -3129,7 +3224,7 @@ function wireImport(absPath, brandImport, modeImport, mode) {
3129
3224
  const previousMode = existingModeMatch[2];
3130
3225
  const next3 = content.slice(0, existingModeMatch.index) + `${modeImportLine}
3131
3226
  ` + content.slice(existingModeMatch.index + existingModeMatch[0].length);
3132
- writeFileSync6(absPath, next3, "utf8");
3227
+ writeFileSync4(absPath, next3, "utf8");
3133
3228
  return { status: "rewired", detail: `rewired mode: ${previousMode} \u2192 ${mode}` };
3134
3229
  }
3135
3230
  const brandIdx = content.indexOf(brandImportLine);
@@ -3137,7 +3232,7 @@ function wireImport(absPath, brandImport, modeImport, mode) {
3137
3232
  if (content[insertAt] === "\r") insertAt++;
3138
3233
  if (content[insertAt] === "\n") insertAt++;
3139
3234
  const next2 = content.slice(0, insertAt) + modeImportLine + "\n" + content.slice(insertAt);
3140
- writeFileSync6(absPath, next2, "utf8");
3235
+ writeFileSync4(absPath, next2, "utf8");
3141
3236
  return { status: "rewired", detail: `added missing mode import: ${mode}` };
3142
3237
  }
3143
3238
  const lines = `${brandImportLine}
@@ -3152,8 +3247,8 @@ ${modeImportLine}
3152
3247
  next = insertAfterCharset(content, content ? `${lines}
3153
3248
  ` : lines);
3154
3249
  }
3155
- mkdirSync6(dirname7(absPath), { recursive: true });
3156
- writeFileSync6(absPath, next, "utf8");
3250
+ mkdirSync5(dirname6(absPath), { recursive: true });
3251
+ writeFileSync4(absPath, next, "utf8");
3157
3252
  return { status: "wired" };
3158
3253
  }
3159
3254
  async function init(opts) {
@@ -3235,9 +3330,9 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
3235
3330
  const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
3236
3331
  const projectSlug = deriveProjectSlug(opts.projectRoot);
3237
3332
  const brandRelPath = relKey(".jig", "tokens", brandFileName(projectSlug));
3238
- const brandAbsPath = join20(opts.projectRoot, ".jig", "tokens", brandFileName(projectSlug));
3333
+ const brandAbsPath = join21(opts.projectRoot, ".jig", "tokens", brandFileName(projectSlug));
3239
3334
  const configRelPath = "jig.config.json";
3240
- const configAbsPath = join20(opts.projectRoot, configRelPath);
3335
+ const configAbsPath = join21(opts.projectRoot, configRelPath);
3241
3336
  const initManifest = readInitManifest(opts.projectRoot);
3242
3337
  const files = { ...initManifest?.files ?? {} };
3243
3338
  const brandState = fileState(opts.projectRoot, brandAbsPath, brandRelPath, initManifest);
@@ -3266,44 +3361,44 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
3266
3361
  if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
3267
3362
  }
3268
3363
  if (brandAction === "written") {
3269
- const defaultCssRaw = readFileSync15(join20(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
3364
+ const defaultCssRaw = readFileSync16(join21(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
3270
3365
  const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
3271
- mkdirSync6(dirname7(brandAbsPath), { recursive: true });
3272
- writeFileSync6(brandAbsPath, content, "utf8");
3366
+ mkdirSync5(dirname6(brandAbsPath), { recursive: true });
3367
+ writeFileSync4(brandAbsPath, content, "utf8");
3273
3368
  files[brandRelPath] = checksum(content);
3274
3369
  }
3275
3370
  if (configAction === "written") {
3276
3371
  const config = { brand: brandRelPath, surfaces };
3277
3372
  const content = `${JSON.stringify(config, null, 2)}
3278
3373
  `;
3279
- writeFileSync6(configAbsPath, content, "utf8");
3374
+ writeFileSync4(configAbsPath, content, "utf8");
3280
3375
  files[configRelPath] = checksum(content);
3281
3376
  } else if (configAction === "merged") {
3282
3377
  let existing = {};
3283
3378
  try {
3284
- existing = JSON.parse(readFileSync15(configAbsPath, "utf8"));
3379
+ existing = JSON.parse(readFileSync16(configAbsPath, "utf8"));
3285
3380
  } catch {
3286
3381
  existing = {};
3287
3382
  }
3288
3383
  const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
3289
- const brandStillResolves = existingBrand ? existsSync10(join20(opts.projectRoot, ...existingBrand.split("/"))) : false;
3384
+ const brandStillResolves = existingBrand ? existsSync11(join21(opts.projectRoot, ...existingBrand.split("/"))) : false;
3290
3385
  const merged = {
3291
3386
  brand: brandStillResolves ? existingBrand : brandRelPath,
3292
3387
  surfaces: Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : surfaces
3293
3388
  };
3294
3389
  const content = `${JSON.stringify(merged, null, 2)}
3295
3390
  `;
3296
- writeFileSync6(configAbsPath, content, "utf8");
3391
+ writeFileSync4(configAbsPath, content, "utf8");
3297
3392
  files[configRelPath] = checksum(content);
3298
3393
  }
3299
3394
  const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
3300
3395
  const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
3301
- const wiringBrandAbsPath = join20(opts.projectRoot, ...effectiveConfig.brand.split("/"));
3396
+ const wiringBrandAbsPath = join21(opts.projectRoot, ...effectiveConfig.brand.split("/"));
3302
3397
  const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
3303
3398
  const modeAbsPaths = {};
3304
3399
  for (const mode of declaredModes) {
3305
3400
  const modeFileName = `mode.${mode}.css`;
3306
- const modeAbsPath2 = join20(opts.projectRoot, ".jig", "tokens", modeFileName);
3401
+ const modeAbsPath2 = join21(opts.projectRoot, ".jig", "tokens", modeFileName);
3307
3402
  modeAbsPaths[mode] = modeAbsPath2;
3308
3403
  const modeRelPath = relKey(".jig", "tokens", modeFileName);
3309
3404
  const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
@@ -3311,10 +3406,10 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
3311
3406
  if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
3312
3407
  else if (modeState.modified) modeAction = "skipped-edited";
3313
3408
  if (modeAction === "written") {
3314
- const modeSourceRaw = readFileSync15(join20(opts.packageRoot, "tokens", modeFileName), "utf8");
3409
+ const modeSourceRaw = readFileSync16(join21(opts.packageRoot, "tokens", modeFileName), "utf8");
3315
3410
  const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
3316
- mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
3317
- writeFileSync6(modeAbsPath2, content, "utf8");
3411
+ mkdirSync5(dirname6(modeAbsPath2), { recursive: true });
3412
+ writeFileSync4(modeAbsPath2, content, "utf8");
3318
3413
  files[modeRelPath] = checksum(content);
3319
3414
  } else {
3320
3415
  log(` ${modeRelPath} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone (re-run 'jig update' to refresh it).`);
@@ -3325,13 +3420,13 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
3325
3420
  const wireTarget = findWireTarget(detection);
3326
3421
  let wiring;
3327
3422
  if (wireTarget) {
3328
- const targetAbsDir = dirname7(join20(opts.projectRoot, wireTarget));
3423
+ const targetAbsDir = dirname6(join21(opts.projectRoot, wireTarget));
3329
3424
  const brandImport = relativeImportPath(targetAbsDir, wiringBrandAbsPath);
3330
3425
  const modeImport = relativeImportPath(targetAbsDir, modeAbsPath);
3331
3426
  const snippet = `@import "${brandImport}";
3332
3427
  @import "${modeImport}";`;
3333
3428
  try {
3334
- const outcome = wireImport(join20(opts.projectRoot, wireTarget), brandImport, modeImport, primaryMode);
3429
+ const outcome = wireImport(join21(opts.projectRoot, wireTarget), brandImport, modeImport, primaryMode);
3335
3430
  wiring = { target: wireTarget, status: outcome.status, snippet };
3336
3431
  const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
3337
3432
  log(`
@@ -3356,7 +3451,7 @@ Could not write to ${wireTarget}: ${err.message}`);
3356
3451
  if (pasteTargets.length > 0) {
3357
3452
  log("Paste the block for whichever of these is your global stylesheet:");
3358
3453
  for (const target of pasteTargets) {
3359
- const dir = dirname7(join20(opts.projectRoot, target));
3454
+ const dir = dirname6(join21(opts.projectRoot, target));
3360
3455
  const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
3361
3456
  @import "${relativeImportPath(dir, modeAbsPath)}";`;
3362
3457
  log(`
@@ -3397,7 +3492,7 @@ Could not write to ${wireTarget}: ${err.message}`);
3397
3492
 
3398
3493
  // src/index.ts
3399
3494
  var packageRoot = getPackageRoot();
3400
- var { version } = JSON.parse(readFileSync16(join21(packageRoot, "package.json"), "utf8"));
3495
+ var { version } = JSON.parse(readFileSync17(join22(packageRoot, "package.json"), "utf8"));
3401
3496
  function warnIfUnpublishedPin() {
3402
3497
  if (isPublishedBuild(packageRoot)) return;
3403
3498
  console.warn(
@@ -3434,9 +3529,9 @@ program.command("install").description("Install Jig rules and the agent skill fi
3434
3529
  process.exit(1);
3435
3530
  }
3436
3531
  });
3437
- program.command("explain").description("Print a rule's full text, its correction, and the version it was introduced in.").argument("<rule-id>", "a rule id, e.g. 'C-19' or 'P-02'").action((ruleId) => {
3532
+ program.command("explain").description("Explain a rule, or find the rules you cannot name.").argument("[query]", "a rule id ('C-19'), a word to search for ('contrast'), or a section letter with --list").option("--list", "list every rule id and title, or one section with a section letter").action((query, options) => {
3438
3533
  try {
3439
- console.log(explain({ ruleId, version }));
3534
+ console.log(explain({ ruleId: query ?? "", version, list: options.list }));
3440
3535
  } catch (err) {
3441
3536
  console.error(err.message);
3442
3537
  process.exit(1);