@lovrozagar/grunt 0.9.5 → 0.10.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/.agents/skills/jev/SKILL.md +18 -0
- package/.claude/skills/jev/SKILL.md +18 -0
- package/.grok/hooks/orchestrate-parent.js +9 -1
- package/.grok/skills/jev/SKILL.md +18 -0
- package/.rulesync/reference/INDEX.md +3 -1
- package/.rulesync/reference/hooks.md +1 -1
- package/.rulesync/reference/jev.md +56 -0
- package/.rulesync/reference/law.md +1 -1
- package/.rulesync/reference/refs-map.md +1 -0
- package/.rulesync/reference/setup.md +3 -1
- package/.rulesync/reference/skills-map.md +1 -0
- package/.rulesync/skills/jev/SKILL.md +15 -0
- package/CHANGELOG.md +7 -0
- package/README.md +19 -7
- package/cli/grunt.mjs +60 -8
- package/cli/grunt.test.ts +122 -1
- package/cli/init.mjs +28 -6
- package/cli/init.test.ts +104 -5
- package/cli/prompt.mjs +1 -0
- package/cli/prompt.test.ts +29 -0
- package/package.json +4 -1
- package/scripts/board.mjs +390 -0
- package/scripts/doctor.mjs +38 -13
- package/scripts/feature-packs.mjs +123 -0
- package/scripts/jev.mjs +355 -0
- package/scripts/prompt.mjs +4 -0
- package/scripts/session-map.mjs +3 -1
- package/scripts/setup.mjs +85 -47
package/cli/grunt.test.ts
CHANGED
|
@@ -14,7 +14,9 @@ const detectPackageManager = vi.hoisted(() => vi.fn());
|
|
|
14
14
|
const actualDetect = vi.hoisted(() => ({ fn: null as null | ((...args: unknown[]) => unknown) }));
|
|
15
15
|
const isInteractive = vi.hoisted(() => vi.fn(() => false));
|
|
16
16
|
const select = vi.hoisted(() => vi.fn());
|
|
17
|
+
const multiselect = vi.hoisted(() => vi.fn(async () => []));
|
|
17
18
|
const confirm = vi.hoisted(() => vi.fn());
|
|
19
|
+
const selectedPacks = vi.hoisted(() => vi.fn(() => [] as string[]));
|
|
18
20
|
const spinner = vi.hoisted(() =>
|
|
19
21
|
vi.fn(() => ({ start: vi.fn(), stop: vi.fn() })),
|
|
20
22
|
);
|
|
@@ -46,10 +48,15 @@ vi.mock("../scripts/package-manager.mjs", async (importOriginal) => {
|
|
|
46
48
|
vi.mock("./prompt.mjs", () => ({
|
|
47
49
|
isInteractive,
|
|
48
50
|
select,
|
|
51
|
+
multiselect,
|
|
49
52
|
confirm,
|
|
50
53
|
spinner,
|
|
51
54
|
bailIfCancel,
|
|
52
55
|
}));
|
|
56
|
+
vi.mock("../scripts/feature-packs.mjs", async (importOriginal) => {
|
|
57
|
+
const actual = await importOriginal<typeof import("../scripts/feature-packs.mjs")>();
|
|
58
|
+
return { ...actual, selectedPacks };
|
|
59
|
+
});
|
|
53
60
|
|
|
54
61
|
import {
|
|
55
62
|
PACKAGE_MANAGER_ASK,
|
|
@@ -68,7 +75,7 @@ Commands:
|
|
|
68
75
|
sync-globals run grunt:sync:globals (dry-run; --apply to write)
|
|
69
76
|
purge-mcps run grunt:purge:global-mcps (dry-run; --apply to write)
|
|
70
77
|
doctor run grunt:doctor
|
|
71
|
-
setup run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
|
|
78
|
+
setup run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser, jev)
|
|
72
79
|
upgrade Self-update to the latest grunt, then re-init: copy owned files, prune retired names, print reserved skills
|
|
73
80
|
map [dir] Code-only folder map (git-tracked, no files); dir for depth
|
|
74
81
|
help Show this help
|
|
@@ -82,6 +89,7 @@ Flags:
|
|
|
82
89
|
--apply Write for sync-globals / purge-mcps
|
|
83
90
|
--host <id> sync-globals host
|
|
84
91
|
--pm <name> npm | yarn | pnpm | bun (else lockfile, then how grunt was launched, then ask)
|
|
92
|
+
--packs <list> Optional packs: google-workspace,listen,speak,clasp or none
|
|
85
93
|
`;
|
|
86
94
|
|
|
87
95
|
const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -125,6 +133,10 @@ describe("start", () => {
|
|
|
125
133
|
spinner.mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() }));
|
|
126
134
|
bailIfCancel.mockReset();
|
|
127
135
|
bailIfCancel.mockImplementation((v) => v);
|
|
136
|
+
multiselect.mockReset();
|
|
137
|
+
multiselect.mockResolvedValue([]);
|
|
138
|
+
selectedPacks.mockReset();
|
|
139
|
+
selectedPacks.mockReturnValue([]);
|
|
128
140
|
});
|
|
129
141
|
|
|
130
142
|
afterEach(() => {
|
|
@@ -659,6 +671,46 @@ describe("parseArgv --pm", () => {
|
|
|
659
671
|
});
|
|
660
672
|
});
|
|
661
673
|
|
|
674
|
+
it("TTY upgrade asks packs only", async () => {
|
|
675
|
+
isInteractive.mockReturnValue(true);
|
|
676
|
+
confirm.mockClear();
|
|
677
|
+
multiselect.mockClear();
|
|
678
|
+
init.mockClear();
|
|
679
|
+
multiselect.mockResolvedValue(["listen", "speak"]);
|
|
680
|
+
process.argv = ["node", "grunt", "upgrade", "--no-self-update"];
|
|
681
|
+
await start();
|
|
682
|
+
expect(confirm).not.toHaveBeenCalled();
|
|
683
|
+
expect(multiselect).toHaveBeenCalledWith(
|
|
684
|
+
expect.objectContaining({ message: "Optional packs", required: false, initialValues: [] }),
|
|
685
|
+
);
|
|
686
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
687
|
+
skipGlobals: false,
|
|
688
|
+
packageManager: "npm",
|
|
689
|
+
packs: ["listen", "speak"],
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("TTY init pre-checks a saved features file and skips the pack prompt when --packs is set", async () => {
|
|
694
|
+
isInteractive.mockReturnValue(true);
|
|
695
|
+
selectedPacks.mockReturnValue(["speak"]);
|
|
696
|
+
confirm.mockResolvedValue(true);
|
|
697
|
+
process.argv = ["node", "grunt", "init"];
|
|
698
|
+
await start();
|
|
699
|
+
expect(multiselect).toHaveBeenCalledWith(
|
|
700
|
+
expect.objectContaining({ initialValues: ["speak"], required: false }),
|
|
701
|
+
);
|
|
702
|
+
multiselect.mockClear();
|
|
703
|
+
confirm.mockClear();
|
|
704
|
+
init.mockClear();
|
|
705
|
+
process.argv = ["node", "grunt", "init", "--packs", "listen"];
|
|
706
|
+
await start();
|
|
707
|
+
expect(multiselect).not.toHaveBeenCalled();
|
|
708
|
+
expect(init).toHaveBeenCalledWith(
|
|
709
|
+
process.cwd(),
|
|
710
|
+
expect.objectContaining({ packs: ["listen"], applyGlobals: true }),
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
|
|
662
714
|
it("missing, empty, flag, and unknown are pmError", () => {
|
|
663
715
|
expect(parseArgv(["generate", "--pm"])).toMatchObject({ pmError: true });
|
|
664
716
|
expect(parseArgv(["generate", "--pm="])).toMatchObject({ pmError: true });
|
|
@@ -666,3 +718,72 @@ describe("parseArgv --pm", () => {
|
|
|
666
718
|
expect(parseArgv(["generate", "--pm", "deno"])).toMatchObject({ pmError: true });
|
|
667
719
|
});
|
|
668
720
|
});
|
|
721
|
+
|
|
722
|
+
describe("parseArgv --packs", () => {
|
|
723
|
+
it("pair, equals, and none", () => {
|
|
724
|
+
expect(parseArgv(["init", "--packs", "speak,listen"])).toMatchObject({
|
|
725
|
+
cmd: "init",
|
|
726
|
+
packs: ["listen", "speak"],
|
|
727
|
+
packsError: false,
|
|
728
|
+
});
|
|
729
|
+
expect(parseArgv(["init", "--packs=google-workspace,clasp"])).toMatchObject({
|
|
730
|
+
packs: ["google-workspace", "clasp"],
|
|
731
|
+
packsError: false,
|
|
732
|
+
});
|
|
733
|
+
expect(parseArgv(["init", "--packs", "none"])).toMatchObject({ packs: [], packsError: false });
|
|
734
|
+
expect(parseArgv(["init", "--packs="])).toMatchObject({ packs: [], packsError: false });
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it("missing, flag, and unknown are packsError", () => {
|
|
738
|
+
expect(parseArgv(["init", "--packs"])).toMatchObject({ packsError: true });
|
|
739
|
+
expect(parseArgv(["init", "--packs", "--yes"])).toMatchObject({ packsError: true });
|
|
740
|
+
expect(parseArgv(["init", "--packs", "nope"])).toMatchObject({ packsError: true });
|
|
741
|
+
expect(parseArgv(["init", "--packs=nope"])).toMatchObject({ packsError: true });
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("unknown --packs writes usage", async () => {
|
|
745
|
+
const lines: string[] = [];
|
|
746
|
+
const stdoutWrite = process.stdout.write;
|
|
747
|
+
const exitCode = process.exitCode;
|
|
748
|
+
const argv = process.argv.slice();
|
|
749
|
+
process.stdout.write = ((buf: string | Uint8Array) => {
|
|
750
|
+
lines.push(String(buf));
|
|
751
|
+
return true;
|
|
752
|
+
}) as typeof process.stdout.write;
|
|
753
|
+
process.exitCode = 0;
|
|
754
|
+
process.argv = ["node", "grunt", "init", "--packs", "nope"];
|
|
755
|
+
init.mockClear();
|
|
756
|
+
try {
|
|
757
|
+
await start();
|
|
758
|
+
expect(lines.join("")).toBe(USAGE);
|
|
759
|
+
expect(process.exitCode).toBe(1);
|
|
760
|
+
expect(init).not.toHaveBeenCalled();
|
|
761
|
+
} finally {
|
|
762
|
+
process.stdout.write = stdoutWrite;
|
|
763
|
+
process.exitCode = exitCode;
|
|
764
|
+
process.argv = argv;
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it("TTY upgrade with --packs skips the pack prompt", async () => {
|
|
769
|
+
isInteractive.mockReturnValue(true);
|
|
770
|
+
multiselect.mockClear();
|
|
771
|
+
confirm.mockClear();
|
|
772
|
+
init.mockClear();
|
|
773
|
+
process.argv = ["node", "grunt", "upgrade", "--no-self-update", "--packs", "listen"];
|
|
774
|
+
const stdoutWrite = process.stdout.write;
|
|
775
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
776
|
+
try {
|
|
777
|
+
await start();
|
|
778
|
+
expect(multiselect).not.toHaveBeenCalled();
|
|
779
|
+
expect(confirm).not.toHaveBeenCalled();
|
|
780
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), {
|
|
781
|
+
skipGlobals: false,
|
|
782
|
+
packageManager: "npm",
|
|
783
|
+
packs: ["listen"],
|
|
784
|
+
});
|
|
785
|
+
} finally {
|
|
786
|
+
process.stdout.write = stdoutWrite;
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
});
|
package/cli/init.mjs
CHANGED
|
@@ -31,6 +31,14 @@ import {
|
|
|
31
31
|
runScriptArgs,
|
|
32
32
|
} from "../scripts/package-manager.mjs"
|
|
33
33
|
import { LAW_REL, emitMaps } from "../scripts/emit-maps.mjs"
|
|
34
|
+
import {
|
|
35
|
+
isUnselectedPackPath,
|
|
36
|
+
resolvePackSelection,
|
|
37
|
+
unselectedPackRels,
|
|
38
|
+
unselectedRefNames,
|
|
39
|
+
unselectedScriptNames,
|
|
40
|
+
writePacks,
|
|
41
|
+
} from "../scripts/feature-packs.mjs"
|
|
34
42
|
|
|
35
43
|
export {
|
|
36
44
|
GUARDED_ROOT_FILES,
|
|
@@ -78,9 +86,12 @@ export const PRODUCT_SCRIPTS = [
|
|
|
78
86
|
"scrub-text-lib.mjs",
|
|
79
87
|
"session-map.mjs",
|
|
80
88
|
"sync-global-settings.mjs",
|
|
89
|
+
"board.mjs",
|
|
81
90
|
"browser.mjs",
|
|
91
|
+
"feature-packs.mjs",
|
|
82
92
|
"speak.mjs",
|
|
83
93
|
"listen.mjs",
|
|
94
|
+
"jev.mjs",
|
|
84
95
|
"google-workspace.mjs",
|
|
85
96
|
"interactive.mjs",
|
|
86
97
|
"prompt.mjs",
|
|
@@ -120,6 +131,7 @@ export const RESERVED_SKILLS = [
|
|
|
120
131
|
"google-workspace",
|
|
121
132
|
"handoff",
|
|
122
133
|
"implement-plan",
|
|
134
|
+
"jev",
|
|
123
135
|
"listen",
|
|
124
136
|
"pickup",
|
|
125
137
|
"speak",
|
|
@@ -388,10 +400,11 @@ export function snapshotReferenceDocs(dest) {
|
|
|
388
400
|
return snap
|
|
389
401
|
}
|
|
390
402
|
|
|
391
|
-
export function remergeReferenceDocs(dest, snap, pkgRoot) {
|
|
403
|
+
export function remergeReferenceDocs(dest, snap, pkgRoot, skip = new Set()) {
|
|
392
404
|
const pkgDir = path.join(pkgRoot, REFERENCE_REL)
|
|
393
405
|
const destDir = path.join(dest, REFERENCE_REL)
|
|
394
406
|
for (const [name, existing] of Object.entries(snap)) {
|
|
407
|
+
if (skip.has(name)) continue
|
|
395
408
|
const pkgFile = path.join(pkgDir, name)
|
|
396
409
|
if (!fs.existsSync(pkgFile)) continue
|
|
397
410
|
const incoming = fs.readFileSync(pkgFile, "utf8")
|
|
@@ -555,7 +568,7 @@ export function resolveInitPackageManager(dest, { packageManager, env = process.
|
|
|
555
568
|
return detected.manager
|
|
556
569
|
}
|
|
557
570
|
|
|
558
|
-
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false, applyGlobals, onPhase, packageManager, env = process.env } = {}) {
|
|
571
|
+
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false, applyGlobals, onPhase, packageManager, env = process.env, packs } = {}) {
|
|
559
572
|
dest = path.resolve(dest)
|
|
560
573
|
const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
|
|
561
574
|
const skipGlobalsApply =
|
|
@@ -571,6 +584,9 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
571
584
|
}
|
|
572
585
|
|
|
573
586
|
const self = samePath(dest, pkgRoot)
|
|
587
|
+
const selection = self ? { packs: [], answered: false } : resolvePackSelection(dest, packs)
|
|
588
|
+
const selected = new Set(selection.packs)
|
|
589
|
+
const allowPack = (src) => !isUnselectedPackPath(path.relative(pkgRoot, src), selected)
|
|
574
590
|
|
|
575
591
|
phase("merge", () => {
|
|
576
592
|
const wsSkills = path.join(dest, WORKSPACE_SKILLS_REL)
|
|
@@ -597,12 +613,12 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
597
613
|
fs.mkdirSync(d, { recursive: true })
|
|
598
614
|
if (samePath(src, d)) continue
|
|
599
615
|
if (dir === ".claude") {
|
|
600
|
-
copyTree(src, d, (s) => path.basename(s) !== "settings.json")
|
|
616
|
+
copyTree(src, d, (s) => path.basename(s) !== "settings.json" && allowPack(s))
|
|
601
617
|
mergeClaudeSettings(dest, pkgRoot)
|
|
602
618
|
} else if (dir === ".rulesync") {
|
|
603
|
-
copyTree(src, d, (s) => !GENERATED_MAP_FILES.has(path.basename(s)))
|
|
619
|
+
copyTree(src, d, (s) => !GENERATED_MAP_FILES.has(path.basename(s)) && allowPack(s))
|
|
604
620
|
} else {
|
|
605
|
-
copyTree(src, d)
|
|
621
|
+
copyTree(src, d, allowPack)
|
|
606
622
|
}
|
|
607
623
|
}
|
|
608
624
|
|
|
@@ -613,7 +629,9 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
613
629
|
}
|
|
614
630
|
|
|
615
631
|
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true })
|
|
632
|
+
const skipScripts = unselectedScriptNames(selected)
|
|
616
633
|
for (const name of PRODUCT_SCRIPTS) {
|
|
634
|
+
if (skipScripts.has(name)) continue
|
|
617
635
|
const src = path.join(pkgRoot, "scripts", name)
|
|
618
636
|
const d = path.join(dest, "scripts", name)
|
|
619
637
|
if (samePath(src, d)) continue
|
|
@@ -624,10 +642,14 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
624
642
|
mergeGitignore(dest)
|
|
625
643
|
|
|
626
644
|
if (!self) {
|
|
627
|
-
remergeReferenceDocs(dest, refSnap, pkgRoot)
|
|
645
|
+
remergeReferenceDocs(dest, refSnap, pkgRoot, unselectedRefNames(selected))
|
|
628
646
|
seedPipelineConfig(dest, destPipelineSrc)
|
|
629
647
|
mergePackageJson(dest, pkgRoot)
|
|
630
648
|
copyRootIfMissing(dest, pkgRoot)
|
|
649
|
+
for (const rel of unselectedPackRels(selected)) {
|
|
650
|
+
fs.rmSync(path.join(dest, rel), { recursive: true, force: true })
|
|
651
|
+
}
|
|
652
|
+
if (selection.answered) writePacks(dest, selection.packs)
|
|
631
653
|
emitDestMaps(dest)
|
|
632
654
|
}
|
|
633
655
|
})
|
package/cli/init.test.ts
CHANGED
|
@@ -1925,7 +1925,8 @@ describe("init", () => {
|
|
|
1925
1925
|
expect(fs.existsSync(path.join(dest, ".mcp.json"))).toBe(false);
|
|
1926
1926
|
|
|
1927
1927
|
for (const name of PRODUCT_FILES) {
|
|
1928
|
-
|
|
1928
|
+
const optional = ["speak.mjs", "listen.mjs", "google-workspace.mjs"].includes(name);
|
|
1929
|
+
expect(fs.existsSync(path.join(dest, "scripts", name)), name).toBe(!optional);
|
|
1929
1930
|
}
|
|
1930
1931
|
expect(fs.statSync(path.join(dest, "scripts", "scrub-text")).isDirectory()).toBe(true);
|
|
1931
1932
|
expect(fs.readFileSync(path.join(dest, "scripts", "scrub-text", "inside"), "utf8")).toBe(
|
|
@@ -2036,6 +2037,7 @@ describe("init", () => {
|
|
|
2036
2037
|
"google-workspace",
|
|
2037
2038
|
"handoff",
|
|
2038
2039
|
"implement-plan",
|
|
2040
|
+
"jev",
|
|
2039
2041
|
"listen",
|
|
2040
2042
|
"pickup",
|
|
2041
2043
|
"speak",
|
|
@@ -2119,9 +2121,13 @@ describe("init", () => {
|
|
|
2119
2121
|
expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
2120
2122
|
expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
|
|
2121
2123
|
expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
|
|
2122
|
-
expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(
|
|
2123
|
-
expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(
|
|
2124
|
-
expect(fs.existsSync(path.join(dest, "scripts", "google-workspace.mjs"))).toBe(
|
|
2124
|
+
expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(false);
|
|
2125
|
+
expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(false);
|
|
2126
|
+
expect(fs.existsSync(path.join(dest, "scripts", "google-workspace.mjs"))).toBe(false);
|
|
2127
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "clasp"))).toBe(false);
|
|
2128
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "browser"))).toBe(true);
|
|
2129
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "reference", "listen.md"))).toBe(false);
|
|
2130
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.features.jsonc"))).toBe(false);
|
|
2125
2131
|
expect(fs.existsSync(path.join(dest, ".claude", "settings.json"))).toBe(true);
|
|
2126
2132
|
expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
|
|
2127
2133
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
@@ -2132,6 +2138,84 @@ describe("init", () => {
|
|
|
2132
2138
|
]);
|
|
2133
2139
|
});
|
|
2134
2140
|
|
|
2141
|
+
function writeSkill(root: string, name: string) {
|
|
2142
|
+
for (const dir of [".rulesync/skills", ".grok/skills", ".claude/skills", ".agents/skills"]) {
|
|
2143
|
+
fs.mkdirSync(path.join(root, dir, name), { recursive: true });
|
|
2144
|
+
fs.writeFileSync(path.join(root, dir, name, "SKILL.md"), `${name}\n`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
it("copies only selected packs and drops an unselected ref instead of merging it back", () => {
|
|
2149
|
+
const pkgRoot = stubPkgRoot();
|
|
2150
|
+
writeSkill(pkgRoot, "ask");
|
|
2151
|
+
writeSkill(pkgRoot, "listen");
|
|
2152
|
+
writeSkill(pkgRoot, "speak");
|
|
2153
|
+
writeSkill(pkgRoot, "clasp");
|
|
2154
|
+
fs.mkdirSync(path.join(pkgRoot, ".rulesync", "reference"), { recursive: true });
|
|
2155
|
+
fs.writeFileSync(path.join(pkgRoot, ".rulesync", "reference", "listen.md"), "# Listen\n\npack\n");
|
|
2156
|
+
fs.writeFileSync(path.join(pkgRoot, ".rulesync", "reference", "law.md"), "# Law\n\n## Skills naming\n\nstock\n");
|
|
2157
|
+
const dest = tmp("packs-default-");
|
|
2158
|
+
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
2159
|
+
fs.writeFileSync(
|
|
2160
|
+
path.join(dest, ".rulesync", "reference", "listen.md"),
|
|
2161
|
+
"# Listen\n\npack\n\n## Mine\n\nkeep\n",
|
|
2162
|
+
);
|
|
2163
|
+
fs.writeFileSync(
|
|
2164
|
+
path.join(dest, ".rulesync", "reference", "law.md"),
|
|
2165
|
+
"# Law\n\n## Skills naming\n\nold\n\n## Mine\n\nkeep\n",
|
|
2166
|
+
);
|
|
2167
|
+
fs.mkdirSync(path.join(dest, ".grok", "skills", "clasp"), { recursive: true });
|
|
2168
|
+
fs.writeFileSync(path.join(dest, ".grok", "skills", "clasp", "SKILL.md"), "old clasp\n");
|
|
2169
|
+
const exec = vi.fn();
|
|
2170
|
+
init(dest, { pkgRoot, execFileSync: exec, packageManager: "npm" });
|
|
2171
|
+
expect(fs.readFileSync(path.join(dest, ".rulesync", "skills", "ask", "SKILL.md"), "utf8")).toBe("ask\n");
|
|
2172
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "listen"))).toBe(false);
|
|
2173
|
+
expect(fs.existsSync(path.join(dest, ".grok", "skills", "speak"))).toBe(false);
|
|
2174
|
+
expect(fs.existsSync(path.join(dest, ".claude", "skills", "clasp"))).toBe(false);
|
|
2175
|
+
expect(fs.existsSync(path.join(dest, ".agents", "skills", "listen"))).toBe(false);
|
|
2176
|
+
expect(fs.existsSync(path.join(dest, ".grok", "skills", "clasp"))).toBe(false);
|
|
2177
|
+
expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(false);
|
|
2178
|
+
expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(false);
|
|
2179
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "reference", "listen.md"))).toBe(false);
|
|
2180
|
+
expect(fs.readFileSync(path.join(dest, ".rulesync", "reference", "law.md"), "utf8")).toContain("## Mine");
|
|
2181
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.features.jsonc"))).toBe(false);
|
|
2182
|
+
|
|
2183
|
+
const picked = tmp("packs-listen-");
|
|
2184
|
+
init(picked, { pkgRoot, execFileSync: exec, packageManager: "npm", packs: ["listen"] });
|
|
2185
|
+
expect(fs.readFileSync(path.join(picked, ".rulesync", "skills", "listen", "SKILL.md"), "utf8")).toBe("listen\n");
|
|
2186
|
+
expect(fs.readFileSync(path.join(picked, "scripts", "listen.mjs"), "utf8")).toBe("listen.mjs");
|
|
2187
|
+
expect(fs.existsSync(path.join(picked, ".rulesync", "reference", "listen.md"))).toBe(true);
|
|
2188
|
+
expect(fs.existsSync(path.join(picked, ".rulesync", "skills", "speak"))).toBe(false);
|
|
2189
|
+
expect(fs.existsSync(path.join(picked, "scripts", "speak.mjs"))).toBe(false);
|
|
2190
|
+
expect(JSON.parse(fs.readFileSync(path.join(picked, ".rulesync", "grunt.features.jsonc"), "utf8"))).toEqual({
|
|
2191
|
+
packs: ["listen"],
|
|
2192
|
+
});
|
|
2193
|
+
|
|
2194
|
+
const saved = tmp("packs-saved-");
|
|
2195
|
+
fs.mkdirSync(path.join(saved, ".rulesync"), { recursive: true });
|
|
2196
|
+
fs.writeFileSync(
|
|
2197
|
+
path.join(saved, ".rulesync", "grunt.features.jsonc"),
|
|
2198
|
+
'{"packs":["speak"]}\n',
|
|
2199
|
+
);
|
|
2200
|
+
init(saved, { pkgRoot, execFileSync: exec, packageManager: "npm" });
|
|
2201
|
+
expect(fs.existsSync(path.join(saved, ".rulesync", "skills", "speak"))).toBe(true);
|
|
2202
|
+
expect(fs.existsSync(path.join(saved, "scripts", "listen.mjs"))).toBe(false);
|
|
2203
|
+
expect(fs.readFileSync(path.join(saved, ".rulesync", "grunt.features.jsonc"), "utf8")).toBe(
|
|
2204
|
+
'{"packs":["speak"]}\n',
|
|
2205
|
+
);
|
|
2206
|
+
|
|
2207
|
+
const pkgSelf = stubPkgRoot();
|
|
2208
|
+
writeSkill(pkgSelf, "clasp");
|
|
2209
|
+
fs.writeFileSync(path.join(pkgSelf, "scripts", "speak.mjs"), "keep-speak\n");
|
|
2210
|
+
init(pkgSelf, { pkgRoot: pkgSelf, execFileSync: exec, packs: [] });
|
|
2211
|
+
expect(fs.readFileSync(path.join(pkgSelf, ".rulesync", "skills", "clasp", "SKILL.md"), "utf8")).toBe("clasp\n");
|
|
2212
|
+
expect(fs.readFileSync(path.join(pkgSelf, "scripts", "speak.mjs"), "utf8")).toBe("keep-speak\n");
|
|
2213
|
+
expect(fs.existsSync(path.join(pkgSelf, ".rulesync", "grunt.features.jsonc"))).toBe(false);
|
|
2214
|
+
expect(() => init(tmp("packs-bad-"), { pkgRoot, execFileSync: exec, packageManager: "npm", packs: ["nope"] })).toThrow(
|
|
2215
|
+
/unknown pack/,
|
|
2216
|
+
);
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2135
2219
|
it("--skip-globals skips sync:globals:apply", () => {
|
|
2136
2220
|
const pkgRoot = stubPkgRoot();
|
|
2137
2221
|
const dest = tmp("grunt-skip-globals-");
|
|
@@ -2611,16 +2695,31 @@ describe("product scripts are consumer-safe", () => {
|
|
|
2611
2695
|
expect(fs.existsSync(path.join(dest, "cli"))).toBe(false);
|
|
2612
2696
|
expect(fs.existsSync(path.join(dest, "scripts", "guarded-roots.mjs"))).toBe(true);
|
|
2613
2697
|
|
|
2698
|
+
const optionalScripts = new Set(["speak.mjs", "listen.mjs", "google-workspace.mjs"]);
|
|
2614
2699
|
for (const name of PRODUCT_FILES) {
|
|
2615
2700
|
const file = path.join(dest, "scripts", name);
|
|
2701
|
+
if (optionalScripts.has(name)) {
|
|
2702
|
+
expect(fs.existsSync(file), name).toBe(false);
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2616
2705
|
expect(fs.existsSync(file), name).toBe(true);
|
|
2617
2706
|
const text = fs.readFileSync(file, "utf8");
|
|
2618
2707
|
for (const spec of relativeSpecs(text, RELATIVE_IMPORT_RE)) {
|
|
2619
2708
|
const resolved = path.resolve(path.dirname(file), spec);
|
|
2620
|
-
|
|
2709
|
+
const optional = optionalScripts.has(path.basename(resolved));
|
|
2710
|
+
expect(fs.existsSync(resolved), `${name} -> ${spec}`).toBe(!optional);
|
|
2621
2711
|
}
|
|
2622
2712
|
}
|
|
2623
2713
|
|
|
2714
|
+
const setupRun = spawnDest(dest, ["./scripts/setup.mjs"], {
|
|
2715
|
+
...process.env,
|
|
2716
|
+
CI: "1",
|
|
2717
|
+
PATH: ["/usr/bin", "/bin"].join(path.delimiter),
|
|
2718
|
+
});
|
|
2719
|
+
expect(`${setupRun.stdout}${setupRun.stderr}`).not.toMatch(/ERR_MODULE_NOT_FOUND/);
|
|
2720
|
+
expect(setupRun.status).toBe(1);
|
|
2721
|
+
expect(`${setupRun.stdout}${setupRun.stderr}`).toContain("usage: setup");
|
|
2722
|
+
|
|
2624
2723
|
const loaded = spawnDest(dest, ["./scripts/guarded-roots.mjs"]);
|
|
2625
2724
|
expect(loaded.status).toBe(1);
|
|
2626
2725
|
expect(loaded.stderr).toBe("usage: guarded-roots.mjs generate|check|watch\n");
|
package/cli/prompt.mjs
CHANGED
package/cli/prompt.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
const clackSelect = vi.hoisted(() => vi.fn());
|
|
4
|
+
const clackMultiselect = vi.hoisted(() => vi.fn());
|
|
4
5
|
const clackConfirm = vi.hoisted(() => vi.fn());
|
|
5
6
|
const clackText = vi.hoisted(() => vi.fn());
|
|
6
7
|
const clackPassword = vi.hoisted(() => vi.fn());
|
|
@@ -10,6 +11,7 @@ const clackCancel = vi.hoisted(() => vi.fn());
|
|
|
10
11
|
|
|
11
12
|
vi.mock("@clack/prompts", () => ({
|
|
12
13
|
select: clackSelect,
|
|
14
|
+
multiselect: clackMultiselect,
|
|
13
15
|
confirm: clackConfirm,
|
|
14
16
|
text: clackText,
|
|
15
17
|
password: clackPassword,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
confirm,
|
|
24
26
|
isInteractive,
|
|
25
27
|
password,
|
|
28
|
+
multiselect,
|
|
26
29
|
select,
|
|
27
30
|
spinner,
|
|
28
31
|
text,
|
|
@@ -164,12 +167,38 @@ describe("bailIfCancel", () => {
|
|
|
164
167
|
describe("clack wrappers", () => {
|
|
165
168
|
afterEach(() => {
|
|
166
169
|
clackSelect.mockReset();
|
|
170
|
+
clackMultiselect.mockReset();
|
|
167
171
|
clackConfirm.mockReset();
|
|
168
172
|
clackSpinner.mockReset();
|
|
169
173
|
clackIsCancel.mockReset();
|
|
170
174
|
clackCancel.mockReset();
|
|
171
175
|
});
|
|
172
176
|
|
|
177
|
+
it("multiselect returns the chosen packs", async () => {
|
|
178
|
+
clackMultiselect.mockResolvedValue(["listen"]);
|
|
179
|
+
clackIsCancel.mockReturnValue(false);
|
|
180
|
+
await expect(
|
|
181
|
+
multiselect({ message: "Optional packs", options: [], required: false }),
|
|
182
|
+
).resolves.toEqual(["listen"]);
|
|
183
|
+
expect(clackMultiselect).toHaveBeenCalledWith({
|
|
184
|
+
message: "Optional packs",
|
|
185
|
+
options: [],
|
|
186
|
+
required: false,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("multiselect cancel exits 0", async () => {
|
|
191
|
+
clackMultiselect.mockResolvedValue(Symbol("cancel"));
|
|
192
|
+
clackIsCancel.mockReturnValue(true);
|
|
193
|
+
const exit = vi.spyOn(process, "exit").mockImplementation((() => {
|
|
194
|
+
throw new Error("EXIT");
|
|
195
|
+
}) as typeof process.exit);
|
|
196
|
+
await expect(multiselect({})).rejects.toThrow("EXIT");
|
|
197
|
+
expect(clackCancel).toHaveBeenCalledWith("Aborted");
|
|
198
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
199
|
+
exit.mockRestore();
|
|
200
|
+
});
|
|
201
|
+
|
|
173
202
|
it("select bails then returns", async () => {
|
|
174
203
|
clackSelect.mockResolvedValue("init");
|
|
175
204
|
clackIsCancel.mockReturnValue(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/grunt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Multi-agent orchestrator SoT — deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -48,9 +48,12 @@
|
|
|
48
48
|
"scripts/scrub-text-lib.mjs",
|
|
49
49
|
"scripts/session-map.mjs",
|
|
50
50
|
"scripts/sync-global-settings.mjs",
|
|
51
|
+
"scripts/board.mjs",
|
|
51
52
|
"scripts/browser.mjs",
|
|
53
|
+
"scripts/feature-packs.mjs",
|
|
52
54
|
"scripts/speak.mjs",
|
|
53
55
|
"scripts/listen.mjs",
|
|
56
|
+
"scripts/jev.mjs",
|
|
54
57
|
"scripts/google-workspace.mjs",
|
|
55
58
|
"scripts/interactive.mjs",
|
|
56
59
|
"scripts/prompt.mjs",
|