@lovrozagar/grunt 0.9.5 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/explain/SKILL.md +1 -1
- package/.agents/skills/jev/SKILL.md +18 -0
- package/.claude/skills/explain/SKILL.md +1 -1
- package/.claude/skills/jev/SKILL.md +18 -0
- package/.grok/hooks/orchestrate-parent.js +9 -1
- package/.grok/skills/explain/SKILL.md +1 -1
- package/.grok/skills/jev/SKILL.md +18 -0
- package/.rulesync/reference/INDEX.md +4 -2
- package/.rulesync/reference/hooks.md +1 -1
- package/.rulesync/reference/jev.md +56 -0
- package/.rulesync/reference/law.md +1 -1
- package/.rulesync/reference/output.md +9 -1
- package/.rulesync/reference/refs-map.md +2 -1
- package/.rulesync/reference/setup.md +3 -1
- package/.rulesync/reference/skills-map.md +1 -0
- package/.rulesync/rules/overview.md +4 -0
- package/.rulesync/skills/explain/SKILL.md +1 -1
- package/.rulesync/skills/jev/SKILL.md +15 -0
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +11 -0
- package/README.md +20 -8
- 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.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"
|
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import { fileURLToPath } from "node:url"
|
|
5
5
|
import { destAlreadyInited, init, RESERVED_SKILLS, toGruntScriptName } from "./init.mjs"
|
|
6
|
-
import { confirm, isInteractive, select, spinner } from "./prompt.mjs"
|
|
6
|
+
import { confirm, isInteractive, multiselect, select, spinner } from "./prompt.mjs"
|
|
7
7
|
import { selfUpdate } from "./self-update.mjs"
|
|
8
8
|
import {
|
|
9
9
|
PACKAGE_MANAGER_ASK,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
runScriptArgs,
|
|
15
15
|
} from "../scripts/package-manager.mjs"
|
|
16
16
|
import { mapCommand } from "../scripts/folder-map.mjs"
|
|
17
|
+
import { PACK_OPTIONS, parsePacksFlag, selectedPacks } from "../scripts/feature-packs.mjs"
|
|
17
18
|
|
|
18
19
|
const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
19
20
|
|
|
@@ -28,7 +29,7 @@ Commands:
|
|
|
28
29
|
sync-globals run grunt:sync:globals (dry-run; --apply to write)
|
|
29
30
|
purge-mcps run grunt:purge:global-mcps (dry-run; --apply to write)
|
|
30
31
|
doctor run grunt:doctor
|
|
31
|
-
setup run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser)
|
|
32
|
+
setup run grunt:setup — handheld keys/OAuth (speak, listen, google-workspace, browser, jev)
|
|
32
33
|
upgrade Self-update to the latest grunt, then re-init: copy owned files, prune retired names, print reserved skills
|
|
33
34
|
map [dir] Code-only folder map (git-tracked, no files); dir for depth
|
|
34
35
|
help Show this help
|
|
@@ -42,6 +43,7 @@ Flags:
|
|
|
42
43
|
--apply Write for sync-globals / purge-mcps
|
|
43
44
|
--host <id> sync-globals host
|
|
44
45
|
--pm <name> npm | yarn | pnpm | bun (else lockfile, then how grunt was launched, then ask)
|
|
46
|
+
--packs <list> Optional packs: google-workspace,listen,speak,clasp or none
|
|
45
47
|
`
|
|
46
48
|
|
|
47
49
|
const MENU_OPTIONS = [
|
|
@@ -94,6 +96,8 @@ export function parseArgv(argv) {
|
|
|
94
96
|
let hostError = false
|
|
95
97
|
let pm
|
|
96
98
|
let pmError = false
|
|
99
|
+
let packs
|
|
100
|
+
let packsError = false
|
|
97
101
|
const positionals = []
|
|
98
102
|
for (let i = 0; i < argv.length; i++) {
|
|
99
103
|
const a = argv[i]
|
|
@@ -141,6 +145,28 @@ export function parseArgv(argv) {
|
|
|
141
145
|
else pm = pm.toLowerCase()
|
|
142
146
|
continue
|
|
143
147
|
}
|
|
148
|
+
if (a === "--packs") {
|
|
149
|
+
const v = argv[i + 1]
|
|
150
|
+
if (v == null || String(v).startsWith("--")) {
|
|
151
|
+
packsError = true
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
packs = parsePacksFlag(v)
|
|
156
|
+
} catch {
|
|
157
|
+
packsError = true
|
|
158
|
+
}
|
|
159
|
+
i += 1
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (typeof a === "string" && a.startsWith("--packs=")) {
|
|
163
|
+
try {
|
|
164
|
+
packs = parsePacksFlag(a.slice("--packs=".length))
|
|
165
|
+
} catch {
|
|
166
|
+
packsError = true
|
|
167
|
+
}
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
144
170
|
positionals.push(a)
|
|
145
171
|
}
|
|
146
172
|
return {
|
|
@@ -153,6 +179,8 @@ export function parseArgv(argv) {
|
|
|
153
179
|
hostError,
|
|
154
180
|
pm,
|
|
155
181
|
pmError,
|
|
182
|
+
packs,
|
|
183
|
+
packsError,
|
|
156
184
|
}
|
|
157
185
|
}
|
|
158
186
|
|
|
@@ -184,9 +212,29 @@ export async function resolveCliPackageManager({ cwd, env, override, interactive
|
|
|
184
212
|
return null
|
|
185
213
|
}
|
|
186
214
|
|
|
187
|
-
async function
|
|
215
|
+
async function askPacks(cwd) {
|
|
216
|
+
return await multiselect({
|
|
217
|
+
message: "Optional packs",
|
|
218
|
+
options: PACK_OPTIONS,
|
|
219
|
+
initialValues: selectedPacks(cwd),
|
|
220
|
+
required: false,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function initOpts(base, packs) {
|
|
225
|
+
if (packs === undefined) return base
|
|
226
|
+
return { ...base, packs }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function runInit(cwd, { skipGlobals, interactive, packageManager, packs, packsOnly = false }) {
|
|
230
|
+
let selected = packs
|
|
231
|
+
if (packsOnly) {
|
|
232
|
+
if (selected === undefined && interactive) selected = await askPacks(cwd)
|
|
233
|
+
init(cwd, initOpts({ skipGlobals, packageManager }, selected))
|
|
234
|
+
return
|
|
235
|
+
}
|
|
188
236
|
if (!interactive) {
|
|
189
|
-
init(cwd, { skipGlobals, packageManager })
|
|
237
|
+
init(cwd, initOpts({ skipGlobals, packageManager }, selected))
|
|
190
238
|
return
|
|
191
239
|
}
|
|
192
240
|
if (destAlreadyInited(cwd)) {
|
|
@@ -196,16 +244,17 @@ async function runInit(cwd, { skipGlobals, interactive, packageManager }) {
|
|
|
196
244
|
})
|
|
197
245
|
if (!again) return
|
|
198
246
|
}
|
|
247
|
+
if (selected === undefined) selected = await askPacks(cwd)
|
|
199
248
|
const applyGlobals = await confirm({
|
|
200
249
|
message: APPLY_GLOBALS_CONFIRM,
|
|
201
250
|
initialValue: !skipGlobals,
|
|
202
251
|
})
|
|
203
|
-
init(cwd, {
|
|
252
|
+
init(cwd, initOpts({
|
|
204
253
|
skipGlobals: !applyGlobals,
|
|
205
254
|
applyGlobals,
|
|
206
255
|
onPhase: bindSpinner(),
|
|
207
256
|
packageManager,
|
|
208
|
-
})
|
|
257
|
+
}, selected))
|
|
209
258
|
}
|
|
210
259
|
|
|
211
260
|
async function dispatch(cmd, flags, interactive, pm) {
|
|
@@ -222,6 +271,7 @@ async function dispatch(cmd, flags, interactive, pm) {
|
|
|
222
271
|
skipGlobals: flags.skipGlobals,
|
|
223
272
|
interactive: interactive && (!cmd || cmd === "init"),
|
|
224
273
|
packageManager: pm,
|
|
274
|
+
packs: flags.packs,
|
|
225
275
|
})
|
|
226
276
|
return
|
|
227
277
|
}
|
|
@@ -270,8 +320,10 @@ async function dispatch(cmd, flags, interactive, pm) {
|
|
|
270
320
|
}
|
|
271
321
|
await runInit(process.cwd(), {
|
|
272
322
|
skipGlobals: flags.skipGlobals,
|
|
273
|
-
interactive
|
|
323
|
+
interactive,
|
|
324
|
+
packsOnly: true,
|
|
274
325
|
packageManager: pm,
|
|
326
|
+
packs: flags.packs,
|
|
275
327
|
})
|
|
276
328
|
process.stdout.write(`reserved: ${RESERVED_SKILLS.join(" ")}\n`)
|
|
277
329
|
return
|
|
@@ -286,7 +338,7 @@ function commandNeedsPackageManager(cmd) {
|
|
|
286
338
|
|
|
287
339
|
export async function start() {
|
|
288
340
|
const flags = parseArgv(process.argv.slice(2))
|
|
289
|
-
if (flags.hostError || flags.pmError) {
|
|
341
|
+
if (flags.hostError || flags.pmError || flags.packsError) {
|
|
290
342
|
process.stdout.write(USAGE)
|
|
291
343
|
process.exitCode = 1
|
|
292
344
|
return
|
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.1",
|
|
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",
|