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/README.md +17 -70
- package/dist/index.js +293 -198
- package/package.json +1 -1
- package/rules/00-anti-patterns.md +43 -7
- package/rules/01-modes.md +8 -7
- package/rules/02-tokens.md +182 -9
- package/rules/03-patterns.md +71 -4
- package/templates/COMMAND.md.tmpl +20 -2
- package/templates/command-metadata.json +2 -2
- package/tokens/brand.default.css +50 -2
- package/tokens/mode.editorial.css +44 -2
- package/tokens/mode.operator.css +16 -1
- package/tokens/mode.product.css +24 -2
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
|
|
5
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { join as
|
|
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
|
|
46
|
-
import {
|
|
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/
|
|
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
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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 =
|
|
524
|
+
const template = readFileSync4(join7(packageRoot2, "templates", "SKILL.md.tmpl"), "utf8");
|
|
508
525
|
const metadata = JSON.parse(
|
|
509
|
-
|
|
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
|
-
|
|
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 =
|
|
530
|
-
if (!
|
|
531
|
-
const template =
|
|
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 =
|
|
566
|
-
for (const file of
|
|
567
|
-
const body =
|
|
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:
|
|
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:
|
|
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 =
|
|
628
|
+
const abs = join7(installRoot, ...key.split("/"));
|
|
612
629
|
const isBlockFile = file.content.includes(BLOCK_START);
|
|
613
|
-
const content = isBlockFile &&
|
|
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
|
|
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
|
-
|
|
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 {
|
|
643
|
-
import { existsSync as
|
|
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
|
|
687
|
-
import { dirname as dirname5, join as
|
|
688
|
-
var INIT_MANIFEST_REL =
|
|
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 =
|
|
691
|
-
if (!
|
|
708
|
+
const path = join8(projectRoot, INIT_MANIFEST_REL);
|
|
709
|
+
if (!existsSync6(path)) return null;
|
|
692
710
|
try {
|
|
693
|
-
const raw = JSON.parse(
|
|
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 =
|
|
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
|
|
730
|
-
import { join as
|
|
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(
|
|
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 =
|
|
750
|
-
if (!
|
|
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 =
|
|
784
|
+
const bundleDir = join9(projectRoot, ...bundleDirRel.split("/"));
|
|
767
785
|
const mdcRel = ".cursor/rules/jig.mdc";
|
|
768
|
-
const mdcAbs =
|
|
786
|
+
const mdcAbs = join9(projectRoot, ...mdcRel.split("/"));
|
|
769
787
|
const legacyManifest = readLegacyManifest(bundleDir);
|
|
770
788
|
const files = [];
|
|
771
|
-
if (
|
|
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(
|
|
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
|
|
889
|
+
const writer = createWriter(installRoot, existing.files);
|
|
890
|
+
const files = writer.files;
|
|
872
891
|
const write = (key, content) => {
|
|
873
|
-
|
|
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 =
|
|
882
|
-
for (const file of
|
|
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}`)) +
|
|
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,
|
|
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 =
|
|
925
|
-
const current =
|
|
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),
|
|
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 =
|
|
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
|
-
|
|
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) +
|
|
964
|
-
|
|
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
|
|
988
|
+
import { join as join13 } from "path";
|
|
978
989
|
|
|
979
990
|
// src/rules/load.ts
|
|
980
|
-
import { readFileSync as
|
|
981
|
-
import { join as
|
|
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 =
|
|
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(
|
|
1067
|
+
parsed.push(...parseRules(readFileSync8(join11(rulesDir, file), "utf8"), file));
|
|
1057
1068
|
}
|
|
1058
|
-
const entries = validateIndex(JSON.parse(
|
|
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
|
|
1081
|
-
import { join as
|
|
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
|
|
1114
|
-
specs.push(...parseSpecs(
|
|
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])
|
|
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 (
|
|
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
|
-
`'${
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
`
|
|
1155
|
-
|
|
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
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
|
1169
|
-
import { join as
|
|
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
|
|
1174
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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
|
|
1477
|
-
import { join as
|
|
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({
|
|
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 =
|
|
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
|
|
2532
|
-
import { join as
|
|
2533
|
-
function
|
|
2534
|
-
const
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
const
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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 =
|
|
2649
|
+
var LEGACY_INDEX_REL = join17(".jig", "rules.index.json");
|
|
2555
2650
|
function resolveIndexPath(projectRoot) {
|
|
2556
|
-
const legacy =
|
|
2557
|
-
if (
|
|
2558
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
2630
|
-
import { dirname as
|
|
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
|
|
2634
|
-
import { join as
|
|
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(
|
|
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 =
|
|
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
|
|
2705
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
3004
|
-
import { basename, join as
|
|
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(
|
|
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 =
|
|
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 (!
|
|
3184
|
+
if (!existsSync11(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
|
|
3090
3185
|
try {
|
|
3091
|
-
const parsed = JSON.parse(
|
|
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 ?
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3156
|
-
|
|
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 =
|
|
3333
|
+
const brandAbsPath = join21(opts.projectRoot, ".jig", "tokens", brandFileName(projectSlug));
|
|
3239
3334
|
const configRelPath = "jig.config.json";
|
|
3240
|
-
const configAbsPath =
|
|
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 =
|
|
3364
|
+
const defaultCssRaw = readFileSync16(join21(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
|
|
3270
3365
|
const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
|
|
3271
|
-
|
|
3272
|
-
|
|
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
|
-
|
|
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(
|
|
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 ?
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3409
|
+
const modeSourceRaw = readFileSync16(join21(opts.packageRoot, "tokens", modeFileName), "utf8");
|
|
3315
3410
|
const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
|
|
3316
|
-
|
|
3317
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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("
|
|
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);
|