@lovrozagar/grunt 0.3.8 → 0.3.9
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/agents/orchestrator.md +9 -9
- package/.agents/hooks.json +1 -1
- package/.agents/skills/cascade/SKILL.md +4 -2
- package/.agents/skills/explain/SKILL.md +5 -3
- package/.agents/skills/handoff/SKILL.md +3 -2
- package/.agents/skills/parent/SKILL.md +3 -8
- package/.agents/skills/solo/SKILL.md +2 -0
- package/.claude/agents/orchestrator.md +10 -10
- package/.claude/settings.json +1 -1
- package/.claude/skills/cascade/SKILL.md +4 -2
- package/.claude/skills/explain/SKILL.md +5 -3
- package/.claude/skills/handoff/SKILL.md +3 -2
- package/.claude/skills/parent/SKILL.md +3 -8
- package/.claude/skills/solo/SKILL.md +2 -0
- package/.codex/agents/orchestrator.toml +8 -5
- package/.codex/hooks.json +1 -1
- package/.grok/agents/orchestrator.md +9 -6
- package/.grok/hooks/orchestrate-parent.js +2 -1
- package/.grok/skills/cascade/SKILL.md +4 -2
- package/.grok/skills/explain/SKILL.md +5 -4
- package/.grok/skills/handoff/SKILL.md +3 -2
- package/.grok/skills/implement-plan/SKILL.md +8 -12
- package/.grok/skills/parent/SKILL.md +3 -9
- package/.grok/skills/solo/SKILL.md +2 -0
- package/.grok/skills/write-plan/SKILL.md +4 -8
- package/.rulesync/hooks.jsonc +2 -1
- package/.rulesync/reference/cascade.md +2 -2
- package/.rulesync/reference/hooks.md +2 -2
- package/.rulesync/rules/CLAUDE.md +7 -3
- package/.rulesync/rules/overview.md +7 -3
- package/.rulesync/skills/cascade/SKILL.md +4 -2
- package/.rulesync/skills/explain/SKILL.md +5 -3
- package/.rulesync/skills/handoff/SKILL.md +3 -2
- package/.rulesync/skills/parent/SKILL.md +3 -9
- package/.rulesync/skills/solo/SKILL.md +2 -0
- package/.rulesync/subagents/orchestrator.md +11 -8
- package/AGENTS.md +7 -3
- package/CLAUDE.md +7 -3
- package/README.md +4 -2
- package/bin/grunt.js +3 -1
- package/cli/grunt.mjs +148 -15
- package/cli/grunt.test.ts +304 -25
- package/cli/init.mjs +65 -43
- package/cli/init.test.ts +78 -0
- package/cli/prompt.mjs +36 -0
- package/cli/prompt.test.ts +199 -0
- package/package.json +4 -1
package/cli/grunt.test.ts
CHANGED
|
@@ -4,26 +4,49 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
6
|
const init = vi.hoisted(() => vi.fn());
|
|
7
|
+
const destAlreadyInited = vi.hoisted(() => vi.fn(() => false));
|
|
8
|
+
const shouldAutoSkipGlobals = vi.hoisted(() => vi.fn(() => false));
|
|
7
9
|
const execFileSync = vi.hoisted(() => vi.fn());
|
|
10
|
+
const isInteractive = vi.hoisted(() => vi.fn(() => false));
|
|
11
|
+
const select = vi.hoisted(() => vi.fn());
|
|
12
|
+
const confirm = vi.hoisted(() => vi.fn());
|
|
13
|
+
const spinner = vi.hoisted(() =>
|
|
14
|
+
vi.fn(() => ({ start: vi.fn(), stop: vi.fn() })),
|
|
15
|
+
);
|
|
16
|
+
const bailIfCancel = vi.hoisted(() => vi.fn((v) => v));
|
|
8
17
|
|
|
9
|
-
vi.mock("./init.mjs", () => ({ init }));
|
|
18
|
+
vi.mock("./init.mjs", () => ({ init, destAlreadyInited, shouldAutoSkipGlobals }));
|
|
10
19
|
vi.mock("node:child_process", () => ({ execFileSync }));
|
|
20
|
+
vi.mock("./prompt.mjs", () => ({
|
|
21
|
+
isInteractive,
|
|
22
|
+
select,
|
|
23
|
+
confirm,
|
|
24
|
+
spinner,
|
|
25
|
+
bailIfCancel,
|
|
26
|
+
}));
|
|
11
27
|
|
|
12
|
-
import { start } from "./grunt.mjs";
|
|
28
|
+
import { parseArgv, start } from "./grunt.mjs";
|
|
13
29
|
|
|
14
30
|
const USAGE = `Usage: grunt [command]
|
|
15
31
|
|
|
16
|
-
Default (no command): init — full setup
|
|
32
|
+
Default (no command): TTY menu; else init — full setup
|
|
17
33
|
|
|
18
34
|
Commands:
|
|
19
|
-
init
|
|
20
|
-
generate
|
|
21
|
-
check
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
init Full setup: merge SoT, npm install, rulesync:generate, sync:globals:apply, rulesync:check
|
|
36
|
+
generate npm run rulesync:generate
|
|
37
|
+
check npm run rulesync:check
|
|
38
|
+
sync-globals npm run sync:globals (dry-run; --apply to write)
|
|
39
|
+
purge-mcps npm run purge:global-mcps (dry-run; --apply to write)
|
|
40
|
+
doctor npm run rulesync:doctor
|
|
41
|
+
help Show this help
|
|
42
|
+
version Print package version
|
|
24
43
|
|
|
25
44
|
Flags:
|
|
26
|
-
--skip-globals
|
|
45
|
+
--skip-globals Skip sync:globals:apply (auto-skipped when already initialized)
|
|
46
|
+
--yes, -y Non-interactive (not --apply)
|
|
47
|
+
--non-interactive Same as --yes
|
|
48
|
+
--apply Write for sync-globals / purge-mcps
|
|
49
|
+
--host <id> sync-globals host
|
|
27
50
|
`;
|
|
28
51
|
|
|
29
52
|
const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -48,7 +71,19 @@ describe("start", () => {
|
|
|
48
71
|
return true;
|
|
49
72
|
}) as typeof process.stdout.write;
|
|
50
73
|
init.mockReset();
|
|
74
|
+
destAlreadyInited.mockReset();
|
|
75
|
+
destAlreadyInited.mockReturnValue(false);
|
|
76
|
+
shouldAutoSkipGlobals.mockReset();
|
|
77
|
+
shouldAutoSkipGlobals.mockReturnValue(false);
|
|
51
78
|
execFileSync.mockReset();
|
|
79
|
+
isInteractive.mockReset();
|
|
80
|
+
isInteractive.mockReturnValue(false);
|
|
81
|
+
select.mockReset();
|
|
82
|
+
confirm.mockReset();
|
|
83
|
+
spinner.mockReset();
|
|
84
|
+
spinner.mockImplementation(() => ({ start: vi.fn(), stop: vi.fn() }));
|
|
85
|
+
bailIfCancel.mockReset();
|
|
86
|
+
bailIfCancel.mockImplementation((v) => v);
|
|
52
87
|
});
|
|
53
88
|
|
|
54
89
|
afterEach(() => {
|
|
@@ -57,33 +92,35 @@ describe("start", () => {
|
|
|
57
92
|
process.stdout.write = stdoutWrite;
|
|
58
93
|
});
|
|
59
94
|
|
|
60
|
-
it.each(["help", "--help", "-h"])("%s writes usage", (cmd) => {
|
|
95
|
+
it.each(["help", "--help", "-h"])("%s writes usage", async (cmd) => {
|
|
61
96
|
process.argv = ["node", "grunt", cmd];
|
|
62
|
-
start();
|
|
97
|
+
await start();
|
|
63
98
|
expect(chunks.join("")).toBe(USAGE);
|
|
64
99
|
expect(process.exitCode).toBe(0);
|
|
65
100
|
expect(init).not.toHaveBeenCalled();
|
|
66
101
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
102
|
+
expect(select).not.toHaveBeenCalled();
|
|
67
103
|
});
|
|
68
104
|
|
|
69
|
-
it.each(["version", "--version", "-v"])("%s writes package version", (cmd) => {
|
|
105
|
+
it.each(["version", "--version", "-v"])("%s writes package version", async (cmd) => {
|
|
70
106
|
process.argv = ["node", "grunt", cmd];
|
|
71
|
-
start();
|
|
107
|
+
await start();
|
|
72
108
|
expect(chunks.join("")).toBe(`${version}\n`);
|
|
73
109
|
expect(process.exitCode).toBe(0);
|
|
74
110
|
});
|
|
75
111
|
|
|
76
|
-
it("no-arg runs init on cwd", () => {
|
|
112
|
+
it("no-arg runs init on cwd", async () => {
|
|
77
113
|
process.argv = ["node", "grunt"];
|
|
78
|
-
start();
|
|
114
|
+
await start();
|
|
79
115
|
expect(init).toHaveBeenCalledOnce();
|
|
80
116
|
expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
|
|
81
117
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
118
|
+
expect(select).not.toHaveBeenCalled();
|
|
82
119
|
});
|
|
83
120
|
|
|
84
|
-
it("init runs init on cwd", () => {
|
|
121
|
+
it("init runs init on cwd", async () => {
|
|
85
122
|
process.argv = ["node", "grunt", "init"];
|
|
86
|
-
start();
|
|
123
|
+
await start();
|
|
87
124
|
expect(init).toHaveBeenCalledOnce();
|
|
88
125
|
expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
|
|
89
126
|
});
|
|
@@ -92,16 +129,28 @@ describe("start", () => {
|
|
|
92
129
|
["node", "grunt", "--skip-globals"],
|
|
93
130
|
["node", "grunt", "init", "--skip-globals"],
|
|
94
131
|
["node", "grunt", "--skip-globals", "init"],
|
|
95
|
-
])("passes skipGlobals when argv is %s", (...argv) => {
|
|
132
|
+
])("passes skipGlobals when argv is %s", async (...argv) => {
|
|
96
133
|
process.argv = argv;
|
|
97
|
-
start();
|
|
134
|
+
await start();
|
|
98
135
|
expect(init).toHaveBeenCalledOnce();
|
|
99
136
|
expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: true });
|
|
100
137
|
});
|
|
101
138
|
|
|
102
|
-
it("
|
|
139
|
+
it.each(["--yes", "-y", "--non-interactive"])(
|
|
140
|
+
"%s no-arg still init, no menu",
|
|
141
|
+
async (flag) => {
|
|
142
|
+
process.argv = ["node", "grunt", flag];
|
|
143
|
+
await start();
|
|
144
|
+
expect(isInteractive).toHaveBeenCalledOnce();
|
|
145
|
+
expect(select).not.toHaveBeenCalled();
|
|
146
|
+
expect(init).toHaveBeenCalledOnce();
|
|
147
|
+
expect(init).toHaveBeenCalledWith(process.cwd(), { skipGlobals: false });
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
it("generate npm-runs rulesync:generate", async () => {
|
|
103
152
|
process.argv = ["node", "grunt", "generate"];
|
|
104
|
-
start();
|
|
153
|
+
await start();
|
|
105
154
|
expect(execFileSync).toHaveBeenCalledOnce();
|
|
106
155
|
expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:generate"], {
|
|
107
156
|
cwd: process.cwd(),
|
|
@@ -110,9 +159,9 @@ describe("start", () => {
|
|
|
110
159
|
expect(init).not.toHaveBeenCalled();
|
|
111
160
|
});
|
|
112
161
|
|
|
113
|
-
it("check npm-runs rulesync:check", () => {
|
|
162
|
+
it("check npm-runs rulesync:check", async () => {
|
|
114
163
|
process.argv = ["node", "grunt", "check"];
|
|
115
|
-
start();
|
|
164
|
+
await start();
|
|
116
165
|
expect(execFileSync).toHaveBeenCalledOnce();
|
|
117
166
|
expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:check"], {
|
|
118
167
|
cwd: process.cwd(),
|
|
@@ -120,12 +169,242 @@ describe("start", () => {
|
|
|
120
169
|
});
|
|
121
170
|
});
|
|
122
171
|
|
|
123
|
-
it("
|
|
172
|
+
it("sync-globals dry-run default; --yes is not apply", async () => {
|
|
173
|
+
process.argv = ["node", "grunt", "sync-globals", "--yes"];
|
|
174
|
+
await start();
|
|
175
|
+
expect(execFileSync).toHaveBeenCalledOnce();
|
|
176
|
+
expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "sync:globals"], {
|
|
177
|
+
cwd: process.cwd(),
|
|
178
|
+
stdio: "inherit",
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("sync-globals --host grok last pair passes host", async () => {
|
|
183
|
+
process.argv = ["node", "grunt", "sync-globals", "--host", "grok"];
|
|
184
|
+
await start();
|
|
185
|
+
expect(execFileSync).toHaveBeenCalledWith(
|
|
186
|
+
"npm",
|
|
187
|
+
["run", "sync:globals", "--", "--host", "grok"],
|
|
188
|
+
{ cwd: process.cwd(), stdio: "inherit" },
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("sync-globals --apply --host passes extra args", async () => {
|
|
193
|
+
process.argv = ["node", "grunt", "sync-globals", "--apply", "--host", "grok"];
|
|
194
|
+
await start();
|
|
195
|
+
expect(execFileSync).toHaveBeenCalledWith(
|
|
196
|
+
"npm",
|
|
197
|
+
["run", "sync:globals:apply", "--", "--host", "grok"],
|
|
198
|
+
{ cwd: process.cwd(), stdio: "inherit" },
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("sync-globals --host last without id: usage exit 1", async () => {
|
|
203
|
+
process.argv = ["node", "grunt", "sync-globals", "--host"];
|
|
204
|
+
await start();
|
|
205
|
+
expect(chunks.join("")).toBe(USAGE);
|
|
206
|
+
expect(process.exitCode).toBe(1);
|
|
207
|
+
expect(execFileSync).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("sync-globals --host=claude dry-run", async () => {
|
|
211
|
+
process.argv = ["node", "grunt", "sync-globals", "--host=claude"];
|
|
212
|
+
await start();
|
|
213
|
+
expect(execFileSync).toHaveBeenCalledWith(
|
|
214
|
+
"npm",
|
|
215
|
+
["run", "sync:globals", "--", "--host", "claude"],
|
|
216
|
+
{ cwd: process.cwd(), stdio: "inherit" },
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("purge-mcps dry-run default", async () => {
|
|
221
|
+
process.argv = ["node", "grunt", "purge-mcps"];
|
|
222
|
+
await start();
|
|
223
|
+
expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "purge:global-mcps"], {
|
|
224
|
+
cwd: process.cwd(),
|
|
225
|
+
stdio: "inherit",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("purge-mcps --apply", async () => {
|
|
230
|
+
process.argv = ["node", "grunt", "--apply", "purge-mcps"];
|
|
231
|
+
await start();
|
|
232
|
+
expect(execFileSync).toHaveBeenCalledWith(
|
|
233
|
+
"npm",
|
|
234
|
+
["run", "purge:global-mcps:apply"],
|
|
235
|
+
{ cwd: process.cwd(), stdio: "inherit" },
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("doctor npm-runs rulesync:doctor", async () => {
|
|
240
|
+
process.argv = ["node", "grunt", "doctor"];
|
|
241
|
+
await start();
|
|
242
|
+
expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:doctor"], {
|
|
243
|
+
cwd: process.cwd(),
|
|
244
|
+
stdio: "inherit",
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("unknown writes usage and exitCode 1", async () => {
|
|
124
249
|
process.argv = ["node", "grunt", "nope"];
|
|
125
|
-
start();
|
|
250
|
+
await start();
|
|
126
251
|
expect(chunks.join("")).toBe(USAGE);
|
|
127
252
|
expect(process.exitCode).toBe(1);
|
|
128
253
|
expect(init).not.toHaveBeenCalled();
|
|
129
254
|
expect(execFileSync).not.toHaveBeenCalled();
|
|
130
255
|
});
|
|
256
|
+
|
|
257
|
+
it("TTY no cmd shows menu default init", async () => {
|
|
258
|
+
isInteractive.mockReturnValue(true);
|
|
259
|
+
select.mockResolvedValue("init");
|
|
260
|
+
confirm.mockResolvedValue(true);
|
|
261
|
+
process.argv = ["node", "grunt"];
|
|
262
|
+
await start();
|
|
263
|
+
expect(select).toHaveBeenCalledOnce();
|
|
264
|
+
const opts = select.mock.calls[0][0] as {
|
|
265
|
+
initialValue: string;
|
|
266
|
+
options: { value: string }[];
|
|
267
|
+
};
|
|
268
|
+
expect(opts.initialValue).toBe("init");
|
|
269
|
+
expect(opts.options.map((o) => o.value)).toEqual([
|
|
270
|
+
"init",
|
|
271
|
+
"generate",
|
|
272
|
+
"check",
|
|
273
|
+
"sync-globals",
|
|
274
|
+
"purge-mcps",
|
|
275
|
+
"doctor",
|
|
276
|
+
"help",
|
|
277
|
+
"quit",
|
|
278
|
+
]);
|
|
279
|
+
expect(init).toHaveBeenCalled();
|
|
280
|
+
expect(isInteractive).toHaveBeenCalledOnce();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("TTY menu generate", async () => {
|
|
284
|
+
isInteractive.mockReturnValue(true);
|
|
285
|
+
select.mockResolvedValue("generate");
|
|
286
|
+
process.argv = ["node", "grunt"];
|
|
287
|
+
await start();
|
|
288
|
+
expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:generate"], {
|
|
289
|
+
cwd: process.cwd(),
|
|
290
|
+
stdio: "inherit",
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("TTY menu help writes usage", async () => {
|
|
295
|
+
isInteractive.mockReturnValue(true);
|
|
296
|
+
select.mockResolvedValue("help");
|
|
297
|
+
process.argv = ["node", "grunt"];
|
|
298
|
+
await start();
|
|
299
|
+
expect(chunks.join("")).toBe(USAGE);
|
|
300
|
+
expect(process.exitCode).toBe(0);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("TTY menu quit does nothing", async () => {
|
|
304
|
+
isInteractive.mockReturnValue(true);
|
|
305
|
+
select.mockResolvedValue("quit");
|
|
306
|
+
process.argv = ["node", "grunt"];
|
|
307
|
+
await start();
|
|
308
|
+
expect(init).not.toHaveBeenCalled();
|
|
309
|
+
expect(execFileSync).not.toHaveBeenCalled();
|
|
310
|
+
expect(process.exitCode).toBe(0);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("TTY cancel exits 0", async () => {
|
|
314
|
+
isInteractive.mockReturnValue(true);
|
|
315
|
+
const exit = vi.spyOn(process, "exit").mockImplementation((() => {
|
|
316
|
+
throw new Error("EXIT");
|
|
317
|
+
}) as typeof process.exit);
|
|
318
|
+
select.mockImplementation(async () => {
|
|
319
|
+
process.exit(0);
|
|
320
|
+
});
|
|
321
|
+
process.argv = ["node", "grunt"];
|
|
322
|
+
await expect(start()).rejects.toThrow("EXIT");
|
|
323
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
324
|
+
expect(init).not.toHaveBeenCalled();
|
|
325
|
+
exit.mockRestore();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("TTY init already-inited decline skips init", async () => {
|
|
329
|
+
isInteractive.mockReturnValue(true);
|
|
330
|
+
destAlreadyInited.mockReturnValue(true);
|
|
331
|
+
confirm.mockResolvedValue(false);
|
|
332
|
+
process.argv = ["node", "grunt", "init"];
|
|
333
|
+
await start();
|
|
334
|
+
expect(confirm).toHaveBeenCalled();
|
|
335
|
+
expect(init).not.toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("TTY init confirms globals; spinner onPhase stop before inherit", async () => {
|
|
339
|
+
isInteractive.mockReturnValue(true);
|
|
340
|
+
shouldAutoSkipGlobals.mockReturnValue(true);
|
|
341
|
+
const spin = { start: vi.fn(), stop: vi.fn() };
|
|
342
|
+
spinner.mockReturnValue(spin);
|
|
343
|
+
confirm.mockResolvedValue(true);
|
|
344
|
+
init.mockImplementation((_dest: string, opts: { onPhase?: (n: string, a: string) => void }) => {
|
|
345
|
+
opts.onPhase?.("merge", "start");
|
|
346
|
+
opts.onPhase?.("merge", "stop");
|
|
347
|
+
opts.onPhase?.("install", "start");
|
|
348
|
+
opts.onPhase?.("install", "stop");
|
|
349
|
+
});
|
|
350
|
+
process.argv = ["node", "grunt", "init"];
|
|
351
|
+
await start();
|
|
352
|
+
expect(confirm).toHaveBeenCalled();
|
|
353
|
+
const globalsConfirm = confirm.mock.calls.find(
|
|
354
|
+
(c) => String((c[0] as { message?: string }).message).includes("globals"),
|
|
355
|
+
);
|
|
356
|
+
expect(globalsConfirm?.[0]).toMatchObject({ initialValue: false });
|
|
357
|
+
expect(init).toHaveBeenCalledWith(
|
|
358
|
+
process.cwd(),
|
|
359
|
+
expect.objectContaining({
|
|
360
|
+
skipGlobals: false,
|
|
361
|
+
applyGlobals: true,
|
|
362
|
+
onPhase: expect.any(Function),
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
expect(isInteractive).toHaveBeenCalledOnce();
|
|
366
|
+
expect(spin.start).toHaveBeenCalledWith("merge");
|
|
367
|
+
expect(spin.stop).toHaveBeenCalled();
|
|
368
|
+
expect(spin.start).toHaveBeenCalledWith("install");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("TTY init --skip-globals default confirm false maps to skip", async () => {
|
|
372
|
+
isInteractive.mockReturnValue(true);
|
|
373
|
+
confirm.mockResolvedValue(false);
|
|
374
|
+
process.argv = ["node", "grunt", "init", "--skip-globals"];
|
|
375
|
+
await start();
|
|
376
|
+
expect(init).toHaveBeenCalledWith(
|
|
377
|
+
process.cwd(),
|
|
378
|
+
expect.objectContaining({
|
|
379
|
+
skipGlobals: true,
|
|
380
|
+
applyGlobals: false,
|
|
381
|
+
onPhase: expect.any(Function),
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
expect(isInteractive).toHaveBeenCalledOnce();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("parseArgv --host", () => {
|
|
389
|
+
it("last flag pair --host <id>", () => {
|
|
390
|
+
expect(parseArgv(["sync-globals", "--host", "grok"])).toMatchObject({
|
|
391
|
+
cmd: "sync-globals",
|
|
392
|
+
host: "grok",
|
|
393
|
+
hostError: false,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("--host last without id is hostError", () => {
|
|
398
|
+
expect(parseArgv(["sync-globals", "--host"])).toMatchObject({ hostError: true });
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("--host= empty is hostError", () => {
|
|
402
|
+
expect(parseArgv(["sync-globals", "--host="])).toMatchObject({ hostError: true });
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("--host followed by flag is hostError", () => {
|
|
406
|
+
expect(parseArgv(["sync-globals", "--host", "--apply"])).toMatchObject({
|
|
407
|
+
hostError: true,
|
|
408
|
+
});
|
|
409
|
+
});
|
|
131
410
|
});
|
package/cli/init.mjs
CHANGED
|
@@ -65,7 +65,7 @@ function destHasGruntSentinel(dest) {
|
|
|
65
65
|
})
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function destAlreadyInited(dest) {
|
|
68
|
+
export function destAlreadyInited(dest) {
|
|
69
69
|
return (
|
|
70
70
|
fs.existsSync(path.join(dest, "scripts", "telemetry.mjs")) ||
|
|
71
71
|
fs.existsSync(path.join(dest, ".grok", "hooks", "orchestrate-parent.js")) ||
|
|
@@ -73,6 +73,10 @@ function destAlreadyInited(dest) {
|
|
|
73
73
|
)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export function shouldAutoSkipGlobals(dest) {
|
|
77
|
+
return destHasGruntSentinel(dest) || fs.existsSync(path.join(dest, "scripts", "telemetry.mjs"))
|
|
78
|
+
}
|
|
79
|
+
|
|
76
80
|
export function mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited = false } = {}) {
|
|
77
81
|
const src = path.join(pkgRoot, file)
|
|
78
82
|
const d = path.join(dest, file)
|
|
@@ -213,63 +217,81 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
213
217
|
fs.writeFileSync(destPath, `${JSON.stringify(destPkg, null, 2)}\n`)
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false } = {}) {
|
|
220
|
+
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false, applyGlobals, onPhase } = {}) {
|
|
217
221
|
dest = path.resolve(dest)
|
|
218
222
|
const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
|
|
219
223
|
const alreadyInited = destAlreadyInited(dest)
|
|
220
224
|
const skipGlobalsApply =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
fs.cpSync(src, d, {
|
|
230
|
-
recursive: true,
|
|
231
|
-
force: true,
|
|
232
|
-
filter: (s) => path.basename(s) !== "settings.json",
|
|
233
|
-
})
|
|
234
|
-
mergeClaudeSettings(dest, pkgRoot)
|
|
235
|
-
} else {
|
|
236
|
-
fs.cpSync(src, d, { recursive: true, force: true })
|
|
225
|
+
applyGlobals === true ? false : skipGlobals || shouldAutoSkipGlobals(dest)
|
|
226
|
+
|
|
227
|
+
const phase = (name, fn) => {
|
|
228
|
+
onPhase?.(name, "start")
|
|
229
|
+
try {
|
|
230
|
+
fn()
|
|
231
|
+
} finally {
|
|
232
|
+
onPhase?.(name, "stop")
|
|
237
233
|
}
|
|
238
234
|
}
|
|
239
235
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
236
|
+
const self = samePath(dest, pkgRoot)
|
|
237
|
+
|
|
238
|
+
phase("merge", () => {
|
|
239
|
+
for (const dir of COPY_DIRS) {
|
|
240
|
+
const src = path.join(pkgRoot, dir)
|
|
241
|
+
const d = path.join(dest, dir)
|
|
242
|
+
fs.mkdirSync(d, { recursive: true })
|
|
243
|
+
if (samePath(src, d)) continue
|
|
244
|
+
if (dir === ".claude") {
|
|
245
|
+
fs.cpSync(src, d, {
|
|
246
|
+
recursive: true,
|
|
247
|
+
force: true,
|
|
248
|
+
filter: (s) => path.basename(s) !== "settings.json",
|
|
249
|
+
})
|
|
250
|
+
mergeClaudeSettings(dest, pkgRoot)
|
|
251
|
+
} else {
|
|
252
|
+
fs.cpSync(src, d, { recursive: true, force: true })
|
|
253
|
+
}
|
|
244
254
|
}
|
|
245
|
-
}
|
|
246
|
-
if (skippedSideFile) {
|
|
247
|
-
console.log("AGENTS.md/CLAUDE.md lack markers and were left alone")
|
|
248
|
-
}
|
|
249
255
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
let skippedSideFile = false
|
|
257
|
+
for (const file of GUARDED_MD_FILES) {
|
|
258
|
+
if (mergeGuardedMarkdown(dest, pkgRoot, file, { alreadyInited }) === "skipped-side-file") {
|
|
259
|
+
skippedSideFile = true
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (skippedSideFile) {
|
|
263
|
+
console.log("AGENTS.md/CLAUDE.md lack markers and were left alone")
|
|
264
|
+
}
|
|
259
265
|
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true })
|
|
267
|
+
for (const name of PRODUCT_SCRIPTS) {
|
|
268
|
+
const src = path.join(pkgRoot, "scripts", name)
|
|
269
|
+
const d = path.join(dest, "scripts", name)
|
|
270
|
+
if (samePath(src, d)) continue
|
|
271
|
+
const st = fs.statSync(src)
|
|
272
|
+
if (st.isDirectory()) fs.cpSync(src, d, { recursive: true, force: true })
|
|
273
|
+
else fs.copyFileSync(src, d)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fs.mkdirSync(path.join(dest, ".tmp"), { recursive: true })
|
|
277
|
+
mergeGitignore(dest)
|
|
278
|
+
|
|
279
|
+
if (!self) mergePackageJson(dest, pkgRoot)
|
|
280
|
+
})
|
|
262
281
|
|
|
263
|
-
const self = samePath(dest, pkgRoot)
|
|
264
282
|
// Self-skip: file/dir merge + .tmp + gitignore only. No package.json merge,
|
|
265
283
|
// npm install, or generate/sync/check (would mutate this package in-place).
|
|
266
284
|
if (self) return
|
|
267
285
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
286
|
+
const runNpm = (name, args) => {
|
|
287
|
+
onPhase?.(name, "start")
|
|
288
|
+
onPhase?.(name, "stop")
|
|
289
|
+
exec("npm", args, { cwd: dest, stdio: "inherit" })
|
|
290
|
+
}
|
|
291
|
+
runNpm("install", ["install"])
|
|
292
|
+
runNpm("generate", ["run", "rulesync:generate"])
|
|
271
293
|
if (!skipGlobalsApply) {
|
|
272
|
-
|
|
294
|
+
runNpm("sync-globals", ["run", "sync:globals:apply"])
|
|
273
295
|
}
|
|
274
|
-
|
|
296
|
+
runNpm("check", ["run", "rulesync:check"])
|
|
275
297
|
}
|
package/cli/init.test.ts
CHANGED
|
@@ -3,12 +3,14 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import {
|
|
6
|
+
destAlreadyInited,
|
|
6
7
|
init,
|
|
7
8
|
mergeClaudeSettings,
|
|
8
9
|
mergeGitignore,
|
|
9
10
|
mergeGuardedMarkdown,
|
|
10
11
|
mergePackageJson,
|
|
11
12
|
samePath,
|
|
13
|
+
shouldAutoSkipGlobals,
|
|
12
14
|
} from "./init.mjs";
|
|
13
15
|
|
|
14
16
|
const tmpDirs: string[] = [];
|
|
@@ -732,6 +734,21 @@ describe("init", () => {
|
|
|
732
734
|
]);
|
|
733
735
|
});
|
|
734
736
|
|
|
737
|
+
it("applyGlobals true still applies when telemetry auto-skip would fire", () => {
|
|
738
|
+
const pkgRoot = stubPkgRoot();
|
|
739
|
+
const dest = tmp("grunt-force-globals-");
|
|
740
|
+
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
|
|
741
|
+
fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
|
|
742
|
+
const exec = vi.fn();
|
|
743
|
+
init(dest, { pkgRoot, execFileSync: exec, applyGlobals: true });
|
|
744
|
+
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
745
|
+
["install"],
|
|
746
|
+
["run", "rulesync:generate"],
|
|
747
|
+
["run", "sync:globals:apply"],
|
|
748
|
+
["run", "rulesync:check"],
|
|
749
|
+
]);
|
|
750
|
+
});
|
|
751
|
+
|
|
735
752
|
it("auto-skip globals when telemetry.mjs exists", () => {
|
|
736
753
|
const pkgRoot = stubPkgRoot();
|
|
737
754
|
const dest = tmp("grunt-auto-tel-");
|
|
@@ -800,4 +817,65 @@ describe("init", () => {
|
|
|
800
817
|
]);
|
|
801
818
|
logSpy.mockRestore();
|
|
802
819
|
});
|
|
820
|
+
|
|
821
|
+
it("onPhase merge then npm phases stop before exec", () => {
|
|
822
|
+
const pkgRoot = stubPkgRoot();
|
|
823
|
+
const dest = tmp("grunt-onphase-");
|
|
824
|
+
const exec = vi.fn();
|
|
825
|
+
const onPhase = vi.fn();
|
|
826
|
+
init(dest, { pkgRoot, execFileSync: exec, onPhase });
|
|
827
|
+
expect(onPhase.mock.calls).toEqual([
|
|
828
|
+
["merge", "start"],
|
|
829
|
+
["merge", "stop"],
|
|
830
|
+
["install", "start"],
|
|
831
|
+
["install", "stop"],
|
|
832
|
+
["generate", "start"],
|
|
833
|
+
["generate", "stop"],
|
|
834
|
+
["sync-globals", "start"],
|
|
835
|
+
["sync-globals", "stop"],
|
|
836
|
+
["check", "start"],
|
|
837
|
+
["check", "stop"],
|
|
838
|
+
]);
|
|
839
|
+
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
840
|
+
["install"],
|
|
841
|
+
["run", "rulesync:generate"],
|
|
842
|
+
["run", "sync:globals:apply"],
|
|
843
|
+
["run", "rulesync:check"],
|
|
844
|
+
]);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("self-skip only merge onPhase", () => {
|
|
848
|
+
const pkgRoot = stubPkgRoot();
|
|
849
|
+
const onPhase = vi.fn();
|
|
850
|
+
init(pkgRoot, { pkgRoot, onPhase });
|
|
851
|
+
expect(onPhase.mock.calls).toEqual([
|
|
852
|
+
["merge", "start"],
|
|
853
|
+
["merge", "stop"],
|
|
854
|
+
]);
|
|
855
|
+
});
|
|
803
856
|
});
|
|
857
|
+
|
|
858
|
+
describe("destAlreadyInited / shouldAutoSkipGlobals", () => {
|
|
859
|
+
it("empty dest is neither", () => {
|
|
860
|
+
const dest = tmp("inspect-empty-");
|
|
861
|
+
expect(destAlreadyInited(dest)).toBe(false);
|
|
862
|
+
expect(shouldAutoSkipGlobals(dest)).toBe(false);
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it(".rulesync means inited, not auto-skip", () => {
|
|
866
|
+
const dest = tmp("inspect-rulesync-");
|
|
867
|
+
fs.mkdirSync(path.join(dest, ".rulesync"));
|
|
868
|
+
expect(destAlreadyInited(dest)).toBe(true);
|
|
869
|
+
expect(shouldAutoSkipGlobals(dest)).toBe(false);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it("sentinel auto-skips globals", () => {
|
|
873
|
+
const dest = tmp("inspect-sent-");
|
|
874
|
+
fs.writeFileSync(
|
|
875
|
+
path.join(dest, "AGENTS.md"),
|
|
876
|
+
"<!-- grunt:begin -->\nx\n<!-- grunt:end -->\n",
|
|
877
|
+
);
|
|
878
|
+
expect(shouldAutoSkipGlobals(dest)).toBe(true);
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
|