@lovrozagar/grunt 0.5.0 → 0.5.2
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/grunt.md +2 -1
- package/.agents/agents/implementer.md +1 -1
- package/.agents/agents/orchestrator.md +17 -12
- package/.agents/agents/thinker.md +6 -5
- package/.agents/skills/ask/SKILL.md +18 -0
- package/.agents/skills/auto/SKILL.md +18 -0
- package/.agents/skills/browser/SKILL.md +4 -3
- package/.agents/skills/cascade/SKILL.md +5 -3
- package/.agents/skills/handoff/SKILL.md +1 -1
- package/.agents/skills/implement-plan/SKILL.md +12 -11
- package/.agents/skills/solo/SKILL.md +5 -3
- package/.agents/skills/tmp/SKILL.md +58 -0
- package/.agents/skills/write-plan/SKILL.md +11 -8
- package/.claude/agents/grunt.md +2 -1
- package/.claude/agents/implementer.md +1 -1
- package/.claude/agents/orchestrator.md +17 -12
- package/.claude/agents/thinker.md +6 -5
- package/.claude/skills/ask/SKILL.md +18 -0
- package/.claude/skills/auto/SKILL.md +18 -0
- package/.claude/skills/browser/SKILL.md +4 -3
- package/.claude/skills/cascade/SKILL.md +5 -3
- package/.claude/skills/handoff/SKILL.md +1 -1
- package/.claude/skills/implement-plan/SKILL.md +12 -11
- package/.claude/skills/solo/SKILL.md +5 -3
- package/.claude/skills/tmp/SKILL.md +58 -0
- package/.claude/skills/write-plan/SKILL.md +11 -8
- package/.codex/agents/grunt.toml +2 -1
- package/.codex/agents/implementer.toml +1 -1
- package/.codex/agents/orchestrator.toml +17 -12
- package/.codex/agents/thinker.toml +6 -5
- package/.grok/agents/grunt.md +2 -1
- package/.grok/agents/implementer.md +1 -1
- package/.grok/agents/orchestrator.md +17 -12
- package/.grok/agents/thinker.md +6 -5
- package/.grok/hooks/orchestrate-parent.js +280 -62
- package/.grok/skills/ask/SKILL.md +18 -0
- package/.grok/skills/auto/SKILL.md +18 -0
- package/.grok/skills/browser/SKILL.md +4 -3
- package/.grok/skills/cascade/SKILL.md +5 -3
- package/.grok/skills/handoff/SKILL.md +1 -1
- package/.grok/skills/implement-plan/SKILL.md +12 -11
- package/.grok/skills/solo/SKILL.md +5 -3
- package/.grok/skills/tmp/SKILL.md +58 -0
- package/.grok/skills/write-plan/SKILL.md +11 -8
- package/.rulesync/grunt.config.jsonc +7 -0
- package/.rulesync/reference/INDEX.md +7 -4
- package/.rulesync/reference/cascade.md +15 -13
- package/.rulesync/reference/hooks.md +6 -4
- package/.rulesync/reference/law.md +1 -1
- package/.rulesync/reference/map.md +14 -9
- package/.rulesync/reference/output.md +18 -8
- package/.rulesync/reference/plan-format.md +10 -10
- package/.rulesync/reference/skills-map.md +6 -3
- package/.rulesync/rules/CLAUDE.md +18 -13
- package/.rulesync/rules/overview.md +18 -13
- package/.rulesync/skills/ask/SKILL.md +18 -0
- package/.rulesync/skills/auto/SKILL.md +18 -0
- package/.rulesync/skills/browser/SKILL.md +2 -2
- package/.rulesync/skills/cascade/SKILL.md +5 -3
- package/.rulesync/skills/handoff/SKILL.md +1 -1
- package/.rulesync/skills/implement-plan/SKILL.md +14 -11
- package/.rulesync/skills/solo/SKILL.md +5 -3
- package/.rulesync/skills/tmp/SKILL.md +58 -0
- package/.rulesync/skills/write-plan/SKILL.md +12 -8
- package/.rulesync/subagents/grunt.md +2 -1
- package/.rulesync/subagents/implementer.md +1 -1
- package/.rulesync/subagents/orchestrator.md +17 -12
- package/.rulesync/subagents/thinker.md +6 -5
- package/AGENTS.md +18 -13
- package/CHANGELOG.md +33 -0
- package/CLAUDE.md +18 -13
- package/README.md +17 -16
- package/cli/grunt.mjs +12 -12
- package/cli/grunt.test.ts +29 -20
- package/cli/init.mjs +116 -22
- package/cli/init.test.ts +545 -34
- package/package.json +8 -1
- package/scripts/gate-fat-tools.mjs +2 -2
- package/scripts/grunt-config.mjs +50 -0
- package/scripts/persist-plan.mjs +17 -2
- package/scripts/persist-tmp.mjs +123 -0
- package/scripts/scrub-spawn-prompt.mjs +1 -1
package/cli/init.test.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
6
|
import {
|
|
6
7
|
MAX_GUARDED_MARKDOWN_BYTES,
|
|
7
8
|
composeGuardedMarkdown,
|
|
8
9
|
destAlreadyInited,
|
|
9
10
|
extractGruntBody,
|
|
11
|
+
extractUserMarkdown,
|
|
10
12
|
GUARDED_ROOT_FILES,
|
|
13
|
+
SENTINEL_BEGIN,
|
|
14
|
+
SENTINEL_END,
|
|
11
15
|
guardedMarkdownDrift,
|
|
12
16
|
healGuardedRootFile,
|
|
13
17
|
init,
|
|
18
|
+
LAUNCH_SCRIPTS,
|
|
14
19
|
mergeClaudeSettings,
|
|
15
20
|
mergeGitignore,
|
|
16
21
|
mergeGuardedContent,
|
|
@@ -20,10 +25,12 @@ import {
|
|
|
20
25
|
samePath,
|
|
21
26
|
shouldAutoSkipGlobals,
|
|
22
27
|
snapshotGuardedRoots,
|
|
28
|
+
toGruntScriptName,
|
|
23
29
|
withGuardedCheckInteriors,
|
|
24
30
|
writeMergedGuardedFile,
|
|
25
31
|
} from "./init.mjs";
|
|
26
32
|
import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
|
|
33
|
+
import { stripJsonc } from "../scripts/grunt-config.mjs";
|
|
27
34
|
|
|
28
35
|
const tmpDirs: string[] = [];
|
|
29
36
|
afterEach(() => {
|
|
@@ -39,6 +46,12 @@ function tmp(prefix: string) {
|
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"];
|
|
49
|
+
const GRUNT_CONFIG_REL = path.join(".rulesync", "grunt.config.jsonc");
|
|
50
|
+
const GRUNT_CONFIG_DEFAULTS = {
|
|
51
|
+
version: 1,
|
|
52
|
+
leftoverGate: "ask",
|
|
53
|
+
spawnMode: "cascade",
|
|
54
|
+
};
|
|
42
55
|
const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
43
56
|
const PRODUCT_FILES = [
|
|
44
57
|
"check-globals.mjs",
|
|
@@ -51,8 +64,10 @@ const PRODUCT_FILES = [
|
|
|
51
64
|
"hooks-union.mjs",
|
|
52
65
|
"pipeline.mjs",
|
|
53
66
|
"grunt-job.mjs",
|
|
67
|
+
"grunt-config.mjs",
|
|
54
68
|
"parse-need.mjs",
|
|
55
69
|
"persist-handoff.mjs",
|
|
70
|
+
"persist-tmp.mjs",
|
|
56
71
|
"persist-plan.mjs",
|
|
57
72
|
"purge-global-mcps.mjs",
|
|
58
73
|
"scrub-spawn-prompt.mjs",
|
|
@@ -103,6 +118,12 @@ const SRC_CLAUDE_SETTINGS = {
|
|
|
103
118
|
enabledMcpjsonServers: [],
|
|
104
119
|
};
|
|
105
120
|
|
|
121
|
+
function readGruntConfig(root: string) {
|
|
122
|
+
const p = path.join(root, GRUNT_CONFIG_REL);
|
|
123
|
+
expect(fs.existsSync(p)).toBe(true);
|
|
124
|
+
return JSON.parse(stripJsonc(fs.readFileSync(p, "utf8")));
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
function stubPkgRoot(pkg: Record<string, unknown> = {
|
|
107
128
|
name: "fixture-pkg",
|
|
108
129
|
scripts: {
|
|
@@ -122,6 +143,10 @@ function stubPkgRoot(pkg: Record<string, unknown> = {
|
|
|
122
143
|
fs.mkdirSync(path.join(root, d));
|
|
123
144
|
fs.writeFileSync(path.join(root, d, "marker"), d);
|
|
124
145
|
}
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
path.join(root, GRUNT_CONFIG_REL),
|
|
148
|
+
`${JSON.stringify(GRUNT_CONFIG_DEFAULTS, null, 2)}\n`,
|
|
149
|
+
);
|
|
125
150
|
fs.writeFileSync(
|
|
126
151
|
path.join(root, ".claude", "settings.json"),
|
|
127
152
|
`${JSON.stringify(SRC_CLAUDE_SETTINGS, null, 2)}\n`,
|
|
@@ -290,6 +315,67 @@ describe("mergeGuardedMarkdown", () => {
|
|
|
290
315
|
);
|
|
291
316
|
});
|
|
292
317
|
|
|
318
|
+
it("already-guarded package src stays begin=1 end=1", () => {
|
|
319
|
+
const pkgRoot = stubPkgRoot();
|
|
320
|
+
fs.writeFileSync(
|
|
321
|
+
path.join(pkgRoot, "CLAUDE.md"),
|
|
322
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
|
|
323
|
+
);
|
|
324
|
+
const dest = tmp("md-wrapped-src-");
|
|
325
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
326
|
+
const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
|
|
327
|
+
expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
|
|
328
|
+
expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
329
|
+
expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("merge heals dest with trailing orphan grunt:end", () => {
|
|
333
|
+
const pkgRoot = stubPkgRoot();
|
|
334
|
+
const dest = tmp("md-orphan-end-");
|
|
335
|
+
fs.writeFileSync(
|
|
336
|
+
path.join(dest, "CLAUDE.md"),
|
|
337
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
|
|
338
|
+
);
|
|
339
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
340
|
+
const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
|
|
341
|
+
expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
|
|
342
|
+
expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("dest stacked researcher shape heals to exactly one pair", () => {
|
|
346
|
+
const pkgRoot = stubPkgRoot();
|
|
347
|
+
fs.writeFileSync(
|
|
348
|
+
path.join(pkgRoot, "CLAUDE.md"),
|
|
349
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
|
|
350
|
+
);
|
|
351
|
+
const dest = tmp("md-stacked-");
|
|
352
|
+
fs.writeFileSync(
|
|
353
|
+
path.join(dest, "CLAUDE.md"),
|
|
354
|
+
"<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
|
|
355
|
+
);
|
|
356
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
357
|
+
const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
|
|
358
|
+
expect(out).toBe("<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n");
|
|
359
|
+
expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
360
|
+
expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("dest stacked keeps user prefix/suffix outside the pair", () => {
|
|
364
|
+
const pkgRoot = stubPkgRoot();
|
|
365
|
+
const dest = tmp("md-stacked-user-");
|
|
366
|
+
fs.writeFileSync(
|
|
367
|
+
path.join(dest, "CLAUDE.md"),
|
|
368
|
+
"prefix\n<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\nsuffix\n",
|
|
369
|
+
);
|
|
370
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
371
|
+
const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
|
|
372
|
+
expect(out).toBe(
|
|
373
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nprefix\nsuffix\n",
|
|
374
|
+
);
|
|
375
|
+
expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
376
|
+
expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
377
|
+
});
|
|
378
|
+
|
|
293
379
|
it("does not fuzzy-dedupe unmarked protocol-looking user text", () => {
|
|
294
380
|
const pkgRoot = stubPkgRoot();
|
|
295
381
|
const dest = tmp("md-proto-");
|
|
@@ -363,6 +449,23 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
363
449
|
);
|
|
364
450
|
});
|
|
365
451
|
|
|
452
|
+
it("remerge after wrapped-src merge+generate does not reattach orphan end", () => {
|
|
453
|
+
const pkgRoot = stubPkgRoot();
|
|
454
|
+
fs.writeFileSync(
|
|
455
|
+
path.join(pkgRoot, "AGENTS.md"),
|
|
456
|
+
"<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\n",
|
|
457
|
+
);
|
|
458
|
+
const dest = tmp("snap-wrap-src-");
|
|
459
|
+
mergeGuardedMarkdown(dest, pkgRoot, "AGENTS.md");
|
|
460
|
+
const snap = snapshotGuardedRoots(dest);
|
|
461
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
|
|
462
|
+
remergeGuardedRoots(dest, snap);
|
|
463
|
+
const out = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
|
|
464
|
+
expect(out).toBe("<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\n");
|
|
465
|
+
expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
466
|
+
expect(out.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
467
|
+
});
|
|
468
|
+
|
|
366
469
|
it("withGuardedCheckInteriors is restored even if check throws", () => {
|
|
367
470
|
const dest = tmp("check-int-");
|
|
368
471
|
const live =
|
|
@@ -378,6 +481,22 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
378
481
|
});
|
|
379
482
|
|
|
380
483
|
it("compose/extract/drift/write helpers cover remaining branches", () => {
|
|
484
|
+
expect(
|
|
485
|
+
extractUserMarkdown(
|
|
486
|
+
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n<!-- grunt:end -->\n",
|
|
487
|
+
),
|
|
488
|
+
).toBe("");
|
|
489
|
+
expect(
|
|
490
|
+
extractUserMarkdown(
|
|
491
|
+
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\nkeep\n<!-- grunt:end -->\n",
|
|
492
|
+
),
|
|
493
|
+
).toBe("keep\n");
|
|
494
|
+
expect(
|
|
495
|
+
composeGuardedMarkdown(
|
|
496
|
+
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
|
|
497
|
+
"<!-- grunt:end -->\n",
|
|
498
|
+
),
|
|
499
|
+
).toBe("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n");
|
|
381
500
|
expect(composeGuardedMarkdown("body", null)).toBe(
|
|
382
501
|
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
|
|
383
502
|
);
|
|
@@ -402,7 +521,9 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
402
521
|
expect(mergeGuardedContent("keep\n", "g\r\n")).toBe(
|
|
403
522
|
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\nkeep\n",
|
|
404
523
|
);
|
|
405
|
-
expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).
|
|
524
|
+
expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toBe(
|
|
525
|
+
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
|
|
526
|
+
);
|
|
406
527
|
const dest = tmp("md-helpers-");
|
|
407
528
|
const missing = path.join(dest, "nope.md");
|
|
408
529
|
expect(guardedMarkdownDrift(missing, "g")).toBe(true);
|
|
@@ -423,6 +544,73 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
423
544
|
expect(writeMergedGuardedFile(huge, "g")).toBe("aborted-unsafe");
|
|
424
545
|
});
|
|
425
546
|
|
|
547
|
+
it("unclosed begin unwraps interior; inline orphan end is stripped", () => {
|
|
548
|
+
expect(extractGruntBody("<!-- grunt:begin -->\nhello")).toBe("hello");
|
|
549
|
+
const composed = composeGuardedMarkdown("<!-- grunt:begin -->\nhello", "");
|
|
550
|
+
expect(composed).toBe("<!-- grunt:begin -->\nhello\n<!-- grunt:end -->\n");
|
|
551
|
+
expect(composed.split(SENTINEL_BEGIN)).toHaveLength(2);
|
|
552
|
+
expect(composed.split(SENTINEL_END)).toHaveLength(2);
|
|
553
|
+
expect(composed).not.toContain(`${SENTINEL_BEGIN}\n${SENTINEL_BEGIN}`);
|
|
554
|
+
expect(mergeGuardedContent("note\n<!-- grunt:begin -->\nstale", "new")).toBe(
|
|
555
|
+
"<!-- grunt:begin -->\nnew\n<!-- grunt:end -->\nnote\n",
|
|
556
|
+
);
|
|
557
|
+
expect(extractUserMarkdown("keep <!-- grunt:end --> me")).not.toContain(SENTINEL_END);
|
|
558
|
+
expect(extractUserMarkdown("keep <!-- grunt:end --> me")).toBe("keep me");
|
|
559
|
+
expect(extractUserMarkdown("keep\n<!-- grunt:end -->\nme")).toBe("keep\nme");
|
|
560
|
+
expect(extractUserMarkdown("keep\n <!-- grunt:end --> \nme")).toBe("keep\nme");
|
|
561
|
+
expect(extractGruntBody("<!-- grunt:begin -->\r\nhello")).toBe("hello");
|
|
562
|
+
expect(extractGruntBody("<!-- grunt:begin -->")).toBe("");
|
|
563
|
+
const emptyWrap = composeGuardedMarkdown("<!-- grunt:begin -->", "");
|
|
564
|
+
expect(emptyWrap.split(SENTINEL_BEGIN)).toHaveLength(2);
|
|
565
|
+
expect(emptyWrap).toBe("<!-- grunt:begin -->\n\n<!-- grunt:end -->\n");
|
|
566
|
+
const mixed =
|
|
567
|
+
"<!-- grunt:begin -->\nok\n<!-- grunt:end -->\nuser bit\n<!-- grunt:begin -->\nstale";
|
|
568
|
+
expect(extractGruntBody(mixed)).toBe("ok");
|
|
569
|
+
expect(extractUserMarkdown(mixed)).toBe("user bit\n");
|
|
570
|
+
expect(
|
|
571
|
+
composeGuardedMarkdown("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n", ""),
|
|
572
|
+
).toBe("<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("extract/compose peel stacked dest and trailing orphan end", () => {
|
|
576
|
+
const stacked =
|
|
577
|
+
"<!-- grunt:begin -->\n<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n<!-- grunt:end -->\n";
|
|
578
|
+
expect(extractGruntBody(stacked)).toBe("BODY");
|
|
579
|
+
const stackedOut = composeGuardedMarkdown(stacked, "");
|
|
580
|
+
expect(stackedOut).toBe("<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n");
|
|
581
|
+
expect(stackedOut.split(SENTINEL_BEGIN)).toHaveLength(2);
|
|
582
|
+
expect(stackedOut.split(SENTINEL_END)).toHaveLength(2);
|
|
583
|
+
expect(composeGuardedMarkdown("BODY\n<!-- grunt:end -->\n", "")).toBe(
|
|
584
|
+
"<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\n",
|
|
585
|
+
);
|
|
586
|
+
expect(extractGruntBody("BODY\n\n<!-- grunt:end -->")).toBe("BODY");
|
|
587
|
+
expect(mergeGuardedContent(stacked, "SRC")).toBe(
|
|
588
|
+
"<!-- grunt:begin -->\nSRC\n<!-- grunt:end -->\n",
|
|
589
|
+
);
|
|
590
|
+
expect(mergeGuardedContent("keep\n<!-- grunt:end -->\n", "SRC")).toBe(
|
|
591
|
+
"<!-- grunt:begin -->\nSRC\n<!-- grunt:end -->\nkeep\n",
|
|
592
|
+
);
|
|
593
|
+
expect(
|
|
594
|
+
extractGruntBody("<!-- grunt:begin --> \nBODY\n<!-- grunt:end --> \n"),
|
|
595
|
+
).toBe("BODY");
|
|
596
|
+
expect(
|
|
597
|
+
extractGruntBody(" <!-- grunt:begin -->\nBODY\n <!-- grunt:end -->\n"),
|
|
598
|
+
).toBe("BODY");
|
|
599
|
+
expect(extractGruntBody("<!-- grunt:begin -->\n<!-- grunt:end -->")).toBe("");
|
|
600
|
+
expect(
|
|
601
|
+
extractGruntBody(
|
|
602
|
+
"<!-- grunt:begin -->\nsee <!-- grunt:end --> here\n<!-- grunt:end -->\n",
|
|
603
|
+
),
|
|
604
|
+
).toBe("see <!-- grunt:end --> here");
|
|
605
|
+
const midLine =
|
|
606
|
+
"<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\nstill user\n<!-- grunt:end -->\n";
|
|
607
|
+
expect(extractGruntBody(midLine)).toBe("BODY");
|
|
608
|
+
expect(extractUserMarkdown(midLine)).toBe("still user\n");
|
|
609
|
+
expect(composeGuardedMarkdown(extractGruntBody(midLine) ?? "", extractUserMarkdown(midLine))).toBe(
|
|
610
|
+
"<!-- grunt:begin -->\nBODY\n<!-- grunt:end -->\nstill user\n",
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
|
|
426
614
|
it("remerge restores unsafe, missing, binary current, and created files", () => {
|
|
427
615
|
const dest = tmp("remerge-edges-");
|
|
428
616
|
const agents = path.join(dest, "AGENTS.md");
|
|
@@ -786,6 +974,13 @@ describe("mergeClaudeSettings", () => {
|
|
|
786
974
|
});
|
|
787
975
|
});
|
|
788
976
|
|
|
977
|
+
describe("toGruntScriptName", () => {
|
|
978
|
+
it("prefixes unprefixed keys and does not double-prefix", () => {
|
|
979
|
+
expect(toGruntScriptName("rulesync:generate")).toBe("grunt:rulesync:generate");
|
|
980
|
+
expect(toGruntScriptName("grunt:doctor")).toBe("grunt:doctor");
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
|
|
789
984
|
describe("mergePackageJson", () => {
|
|
790
985
|
it("creates {} dest and merges scripts skipping test, injects deps, sorts keys", () => {
|
|
791
986
|
const pkgRoot = stubPkgRoot();
|
|
@@ -793,11 +988,25 @@ describe("mergePackageJson", () => {
|
|
|
793
988
|
mergePackageJson(dest, pkgRoot);
|
|
794
989
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
795
990
|
expect(out.scripts).toEqual({
|
|
796
|
-
alpha: "a",
|
|
797
|
-
"
|
|
798
|
-
|
|
991
|
+
"grunt:alpha": "a",
|
|
992
|
+
"grunt:antigravity": LAUNCH_SCRIPTS.antigravity,
|
|
993
|
+
"grunt:claude": LAUNCH_SCRIPTS.claude,
|
|
994
|
+
"grunt:codex": LAUNCH_SCRIPTS.codex,
|
|
995
|
+
"grunt:gemini": LAUNCH_SCRIPTS.gemini,
|
|
996
|
+
"grunt:grok": LAUNCH_SCRIPTS.grok,
|
|
997
|
+
"grunt:rulesync:generate": "gen",
|
|
998
|
+
"grunt:zeta": "z",
|
|
799
999
|
});
|
|
800
|
-
expect(Object.keys(out.scripts)).toEqual([
|
|
1000
|
+
expect(Object.keys(out.scripts)).toEqual([
|
|
1001
|
+
"grunt:alpha",
|
|
1002
|
+
"grunt:antigravity",
|
|
1003
|
+
"grunt:claude",
|
|
1004
|
+
"grunt:codex",
|
|
1005
|
+
"grunt:gemini",
|
|
1006
|
+
"grunt:grok",
|
|
1007
|
+
"grunt:rulesync:generate",
|
|
1008
|
+
"grunt:zeta",
|
|
1009
|
+
]);
|
|
801
1010
|
expect(out.devDependencies).toEqual({
|
|
802
1011
|
rulesync: "latest",
|
|
803
1012
|
"smol-toml": "^1.8.0",
|
|
@@ -817,14 +1026,21 @@ describe("mergePackageJson", () => {
|
|
|
817
1026
|
devDependencies: { lodash: "4" },
|
|
818
1027
|
}),
|
|
819
1028
|
);
|
|
1029
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
820
1030
|
mergePackageJson(dest, pkgRoot);
|
|
821
1031
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
822
1032
|
expect(out.scripts.test).toBe("jest");
|
|
823
1033
|
expect(out.scripts.alpha).toBe("old");
|
|
1034
|
+
expect(out.scripts["grunt:alpha"]).toBe("a");
|
|
1035
|
+
expect(out.scripts["grunt:zeta"]).toBe("z");
|
|
824
1036
|
expect(out.scripts.foo).toBe("bar");
|
|
825
1037
|
expect(out.devDependencies.lodash).toBe("4");
|
|
826
1038
|
expect(out.devDependencies["smol-toml"]).toBe("^1.8.0");
|
|
827
1039
|
expect(out.devDependencies.rulesync).toBe("latest");
|
|
1040
|
+
expect(warnSpy.mock.calls.some((c) => String(c[0]).includes("alpha") && String(c[0]).includes("untouched"))).toBe(
|
|
1041
|
+
true,
|
|
1042
|
+
);
|
|
1043
|
+
warnSpy.mockRestore();
|
|
828
1044
|
});
|
|
829
1045
|
|
|
830
1046
|
it("preserves dest suffix after an upgraded grunt script prefix", () => {
|
|
@@ -844,9 +1060,10 @@ describe("mergePackageJson", () => {
|
|
|
844
1060
|
);
|
|
845
1061
|
mergePackageJson(dest, pkgRoot);
|
|
846
1062
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
847
|
-
expect(out.scripts["rulesync:generate"]).toBe(
|
|
1063
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe(
|
|
848
1064
|
"rulesync generate -t bar && node scripts/codex-sync.mjs",
|
|
849
1065
|
);
|
|
1066
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
850
1067
|
});
|
|
851
1068
|
|
|
852
1069
|
it("preserves suffix after a multi-command grunt script upgrade", () => {
|
|
@@ -869,7 +1086,7 @@ describe("mergePackageJson", () => {
|
|
|
869
1086
|
);
|
|
870
1087
|
mergePackageJson(dest, pkgRoot);
|
|
871
1088
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
872
|
-
expect(out.scripts["rulesync:generate"]).toBe(
|
|
1089
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe(
|
|
873
1090
|
"rulesync generate -t bar && node scripts/emit.mjs && node scripts/codex-sync.mjs",
|
|
874
1091
|
);
|
|
875
1092
|
});
|
|
@@ -889,6 +1106,7 @@ describe("mergePackageJson", () => {
|
|
|
889
1106
|
mergePackageJson(dest, pkgRoot);
|
|
890
1107
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
891
1108
|
expect(out.scripts.alpha).toBe("echo custom && extra");
|
|
1109
|
+
expect(out.scripts["grunt:alpha"]).toBe("a");
|
|
892
1110
|
expect(warnSpy).toHaveBeenCalled();
|
|
893
1111
|
warnSpy.mockRestore();
|
|
894
1112
|
});
|
|
@@ -910,6 +1128,7 @@ describe("mergePackageJson", () => {
|
|
|
910
1128
|
mergePackageJson(dest, pkgRoot);
|
|
911
1129
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
912
1130
|
expect(out.scripts["rulesync:generate"]).toBe("echo custom;echo extra");
|
|
1131
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
|
|
913
1132
|
expect(warnSpy).toHaveBeenCalled();
|
|
914
1133
|
warnSpy.mockRestore();
|
|
915
1134
|
});
|
|
@@ -927,7 +1146,8 @@ describe("mergePackageJson", () => {
|
|
|
927
1146
|
);
|
|
928
1147
|
mergePackageJson(dest, pkgRoot);
|
|
929
1148
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
930
|
-
expect(out.scripts["rulesync:generate"]).toBe("rulesync generate -t bar");
|
|
1149
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
|
|
1150
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
931
1151
|
});
|
|
932
1152
|
|
|
933
1153
|
it("keeps dest when it already starts with the new src value plus suffix", () => {
|
|
@@ -941,7 +1161,8 @@ describe("mergePackageJson", () => {
|
|
|
941
1161
|
fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ scripts: { "rulesync:generate": cur } }));
|
|
942
1162
|
mergePackageJson(dest, pkgRoot);
|
|
943
1163
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
944
|
-
expect(out.scripts["rulesync:generate"]).toBe(cur);
|
|
1164
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe(cur);
|
|
1165
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
945
1166
|
});
|
|
946
1167
|
|
|
947
1168
|
it("keeps unrelated custom script values and dest-only keys, warns on skip", () => {
|
|
@@ -957,9 +1178,11 @@ describe("mergePackageJson", () => {
|
|
|
957
1178
|
mergePackageJson(dest, pkgRoot);
|
|
958
1179
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
959
1180
|
expect(out.scripts.alpha).toBe("echo custom");
|
|
1181
|
+
expect(out.scripts["grunt:alpha"]).toBe("a");
|
|
960
1182
|
expect(out.scripts.foo).toBe("bar");
|
|
961
1183
|
expect(out.scripts.test).toBe("jest");
|
|
962
|
-
expect(out.scripts
|
|
1184
|
+
expect(out.scripts["grunt:zeta"]).toBe("z");
|
|
1185
|
+
expect(out.scripts.zeta).toBeUndefined();
|
|
963
1186
|
expect(warnSpy.mock.calls.some((c) => String(c[0]).includes("alpha") && String(c[0]).includes("untouched"))).toBe(
|
|
964
1187
|
true,
|
|
965
1188
|
);
|
|
@@ -980,7 +1203,8 @@ describe("mergePackageJson", () => {
|
|
|
980
1203
|
const dest = tmp("pj-raw-src-");
|
|
981
1204
|
mergePackageJson(dest, pkgRoot);
|
|
982
1205
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
983
|
-
expect(out.scripts["rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
|
|
1206
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
|
|
1207
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
984
1208
|
expect(out.scripts["rulesync:generate:raw"]).toBeUndefined();
|
|
985
1209
|
expect(out.scripts["rulesync:check:raw"]).toBeUndefined();
|
|
986
1210
|
expect(out.scripts["rulesync:watch:raw"]).toBeUndefined();
|
|
@@ -1001,7 +1225,7 @@ describe("mergePackageJson", () => {
|
|
|
1001
1225
|
);
|
|
1002
1226
|
mergePackageJson(dest, pkgRoot);
|
|
1003
1227
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1004
|
-
expect(out.scripts["rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
|
|
1228
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
|
|
1005
1229
|
expect(out.scripts["rulesync:generate:raw"]).toBe("echo consumer-raw");
|
|
1006
1230
|
});
|
|
1007
1231
|
|
|
@@ -1020,7 +1244,9 @@ describe("mergePackageJson", () => {
|
|
|
1020
1244
|
);
|
|
1021
1245
|
mergePackageJson(dest, pkgRoot);
|
|
1022
1246
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1023
|
-
expect(out.scripts["rulesync:generate"]).toBe("rulesync generate -t bar");
|
|
1247
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
|
|
1248
|
+
expect(out.scripts["grunt:sync:globals:apply"]).toBe("node scripts/sync-global-settings.mjs --apply");
|
|
1249
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
1024
1250
|
});
|
|
1025
1251
|
|
|
1026
1252
|
it("early return when name is @lovrozagar/grunt", () => {
|
|
@@ -1040,9 +1266,257 @@ describe("mergePackageJson", () => {
|
|
|
1040
1266
|
const dest = tmp("pj-noscripts-");
|
|
1041
1267
|
mergePackageJson(dest, pkgRoot);
|
|
1042
1268
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1043
|
-
expect(out.scripts).toEqual({
|
|
1269
|
+
expect(out.scripts).toEqual({
|
|
1270
|
+
"grunt:antigravity": LAUNCH_SCRIPTS.antigravity,
|
|
1271
|
+
"grunt:claude": LAUNCH_SCRIPTS.claude,
|
|
1272
|
+
"grunt:codex": LAUNCH_SCRIPTS.codex,
|
|
1273
|
+
"grunt:gemini": LAUNCH_SCRIPTS.gemini,
|
|
1274
|
+
"grunt:grok": LAUNCH_SCRIPTS.grok,
|
|
1275
|
+
});
|
|
1044
1276
|
expect(out.devDependencies).toEqual({ rulesync: "2", "smol-toml": "1" });
|
|
1045
1277
|
});
|
|
1278
|
+
|
|
1279
|
+
it("golden autorun-apps: suffix keep, doctor move, check npm run rewrite, old keys gone", () => {
|
|
1280
|
+
const pkgRoot = stubPkgRoot({
|
|
1281
|
+
name: "fixture-pkg",
|
|
1282
|
+
scripts: {
|
|
1283
|
+
"rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
|
|
1284
|
+
"rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
|
|
1285
|
+
"rulesync:check": "node ./scripts/guarded-roots.mjs check",
|
|
1286
|
+
"hooks:union": "node scripts/hooks-union.mjs",
|
|
1287
|
+
"hooks:check": "node scripts/hooks-union.mjs --check",
|
|
1288
|
+
doctor: "node ./scripts/doctor.mjs",
|
|
1289
|
+
"rulesync:doctor": "rulesync doctor",
|
|
1290
|
+
"sync:globals": "node scripts/sync-global-settings.mjs",
|
|
1291
|
+
"sync:globals:check": "node scripts/check-globals.mjs",
|
|
1292
|
+
"sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
|
|
1293
|
+
"purge:global-mcps": "node scripts/purge-global-mcps.mjs",
|
|
1294
|
+
"purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
|
|
1295
|
+
test: "vitest run --coverage",
|
|
1296
|
+
},
|
|
1297
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1298
|
+
});
|
|
1299
|
+
const dest = tmp("pj-autorun-");
|
|
1300
|
+
fs.writeFileSync(
|
|
1301
|
+
path.join(dest, "package.json"),
|
|
1302
|
+
JSON.stringify({
|
|
1303
|
+
scripts: {
|
|
1304
|
+
"apps:doctor": "node scripts/audit.mjs",
|
|
1305
|
+
"apps:mental-model:viewer:build":
|
|
1306
|
+
"npm --prefix scripts/excalidraw/viewer install && npm --prefix scripts/excalidraw/viewer run build",
|
|
1307
|
+
check:
|
|
1308
|
+
"node scripts/portability-check.mjs && npm run rulesync:check && npm run lint && npm test && node scripts/media-fixtures.mjs --check",
|
|
1309
|
+
"codex:sync": "node scripts/codex-sync.mjs",
|
|
1310
|
+
doctor: "node ./scripts/doctor.mjs",
|
|
1311
|
+
"hooks:check": "node scripts/hooks-union.mjs --check",
|
|
1312
|
+
"hooks:union": "node scripts/hooks-union.mjs",
|
|
1313
|
+
lint: "eslint scripts tests eslint.config.mjs",
|
|
1314
|
+
"purge:global-mcps": "node scripts/purge-global-mcps.mjs",
|
|
1315
|
+
"purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
|
|
1316
|
+
"rulesync:check": "node ./scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
|
|
1317
|
+
"rulesync:doctor": "rulesync doctor",
|
|
1318
|
+
"rulesync:generate": "node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
|
|
1319
|
+
"rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
|
|
1320
|
+
"sync:globals": "node scripts/sync-global-settings.mjs",
|
|
1321
|
+
"sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
|
|
1322
|
+
"sync:globals:check": "node scripts/check-globals.mjs",
|
|
1323
|
+
test: "node scripts/run-tests.mjs",
|
|
1324
|
+
},
|
|
1325
|
+
}),
|
|
1326
|
+
);
|
|
1327
|
+
mergePackageJson(dest, pkgRoot);
|
|
1328
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1329
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe(
|
|
1330
|
+
"node ./scripts/guarded-roots.mjs generate && node scripts/codex-sync.mjs",
|
|
1331
|
+
);
|
|
1332
|
+
expect(out.scripts["grunt:rulesync:check"]).toBe(
|
|
1333
|
+
"node ./scripts/guarded-roots.mjs check && node scripts/codex-sync.mjs --check",
|
|
1334
|
+
);
|
|
1335
|
+
expect(out.scripts["grunt:rulesync:watch"]).toBe("node ./scripts/guarded-roots.mjs watch");
|
|
1336
|
+
expect(out.scripts["grunt:rulesync:doctor"]).toBe("rulesync doctor");
|
|
1337
|
+
expect(out.scripts["grunt:hooks:union"]).toBe("node scripts/hooks-union.mjs");
|
|
1338
|
+
expect(out.scripts["grunt:hooks:check"]).toBe("node scripts/hooks-union.mjs --check");
|
|
1339
|
+
expect(out.scripts["grunt:sync:globals"]).toBe("node scripts/sync-global-settings.mjs");
|
|
1340
|
+
expect(out.scripts["grunt:sync:globals:apply"]).toBe("node scripts/sync-global-settings.mjs --apply");
|
|
1341
|
+
expect(out.scripts["grunt:sync:globals:check"]).toBe("node scripts/check-globals.mjs");
|
|
1342
|
+
expect(out.scripts["grunt:purge:global-mcps"]).toBe("node scripts/purge-global-mcps.mjs");
|
|
1343
|
+
expect(out.scripts["grunt:purge:global-mcps:apply"]).toBe("node scripts/purge-global-mcps.mjs --apply");
|
|
1344
|
+
expect(out.scripts["grunt:doctor"]).toBe("node ./scripts/doctor.mjs");
|
|
1345
|
+
expect(out.scripts.doctor).toBeUndefined();
|
|
1346
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
1347
|
+
expect(out.scripts["rulesync:check"]).toBeUndefined();
|
|
1348
|
+
expect(out.scripts["rulesync:watch"]).toBeUndefined();
|
|
1349
|
+
expect(out.scripts["hooks:union"]).toBeUndefined();
|
|
1350
|
+
expect(out.scripts["sync:globals"]).toBeUndefined();
|
|
1351
|
+
expect(out.scripts.check).toBe(
|
|
1352
|
+
"node scripts/portability-check.mjs && npm run grunt:rulesync:check && npm run lint && npm test && node scripts/media-fixtures.mjs --check",
|
|
1353
|
+
);
|
|
1354
|
+
expect(out.scripts["apps:doctor"]).toBe("node scripts/audit.mjs");
|
|
1355
|
+
expect(out.scripts["codex:sync"]).toBe("node scripts/codex-sync.mjs");
|
|
1356
|
+
expect(out.scripts.lint).toBe("eslint scripts tests eslint.config.mjs");
|
|
1357
|
+
expect(out.scripts.test).toBe("node scripts/run-tests.mjs");
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
it("leaves custom doctor and writes grunt:doctor from SoT", () => {
|
|
1361
|
+
const pkgRoot = stubPkgRoot({
|
|
1362
|
+
name: "fixture-pkg",
|
|
1363
|
+
scripts: { doctor: "node ./scripts/doctor.mjs" },
|
|
1364
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1365
|
+
});
|
|
1366
|
+
const dest = tmp("pj-custom-doctor-");
|
|
1367
|
+
fs.writeFileSync(
|
|
1368
|
+
path.join(dest, "package.json"),
|
|
1369
|
+
JSON.stringify({ scripts: { doctor: "node scripts/audit.mjs" } }),
|
|
1370
|
+
);
|
|
1371
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1372
|
+
mergePackageJson(dest, pkgRoot);
|
|
1373
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1374
|
+
expect(out.scripts.doctor).toBe("node scripts/audit.mjs");
|
|
1375
|
+
expect(out.scripts["grunt:doctor"]).toBe("node ./scripts/doctor.mjs");
|
|
1376
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
1377
|
+
warnSpy.mockRestore();
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
it("merges destKey when both prefixed and owned legacy exist, deletes legacy", () => {
|
|
1381
|
+
const pkgRoot = stubPkgRoot({
|
|
1382
|
+
name: "fixture-pkg",
|
|
1383
|
+
scripts: { "rulesync:generate": "rulesync generate -t bar" },
|
|
1384
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1385
|
+
});
|
|
1386
|
+
const dest = tmp("pj-both-");
|
|
1387
|
+
fs.writeFileSync(
|
|
1388
|
+
path.join(dest, "package.json"),
|
|
1389
|
+
JSON.stringify({
|
|
1390
|
+
scripts: {
|
|
1391
|
+
"rulesync:generate": "rulesync generate -t foo && node scripts/codex-sync.mjs",
|
|
1392
|
+
"grunt:rulesync:generate": "rulesync generate -t old && node scripts/other.mjs",
|
|
1393
|
+
},
|
|
1394
|
+
}),
|
|
1395
|
+
);
|
|
1396
|
+
mergePackageJson(dest, pkgRoot);
|
|
1397
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1398
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe(
|
|
1399
|
+
"rulesync generate -t bar && node scripts/other.mjs",
|
|
1400
|
+
);
|
|
1401
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
it("does not rewrite npm run rulesync:check:raw when only rulesync:check migrated", () => {
|
|
1405
|
+
const pkgRoot = stubPkgRoot({
|
|
1406
|
+
name: "fixture-pkg",
|
|
1407
|
+
scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
|
|
1408
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1409
|
+
});
|
|
1410
|
+
const dest = tmp("pj-raw-rewrite-");
|
|
1411
|
+
fs.writeFileSync(
|
|
1412
|
+
path.join(dest, "package.json"),
|
|
1413
|
+
JSON.stringify({
|
|
1414
|
+
scripts: {
|
|
1415
|
+
"rulesync:check": "node ./scripts/guarded-roots.mjs check",
|
|
1416
|
+
"rulesync:check:raw": "echo consumer-raw",
|
|
1417
|
+
check: "npm run rulesync:check && npm run rulesync:check:raw",
|
|
1418
|
+
},
|
|
1419
|
+
}),
|
|
1420
|
+
);
|
|
1421
|
+
mergePackageJson(dest, pkgRoot);
|
|
1422
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1423
|
+
expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
|
|
1424
|
+
expect(out.scripts["rulesync:check"]).toBeUndefined();
|
|
1425
|
+
expect(out.scripts["rulesync:check:raw"]).toBe("echo consumer-raw");
|
|
1426
|
+
expect(out.scripts.check).toBe("npm run grunt:rulesync:check && npm run rulesync:check:raw");
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
it("skips non-string dest scripts when rewriting npm run refs", () => {
|
|
1430
|
+
const pkgRoot = stubPkgRoot({
|
|
1431
|
+
name: "fixture-pkg",
|
|
1432
|
+
scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
|
|
1433
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1434
|
+
});
|
|
1435
|
+
const dest = tmp("pj-nonstr-");
|
|
1436
|
+
fs.writeFileSync(
|
|
1437
|
+
path.join(dest, "package.json"),
|
|
1438
|
+
JSON.stringify({
|
|
1439
|
+
scripts: {
|
|
1440
|
+
extra: 1,
|
|
1441
|
+
check: "npm run rulesync:check",
|
|
1442
|
+
},
|
|
1443
|
+
}),
|
|
1444
|
+
);
|
|
1445
|
+
mergePackageJson(dest, pkgRoot);
|
|
1446
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1447
|
+
expect(out.scripts.extra).toBe(1);
|
|
1448
|
+
expect(out.scripts.check).toBe("npm run grunt:rulesync:check");
|
|
1449
|
+
expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
it("emits grunt:claude into a consumer package.json", () => {
|
|
1453
|
+
const pkgRoot = stubPkgRoot();
|
|
1454
|
+
const dest = tmp("pj-launch-emit-");
|
|
1455
|
+
mergePackageJson(dest, pkgRoot);
|
|
1456
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1457
|
+
expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
it("leaves a consumer's bare claude launcher untouched, no warn emitted", () => {
|
|
1461
|
+
const pkgRoot = stubPkgRoot();
|
|
1462
|
+
const dest = tmp("pj-consumer-claude-");
|
|
1463
|
+
fs.writeFileSync(
|
|
1464
|
+
path.join(dest, "package.json"),
|
|
1465
|
+
JSON.stringify({ scripts: { claude: "npm run build && claude" } }),
|
|
1466
|
+
);
|
|
1467
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1468
|
+
mergePackageJson(dest, pkgRoot);
|
|
1469
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1470
|
+
expect(out.scripts.claude).toBe("npm run build && claude");
|
|
1471
|
+
expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
|
|
1472
|
+
expect(warn).not.toHaveBeenCalled();
|
|
1473
|
+
warn.mockRestore();
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it("does not rewrite npm run claude inside a consumer script", () => {
|
|
1477
|
+
const pkgRoot = stubPkgRoot();
|
|
1478
|
+
const dest = tmp("pj-npmrun-claude-");
|
|
1479
|
+
fs.writeFileSync(
|
|
1480
|
+
path.join(dest, "package.json"),
|
|
1481
|
+
JSON.stringify({
|
|
1482
|
+
scripts: { claude: "npm run build && claude", start: "npm run claude" },
|
|
1483
|
+
}),
|
|
1484
|
+
);
|
|
1485
|
+
mergePackageJson(dest, pkgRoot);
|
|
1486
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1487
|
+
expect(out.scripts.start).toBe("npm run claude");
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
it("purges a stale grunt:yolo:claude with the exact old value; an edited variant survives", () => {
|
|
1491
|
+
const pkgRoot = stubPkgRoot();
|
|
1492
|
+
const dest = tmp("pj-purge-stale-");
|
|
1493
|
+
fs.writeFileSync(
|
|
1494
|
+
path.join(dest, "package.json"),
|
|
1495
|
+
JSON.stringify({
|
|
1496
|
+
scripts: {
|
|
1497
|
+
"grunt:yolo:claude": LAUNCH_SCRIPTS.claude,
|
|
1498
|
+
"grunt:yolo:codex": `${LAUNCH_SCRIPTS.codex} --extra`,
|
|
1499
|
+
},
|
|
1500
|
+
}),
|
|
1501
|
+
);
|
|
1502
|
+
mergePackageJson(dest, pkgRoot);
|
|
1503
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1504
|
+
expect(out.scripts["grunt:yolo:claude"]).toBeUndefined();
|
|
1505
|
+
expect(out.scripts["grunt:claude"]).toBe(LAUNCH_SCRIPTS.claude);
|
|
1506
|
+
expect(out.scripts["grunt:yolo:codex"]).toBe(`${LAUNCH_SCRIPTS.codex} --extra`);
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
it("drift: every bare launcher key in grunt's package.json equals LAUNCH_SCRIPTS[key]", () => {
|
|
1510
|
+
const pkg = JSON.parse(
|
|
1511
|
+
fs.readFileSync(
|
|
1512
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
1513
|
+
"utf8",
|
|
1514
|
+
),
|
|
1515
|
+
);
|
|
1516
|
+
for (const [k, v] of Object.entries(LAUNCH_SCRIPTS)) {
|
|
1517
|
+
expect(pkg.scripts[k]).toBe(v);
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1046
1520
|
});
|
|
1047
1521
|
|
|
1048
1522
|
describe("init", () => {
|
|
@@ -1076,6 +1550,9 @@ describe("init", () => {
|
|
|
1076
1550
|
if (d === ".claude") continue;
|
|
1077
1551
|
expect(fs.readFileSync(path.join(dest, d, "marker"), "utf8")).toBe(d);
|
|
1078
1552
|
}
|
|
1553
|
+
expect(readGruntConfig(dest)).toEqual(GRUNT_CONFIG_DEFAULTS);
|
|
1554
|
+
expect(PRODUCT_FILES).toContain("grunt-config.mjs");
|
|
1555
|
+
expect(fs.statSync(path.join(dest, "scripts", "grunt-config.mjs")).isFile()).toBe(true);
|
|
1079
1556
|
expect(fs.readFileSync(path.join(dest, ".claude", "marker"), "utf8")).toBe(".claude");
|
|
1080
1557
|
const settings = JSON.parse(
|
|
1081
1558
|
fs.readFileSync(path.join(dest, ".claude", "settings.json"), "utf8"),
|
|
@@ -1103,9 +1580,9 @@ describe("init", () => {
|
|
|
1103
1580
|
|
|
1104
1581
|
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
1105
1582
|
["npm", ["install"]],
|
|
1106
|
-
["npm", ["run", "rulesync:generate"]],
|
|
1107
|
-
["npm", ["run", "sync:globals:apply"]],
|
|
1108
|
-
["npm", ["run", "rulesync:check"]],
|
|
1583
|
+
["npm", ["run", "grunt:rulesync:generate"]],
|
|
1584
|
+
["npm", ["run", "grunt:sync:globals:apply"]],
|
|
1585
|
+
["npm", ["run", "grunt:rulesync:check"]],
|
|
1109
1586
|
]);
|
|
1110
1587
|
for (const call of exec.mock.calls) {
|
|
1111
1588
|
expect(call[2]).toEqual({ cwd: dest, stdio: "inherit" });
|
|
@@ -1154,13 +1631,21 @@ describe("init", () => {
|
|
|
1154
1631
|
const exec = vi.fn();
|
|
1155
1632
|
init(dest, { execFileSync: exec });
|
|
1156
1633
|
expect(fs.existsSync(path.join(dest, "AGENTS.md"))).toBe(true);
|
|
1634
|
+
const agents = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
|
|
1635
|
+
const claude = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
|
|
1636
|
+
expect(agents.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
1637
|
+
expect(agents.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
1638
|
+
expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
1639
|
+
expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
1157
1640
|
expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
|
|
1641
|
+
expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(true);
|
|
1158
1642
|
expect(fs.existsSync(path.join(dest, ".claude", "settings.json"))).toBe(true);
|
|
1643
|
+
expect(readGruntConfig(dest)).toEqual(GRUNT_CONFIG_DEFAULTS);
|
|
1159
1644
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1160
1645
|
["install"],
|
|
1161
|
-
["run", "rulesync:generate"],
|
|
1162
|
-
["run", "sync:globals:apply"],
|
|
1163
|
-
["run", "rulesync:check"],
|
|
1646
|
+
["run", "grunt:rulesync:generate"],
|
|
1647
|
+
["run", "grunt:sync:globals:apply"],
|
|
1648
|
+
["run", "grunt:rulesync:check"],
|
|
1164
1649
|
]);
|
|
1165
1650
|
});
|
|
1166
1651
|
|
|
@@ -1171,8 +1656,8 @@ describe("init", () => {
|
|
|
1171
1656
|
init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true });
|
|
1172
1657
|
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
1173
1658
|
["npm", ["install"]],
|
|
1174
|
-
["npm", ["run", "rulesync:generate"]],
|
|
1175
|
-
["npm", ["run", "rulesync:check"]],
|
|
1659
|
+
["npm", ["run", "grunt:rulesync:generate"]],
|
|
1660
|
+
["npm", ["run", "grunt:rulesync:check"]],
|
|
1176
1661
|
]);
|
|
1177
1662
|
});
|
|
1178
1663
|
|
|
@@ -1187,9 +1672,9 @@ describe("init", () => {
|
|
|
1187
1672
|
init(dest, { pkgRoot, execFileSync: exec, applyGlobals: true });
|
|
1188
1673
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1189
1674
|
["install"],
|
|
1190
|
-
["run", "rulesync:generate"],
|
|
1191
|
-
["run", "sync:globals:apply"],
|
|
1192
|
-
["run", "rulesync:check"],
|
|
1675
|
+
["run", "grunt:rulesync:generate"],
|
|
1676
|
+
["run", "grunt:sync:globals:apply"],
|
|
1677
|
+
["run", "grunt:rulesync:check"],
|
|
1193
1678
|
]);
|
|
1194
1679
|
});
|
|
1195
1680
|
|
|
@@ -1204,8 +1689,8 @@ describe("init", () => {
|
|
|
1204
1689
|
init(dest, { pkgRoot, execFileSync: exec });
|
|
1205
1690
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1206
1691
|
["install"],
|
|
1207
|
-
["run", "rulesync:generate"],
|
|
1208
|
-
["run", "rulesync:check"],
|
|
1692
|
+
["run", "grunt:rulesync:generate"],
|
|
1693
|
+
["run", "grunt:rulesync:check"],
|
|
1209
1694
|
]);
|
|
1210
1695
|
});
|
|
1211
1696
|
|
|
@@ -1242,9 +1727,9 @@ describe("init", () => {
|
|
|
1242
1727
|
expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
|
|
1243
1728
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1244
1729
|
["install"],
|
|
1245
|
-
["run", "rulesync:generate"],
|
|
1246
|
-
["run", "sync:globals:apply"],
|
|
1247
|
-
["run", "rulesync:check"],
|
|
1730
|
+
["run", "grunt:rulesync:generate"],
|
|
1731
|
+
["run", "grunt:sync:globals:apply"],
|
|
1732
|
+
["run", "grunt:rulesync:check"],
|
|
1248
1733
|
]);
|
|
1249
1734
|
});
|
|
1250
1735
|
|
|
@@ -1255,7 +1740,7 @@ describe("init", () => {
|
|
|
1255
1740
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "consumer claude\n");
|
|
1256
1741
|
fs.writeFileSync(path.join(dest, "GEMINI.md"), "consumer gemini\n");
|
|
1257
1742
|
const exec = vi.fn((_cmd, args) => {
|
|
1258
|
-
if (Array.isArray(args) && args.includes("rulesync:generate")) {
|
|
1743
|
+
if (Array.isArray(args) && args.includes("grunt:rulesync:generate")) {
|
|
1259
1744
|
fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
|
|
1260
1745
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
|
|
1261
1746
|
fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
|
|
@@ -1280,6 +1765,32 @@ describe("init", () => {
|
|
|
1280
1765
|
);
|
|
1281
1766
|
});
|
|
1282
1767
|
|
|
1768
|
+
it("wrapped package src + generate clobber stays begin=1 end=1", () => {
|
|
1769
|
+
const pkgRoot = stubPkgRoot();
|
|
1770
|
+
for (const f of GUARDED_MD_FILES) {
|
|
1771
|
+
fs.writeFileSync(
|
|
1772
|
+
path.join(pkgRoot, f),
|
|
1773
|
+
`<!-- grunt:begin -->\n${f} content\n<!-- grunt:end -->\n`,
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
const dest = tmp("grunt-wrap-gen-");
|
|
1777
|
+
const exec = vi.fn((_cmd, args) => {
|
|
1778
|
+
if (Array.isArray(args) && args.includes("grunt:rulesync:generate")) {
|
|
1779
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
|
|
1780
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
init(dest, { pkgRoot, execFileSync: exec });
|
|
1784
|
+
const agents = fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8");
|
|
1785
|
+
const claude = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
|
|
1786
|
+
expect(agents).toBe("<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\n");
|
|
1787
|
+
expect(claude).toBe("<!-- grunt:begin -->\ngenerated claude\n<!-- grunt:end -->\n");
|
|
1788
|
+
expect(agents.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
1789
|
+
expect(agents.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
1790
|
+
expect(claude.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
1791
|
+
expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1283
1794
|
it("onPhase merge then npm phases stop before exec", () => {
|
|
1284
1795
|
const pkgRoot = stubPkgRoot();
|
|
1285
1796
|
const dest = tmp("grunt-onphase-");
|
|
@@ -1300,9 +1811,9 @@ describe("init", () => {
|
|
|
1300
1811
|
]);
|
|
1301
1812
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1302
1813
|
["install"],
|
|
1303
|
-
["run", "rulesync:generate"],
|
|
1304
|
-
["run", "sync:globals:apply"],
|
|
1305
|
-
["run", "rulesync:check"],
|
|
1814
|
+
["run", "grunt:rulesync:generate"],
|
|
1815
|
+
["run", "grunt:sync:globals:apply"],
|
|
1816
|
+
["run", "grunt:rulesync:check"],
|
|
1306
1817
|
]);
|
|
1307
1818
|
});
|
|
1308
1819
|
|