@lovrozagar/grunt 0.4.2 → 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 +19 -13
- package/.agents/agents/thinker.md +8 -6
- 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/commit-and-push/SKILL.md +3 -3
- package/.agents/skills/explain/SKILL.md +1 -0
- package/.agents/skills/handoff/SKILL.md +1 -1
- package/.agents/skills/implement-plan/SKILL.md +117 -0
- package/.agents/skills/solo/SKILL.md +5 -3
- package/.agents/skills/tmp/SKILL.md +58 -0
- package/.agents/skills/write-plan/SKILL.md +85 -0
- package/.claude/agents/grunt.md +2 -1
- package/.claude/agents/implementer.md +1 -1
- package/.claude/agents/orchestrator.md +19 -13
- package/.claude/agents/thinker.md +8 -6
- 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/commit-and-push/SKILL.md +3 -3
- package/.claude/skills/explain/SKILL.md +1 -0
- package/.claude/skills/handoff/SKILL.md +1 -1
- package/.claude/skills/implement-plan/SKILL.md +117 -0
- package/.claude/skills/solo/SKILL.md +5 -3
- package/.claude/skills/tmp/SKILL.md +58 -0
- package/.claude/skills/write-plan/SKILL.md +85 -0
- package/.codex/agents/grunt.toml +2 -1
- package/.codex/agents/implementer.toml +1 -1
- package/.codex/agents/orchestrator.toml +19 -13
- package/.codex/agents/thinker.toml +8 -6
- package/.grok/agents/grunt.md +2 -1
- package/.grok/agents/implementer.md +1 -1
- package/.grok/agents/orchestrator.md +19 -13
- package/.grok/agents/thinker.md +8 -6
- 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/commit-and-push/SKILL.md +3 -3
- package/.grok/skills/explain/SKILL.md +1 -0
- package/.grok/skills/handoff/SKILL.md +1 -1
- package/.grok/skills/implement-plan/SKILL.md +49 -20
- package/.grok/skills/solo/SKILL.md +5 -3
- package/.grok/skills/tmp/SKILL.md +58 -0
- package/.grok/skills/write-plan/SKILL.md +17 -14
- package/.rulesync/grunt.config.jsonc +7 -0
- package/.rulesync/reference/INDEX.md +46 -0
- package/.rulesync/reference/cascade.md +19 -15
- package/.rulesync/reference/hooks.md +6 -4
- package/.rulesync/reference/law.md +9 -0
- package/.rulesync/reference/map.md +20 -10
- package/.rulesync/reference/output.md +23 -8
- package/{.grok/skills/shared → .rulesync/reference}/plan-format.md +12 -11
- package/.rulesync/reference/refs-map.md +14 -0
- package/.rulesync/reference/skills-map.md +23 -0
- package/.rulesync/rules/CLAUDE.md +22 -15
- package/.rulesync/rules/overview.md +22 -15
- 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/commit-and-push/SKILL.md +3 -3
- package/.rulesync/skills/explain/SKILL.md +1 -0
- package/.rulesync/skills/handoff/SKILL.md +1 -1
- package/.rulesync/skills/implement-plan/SKILL.md +127 -0
- package/.rulesync/skills/solo/SKILL.md +5 -3
- package/.rulesync/skills/tmp/SKILL.md +58 -0
- package/.rulesync/skills/write-plan/SKILL.md +92 -0
- package/.rulesync/subagents/grunt.md +2 -1
- package/.rulesync/subagents/implementer.md +1 -1
- package/.rulesync/subagents/orchestrator.md +19 -13
- package/.rulesync/subagents/thinker.md +8 -6
- package/AGENTS.md +22 -15
- package/CHANGELOG.md +56 -0
- package/CLAUDE.md +22 -15
- package/README.md +27 -22
- package/cli/grunt.mjs +12 -12
- package/cli/grunt.test.ts +29 -20
- package/cli/init.mjs +135 -22
- package/cli/init.test.ts +624 -36
- package/package.json +11 -4
- package/scripts/doctor.mjs +64 -2
- package/scripts/emit-maps.mjs +422 -0
- package/scripts/gate-fat-tools.mjs +2 -2
- package/scripts/grunt-config.mjs +50 -0
- package/scripts/guarded-roots.mjs +4 -7
- package/scripts/persist-plan.mjs +17 -2
- package/scripts/persist-tmp.mjs +123 -0
- package/scripts/pipeline.mjs +67 -0
- package/scripts/scrub-spawn-prompt.mjs +1 -1
- package/scripts/skill-conflicts.mjs +71 -0
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,18 +46,28 @@ 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",
|
|
45
58
|
"emit-agent-shell-tools.mjs",
|
|
46
59
|
"emit-gemini.mjs",
|
|
60
|
+
"emit-maps.mjs",
|
|
47
61
|
"guarded-roots.mjs",
|
|
48
62
|
"emit-mcp-policy.mjs",
|
|
49
63
|
"gate-fat-tools.mjs",
|
|
50
64
|
"hooks-union.mjs",
|
|
65
|
+
"pipeline.mjs",
|
|
51
66
|
"grunt-job.mjs",
|
|
67
|
+
"grunt-config.mjs",
|
|
52
68
|
"parse-need.mjs",
|
|
53
69
|
"persist-handoff.mjs",
|
|
70
|
+
"persist-tmp.mjs",
|
|
54
71
|
"persist-plan.mjs",
|
|
55
72
|
"purge-global-mcps.mjs",
|
|
56
73
|
"scrub-spawn-prompt.mjs",
|
|
@@ -58,6 +75,7 @@ const PRODUCT_FILES = [
|
|
|
58
75
|
"sync-global-settings.mjs",
|
|
59
76
|
"browser.mjs",
|
|
60
77
|
"doctor.mjs",
|
|
78
|
+
"skill-conflicts.mjs",
|
|
61
79
|
];
|
|
62
80
|
|
|
63
81
|
const SRC_CLAUDE_SETTINGS = {
|
|
@@ -100,6 +118,12 @@ const SRC_CLAUDE_SETTINGS = {
|
|
|
100
118
|
enabledMcpjsonServers: [],
|
|
101
119
|
};
|
|
102
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
|
+
|
|
103
127
|
function stubPkgRoot(pkg: Record<string, unknown> = {
|
|
104
128
|
name: "fixture-pkg",
|
|
105
129
|
scripts: {
|
|
@@ -119,6 +143,10 @@ function stubPkgRoot(pkg: Record<string, unknown> = {
|
|
|
119
143
|
fs.mkdirSync(path.join(root, d));
|
|
120
144
|
fs.writeFileSync(path.join(root, d, "marker"), d);
|
|
121
145
|
}
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
path.join(root, GRUNT_CONFIG_REL),
|
|
148
|
+
`${JSON.stringify(GRUNT_CONFIG_DEFAULTS, null, 2)}\n`,
|
|
149
|
+
);
|
|
122
150
|
fs.writeFileSync(
|
|
123
151
|
path.join(root, ".claude", "settings.json"),
|
|
124
152
|
`${JSON.stringify(SRC_CLAUDE_SETTINGS, null, 2)}\n`,
|
|
@@ -287,6 +315,67 @@ describe("mergeGuardedMarkdown", () => {
|
|
|
287
315
|
);
|
|
288
316
|
});
|
|
289
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
|
+
|
|
290
379
|
it("does not fuzzy-dedupe unmarked protocol-looking user text", () => {
|
|
291
380
|
const pkgRoot = stubPkgRoot();
|
|
292
381
|
const dest = tmp("md-proto-");
|
|
@@ -360,6 +449,23 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
360
449
|
);
|
|
361
450
|
});
|
|
362
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
|
+
|
|
363
469
|
it("withGuardedCheckInteriors is restored even if check throws", () => {
|
|
364
470
|
const dest = tmp("check-int-");
|
|
365
471
|
const live =
|
|
@@ -375,6 +481,22 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
375
481
|
});
|
|
376
482
|
|
|
377
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");
|
|
378
500
|
expect(composeGuardedMarkdown("body", null)).toBe(
|
|
379
501
|
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
|
|
380
502
|
);
|
|
@@ -399,7 +521,9 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
399
521
|
expect(mergeGuardedContent("keep\n", "g\r\n")).toBe(
|
|
400
522
|
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\nkeep\n",
|
|
401
523
|
);
|
|
402
|
-
expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).
|
|
524
|
+
expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toBe(
|
|
525
|
+
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
|
|
526
|
+
);
|
|
403
527
|
const dest = tmp("md-helpers-");
|
|
404
528
|
const missing = path.join(dest, "nope.md");
|
|
405
529
|
expect(guardedMarkdownDrift(missing, "g")).toBe(true);
|
|
@@ -420,6 +544,73 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
420
544
|
expect(writeMergedGuardedFile(huge, "g")).toBe("aborted-unsafe");
|
|
421
545
|
});
|
|
422
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
|
+
|
|
423
614
|
it("remerge restores unsafe, missing, binary current, and created files", () => {
|
|
424
615
|
const dest = tmp("remerge-edges-");
|
|
425
616
|
const agents = path.join(dest, "AGENTS.md");
|
|
@@ -495,8 +686,15 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
495
686
|
fs.writeFileSync(path.join(dest, "AGENTS.md"), "fresh ssot\n");
|
|
496
687
|
});
|
|
497
688
|
runGuardedRoots("generate", { cwd: dest, exec });
|
|
498
|
-
expect(exec).
|
|
499
|
-
|
|
689
|
+
expect(exec.mock.calls.map((c) => c[0])).toEqual([
|
|
690
|
+
"rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills",
|
|
691
|
+
"rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks",
|
|
692
|
+
"node scripts/emit-mcp-policy.mjs",
|
|
693
|
+
"node scripts/emit-gemini.mjs",
|
|
694
|
+
"node scripts/emit-agent-shell-tools.mjs",
|
|
695
|
+
"node scripts/emit-maps.mjs",
|
|
696
|
+
"node scripts/hooks-union.mjs",
|
|
697
|
+
]);
|
|
500
698
|
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
501
699
|
"<!-- grunt:begin -->\nfresh ssot\n<!-- grunt:end -->\nkeep me\n",
|
|
502
700
|
);
|
|
@@ -595,8 +793,12 @@ describe("snapshot/remerge guarded roots", () => {
|
|
|
595
793
|
return stop;
|
|
596
794
|
},
|
|
597
795
|
});
|
|
598
|
-
expect(exec).
|
|
599
|
-
|
|
796
|
+
expect(exec.mock.calls.map((c) => c[0])).toEqual([
|
|
797
|
+
"node scripts/emit-mcp-policy.mjs",
|
|
798
|
+
"node scripts/emit-gemini.mjs",
|
|
799
|
+
"node scripts/emit-agent-shell-tools.mjs",
|
|
800
|
+
"rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills --watch",
|
|
801
|
+
]);
|
|
600
802
|
expect(stop).toHaveBeenCalledTimes(1);
|
|
601
803
|
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
602
804
|
"<!-- grunt:begin -->\nwatch2\n<!-- grunt:end -->\nkeep me\n",
|
|
@@ -772,6 +974,13 @@ describe("mergeClaudeSettings", () => {
|
|
|
772
974
|
});
|
|
773
975
|
});
|
|
774
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
|
+
|
|
775
984
|
describe("mergePackageJson", () => {
|
|
776
985
|
it("creates {} dest and merges scripts skipping test, injects deps, sorts keys", () => {
|
|
777
986
|
const pkgRoot = stubPkgRoot();
|
|
@@ -779,11 +988,25 @@ describe("mergePackageJson", () => {
|
|
|
779
988
|
mergePackageJson(dest, pkgRoot);
|
|
780
989
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
781
990
|
expect(out.scripts).toEqual({
|
|
782
|
-
alpha: "a",
|
|
783
|
-
"
|
|
784
|
-
|
|
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",
|
|
785
999
|
});
|
|
786
|
-
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
|
+
]);
|
|
787
1010
|
expect(out.devDependencies).toEqual({
|
|
788
1011
|
rulesync: "latest",
|
|
789
1012
|
"smol-toml": "^1.8.0",
|
|
@@ -803,14 +1026,21 @@ describe("mergePackageJson", () => {
|
|
|
803
1026
|
devDependencies: { lodash: "4" },
|
|
804
1027
|
}),
|
|
805
1028
|
);
|
|
1029
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
806
1030
|
mergePackageJson(dest, pkgRoot);
|
|
807
1031
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
808
1032
|
expect(out.scripts.test).toBe("jest");
|
|
809
1033
|
expect(out.scripts.alpha).toBe("old");
|
|
1034
|
+
expect(out.scripts["grunt:alpha"]).toBe("a");
|
|
1035
|
+
expect(out.scripts["grunt:zeta"]).toBe("z");
|
|
810
1036
|
expect(out.scripts.foo).toBe("bar");
|
|
811
1037
|
expect(out.devDependencies.lodash).toBe("4");
|
|
812
1038
|
expect(out.devDependencies["smol-toml"]).toBe("^1.8.0");
|
|
813
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();
|
|
814
1044
|
});
|
|
815
1045
|
|
|
816
1046
|
it("preserves dest suffix after an upgraded grunt script prefix", () => {
|
|
@@ -830,9 +1060,10 @@ describe("mergePackageJson", () => {
|
|
|
830
1060
|
);
|
|
831
1061
|
mergePackageJson(dest, pkgRoot);
|
|
832
1062
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
833
|
-
expect(out.scripts["rulesync:generate"]).toBe(
|
|
1063
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe(
|
|
834
1064
|
"rulesync generate -t bar && node scripts/codex-sync.mjs",
|
|
835
1065
|
);
|
|
1066
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
836
1067
|
});
|
|
837
1068
|
|
|
838
1069
|
it("preserves suffix after a multi-command grunt script upgrade", () => {
|
|
@@ -855,7 +1086,7 @@ describe("mergePackageJson", () => {
|
|
|
855
1086
|
);
|
|
856
1087
|
mergePackageJson(dest, pkgRoot);
|
|
857
1088
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
858
|
-
expect(out.scripts["rulesync:generate"]).toBe(
|
|
1089
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe(
|
|
859
1090
|
"rulesync generate -t bar && node scripts/emit.mjs && node scripts/codex-sync.mjs",
|
|
860
1091
|
);
|
|
861
1092
|
});
|
|
@@ -875,6 +1106,7 @@ describe("mergePackageJson", () => {
|
|
|
875
1106
|
mergePackageJson(dest, pkgRoot);
|
|
876
1107
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
877
1108
|
expect(out.scripts.alpha).toBe("echo custom && extra");
|
|
1109
|
+
expect(out.scripts["grunt:alpha"]).toBe("a");
|
|
878
1110
|
expect(warnSpy).toHaveBeenCalled();
|
|
879
1111
|
warnSpy.mockRestore();
|
|
880
1112
|
});
|
|
@@ -896,6 +1128,7 @@ describe("mergePackageJson", () => {
|
|
|
896
1128
|
mergePackageJson(dest, pkgRoot);
|
|
897
1129
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
898
1130
|
expect(out.scripts["rulesync:generate"]).toBe("echo custom;echo extra");
|
|
1131
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe("rulesync generate -t bar");
|
|
899
1132
|
expect(warnSpy).toHaveBeenCalled();
|
|
900
1133
|
warnSpy.mockRestore();
|
|
901
1134
|
});
|
|
@@ -913,7 +1146,8 @@ describe("mergePackageJson", () => {
|
|
|
913
1146
|
);
|
|
914
1147
|
mergePackageJson(dest, pkgRoot);
|
|
915
1148
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
916
|
-
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();
|
|
917
1151
|
});
|
|
918
1152
|
|
|
919
1153
|
it("keeps dest when it already starts with the new src value plus suffix", () => {
|
|
@@ -927,7 +1161,8 @@ describe("mergePackageJson", () => {
|
|
|
927
1161
|
fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ scripts: { "rulesync:generate": cur } }));
|
|
928
1162
|
mergePackageJson(dest, pkgRoot);
|
|
929
1163
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
930
|
-
expect(out.scripts["rulesync:generate"]).toBe(cur);
|
|
1164
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe(cur);
|
|
1165
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
931
1166
|
});
|
|
932
1167
|
|
|
933
1168
|
it("keeps unrelated custom script values and dest-only keys, warns on skip", () => {
|
|
@@ -943,15 +1178,57 @@ describe("mergePackageJson", () => {
|
|
|
943
1178
|
mergePackageJson(dest, pkgRoot);
|
|
944
1179
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
945
1180
|
expect(out.scripts.alpha).toBe("echo custom");
|
|
1181
|
+
expect(out.scripts["grunt:alpha"]).toBe("a");
|
|
946
1182
|
expect(out.scripts.foo).toBe("bar");
|
|
947
1183
|
expect(out.scripts.test).toBe("jest");
|
|
948
|
-
expect(out.scripts
|
|
1184
|
+
expect(out.scripts["grunt:zeta"]).toBe("z");
|
|
1185
|
+
expect(out.scripts.zeta).toBeUndefined();
|
|
949
1186
|
expect(warnSpy.mock.calls.some((c) => String(c[0]).includes("alpha") && String(c[0]).includes("untouched"))).toBe(
|
|
950
1187
|
true,
|
|
951
1188
|
);
|
|
952
1189
|
warnSpy.mockRestore();
|
|
953
1190
|
});
|
|
954
1191
|
|
|
1192
|
+
it("does not merge :raw scripts from src", () => {
|
|
1193
|
+
const pkgRoot = stubPkgRoot({
|
|
1194
|
+
name: "fixture-pkg",
|
|
1195
|
+
scripts: {
|
|
1196
|
+
"rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
|
|
1197
|
+
"rulesync:generate:raw": "echo raw",
|
|
1198
|
+
"rulesync:check:raw": "echo check-raw",
|
|
1199
|
+
"rulesync:watch:raw": "echo watch-raw",
|
|
1200
|
+
},
|
|
1201
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1202
|
+
});
|
|
1203
|
+
const dest = tmp("pj-raw-src-");
|
|
1204
|
+
mergePackageJson(dest, pkgRoot);
|
|
1205
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1206
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
|
|
1207
|
+
expect(out.scripts["rulesync:generate"]).toBeUndefined();
|
|
1208
|
+
expect(out.scripts["rulesync:generate:raw"]).toBeUndefined();
|
|
1209
|
+
expect(out.scripts["rulesync:check:raw"]).toBeUndefined();
|
|
1210
|
+
expect(out.scripts["rulesync:watch:raw"]).toBeUndefined();
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it("leaves dest-only :raw keys", () => {
|
|
1214
|
+
const pkgRoot = stubPkgRoot({
|
|
1215
|
+
name: "fixture-pkg",
|
|
1216
|
+
scripts: { "rulesync:generate": "node ./scripts/guarded-roots.mjs generate" },
|
|
1217
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1218
|
+
});
|
|
1219
|
+
const dest = tmp("pj-raw-dest-");
|
|
1220
|
+
fs.writeFileSync(
|
|
1221
|
+
path.join(dest, "package.json"),
|
|
1222
|
+
JSON.stringify({
|
|
1223
|
+
scripts: { "rulesync:generate:raw": "echo consumer-raw" },
|
|
1224
|
+
}),
|
|
1225
|
+
);
|
|
1226
|
+
mergePackageJson(dest, pkgRoot);
|
|
1227
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1228
|
+
expect(out.scripts["grunt:rulesync:generate"]).toBe("node ./scripts/guarded-roots.mjs generate");
|
|
1229
|
+
expect(out.scripts["rulesync:generate:raw"]).toBe("echo consumer-raw");
|
|
1230
|
+
});
|
|
1231
|
+
|
|
955
1232
|
it("replaces a grunt-owned script with no suffix", () => {
|
|
956
1233
|
const pkgRoot = stubPkgRoot({
|
|
957
1234
|
name: "fixture-pkg",
|
|
@@ -967,7 +1244,9 @@ describe("mergePackageJson", () => {
|
|
|
967
1244
|
);
|
|
968
1245
|
mergePackageJson(dest, pkgRoot);
|
|
969
1246
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
970
|
-
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();
|
|
971
1250
|
});
|
|
972
1251
|
|
|
973
1252
|
it("early return when name is @lovrozagar/grunt", () => {
|
|
@@ -987,9 +1266,257 @@ describe("mergePackageJson", () => {
|
|
|
987
1266
|
const dest = tmp("pj-noscripts-");
|
|
988
1267
|
mergePackageJson(dest, pkgRoot);
|
|
989
1268
|
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
990
|
-
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
|
+
});
|
|
991
1276
|
expect(out.devDependencies).toEqual({ rulesync: "2", "smol-toml": "1" });
|
|
992
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
|
+
});
|
|
993
1520
|
});
|
|
994
1521
|
|
|
995
1522
|
describe("init", () => {
|
|
@@ -1023,6 +1550,9 @@ describe("init", () => {
|
|
|
1023
1550
|
if (d === ".claude") continue;
|
|
1024
1551
|
expect(fs.readFileSync(path.join(dest, d, "marker"), "utf8")).toBe(d);
|
|
1025
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);
|
|
1026
1556
|
expect(fs.readFileSync(path.join(dest, ".claude", "marker"), "utf8")).toBe(".claude");
|
|
1027
1557
|
const settings = JSON.parse(
|
|
1028
1558
|
fs.readFileSync(path.join(dest, ".claude", "settings.json"), "utf8"),
|
|
@@ -1050,9 +1580,9 @@ describe("init", () => {
|
|
|
1050
1580
|
|
|
1051
1581
|
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
1052
1582
|
["npm", ["install"]],
|
|
1053
|
-
["npm", ["run", "rulesync:generate"]],
|
|
1054
|
-
["npm", ["run", "sync:globals:apply"]],
|
|
1055
|
-
["npm", ["run", "rulesync:check"]],
|
|
1583
|
+
["npm", ["run", "grunt:rulesync:generate"]],
|
|
1584
|
+
["npm", ["run", "grunt:sync:globals:apply"]],
|
|
1585
|
+
["npm", ["run", "grunt:rulesync:check"]],
|
|
1056
1586
|
]);
|
|
1057
1587
|
for (const call of exec.mock.calls) {
|
|
1058
1588
|
expect(call[2]).toEqual({ cwd: dest, stdio: "inherit" });
|
|
@@ -1101,13 +1631,21 @@ describe("init", () => {
|
|
|
1101
1631
|
const exec = vi.fn();
|
|
1102
1632
|
init(dest, { execFileSync: exec });
|
|
1103
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);
|
|
1104
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);
|
|
1105
1642
|
expect(fs.existsSync(path.join(dest, ".claude", "settings.json"))).toBe(true);
|
|
1643
|
+
expect(readGruntConfig(dest)).toEqual(GRUNT_CONFIG_DEFAULTS);
|
|
1106
1644
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1107
1645
|
["install"],
|
|
1108
|
-
["run", "rulesync:generate"],
|
|
1109
|
-
["run", "sync:globals:apply"],
|
|
1110
|
-
["run", "rulesync:check"],
|
|
1646
|
+
["run", "grunt:rulesync:generate"],
|
|
1647
|
+
["run", "grunt:sync:globals:apply"],
|
|
1648
|
+
["run", "grunt:rulesync:check"],
|
|
1111
1649
|
]);
|
|
1112
1650
|
});
|
|
1113
1651
|
|
|
@@ -1118,8 +1656,8 @@ describe("init", () => {
|
|
|
1118
1656
|
init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true });
|
|
1119
1657
|
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
1120
1658
|
["npm", ["install"]],
|
|
1121
|
-
["npm", ["run", "rulesync:generate"]],
|
|
1122
|
-
["npm", ["run", "rulesync:check"]],
|
|
1659
|
+
["npm", ["run", "grunt:rulesync:generate"]],
|
|
1660
|
+
["npm", ["run", "grunt:rulesync:check"]],
|
|
1123
1661
|
]);
|
|
1124
1662
|
});
|
|
1125
1663
|
|
|
@@ -1134,9 +1672,9 @@ describe("init", () => {
|
|
|
1134
1672
|
init(dest, { pkgRoot, execFileSync: exec, applyGlobals: true });
|
|
1135
1673
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1136
1674
|
["install"],
|
|
1137
|
-
["run", "rulesync:generate"],
|
|
1138
|
-
["run", "sync:globals:apply"],
|
|
1139
|
-
["run", "rulesync:check"],
|
|
1675
|
+
["run", "grunt:rulesync:generate"],
|
|
1676
|
+
["run", "grunt:sync:globals:apply"],
|
|
1677
|
+
["run", "grunt:rulesync:check"],
|
|
1140
1678
|
]);
|
|
1141
1679
|
});
|
|
1142
1680
|
|
|
@@ -1151,8 +1689,8 @@ describe("init", () => {
|
|
|
1151
1689
|
init(dest, { pkgRoot, execFileSync: exec });
|
|
1152
1690
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1153
1691
|
["install"],
|
|
1154
|
-
["run", "rulesync:generate"],
|
|
1155
|
-
["run", "rulesync:check"],
|
|
1692
|
+
["run", "grunt:rulesync:generate"],
|
|
1693
|
+
["run", "grunt:rulesync:check"],
|
|
1156
1694
|
]);
|
|
1157
1695
|
});
|
|
1158
1696
|
|
|
@@ -1189,9 +1727,9 @@ describe("init", () => {
|
|
|
1189
1727
|
expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
|
|
1190
1728
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1191
1729
|
["install"],
|
|
1192
|
-
["run", "rulesync:generate"],
|
|
1193
|
-
["run", "sync:globals:apply"],
|
|
1194
|
-
["run", "rulesync:check"],
|
|
1730
|
+
["run", "grunt:rulesync:generate"],
|
|
1731
|
+
["run", "grunt:sync:globals:apply"],
|
|
1732
|
+
["run", "grunt:rulesync:check"],
|
|
1195
1733
|
]);
|
|
1196
1734
|
});
|
|
1197
1735
|
|
|
@@ -1202,7 +1740,7 @@ describe("init", () => {
|
|
|
1202
1740
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "consumer claude\n");
|
|
1203
1741
|
fs.writeFileSync(path.join(dest, "GEMINI.md"), "consumer gemini\n");
|
|
1204
1742
|
const exec = vi.fn((_cmd, args) => {
|
|
1205
|
-
if (Array.isArray(args) && args.includes("rulesync:generate")) {
|
|
1743
|
+
if (Array.isArray(args) && args.includes("grunt:rulesync:generate")) {
|
|
1206
1744
|
fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
|
|
1207
1745
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
|
|
1208
1746
|
fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
|
|
@@ -1227,6 +1765,32 @@ describe("init", () => {
|
|
|
1227
1765
|
);
|
|
1228
1766
|
});
|
|
1229
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
|
+
|
|
1230
1794
|
it("onPhase merge then npm phases stop before exec", () => {
|
|
1231
1795
|
const pkgRoot = stubPkgRoot();
|
|
1232
1796
|
const dest = tmp("grunt-onphase-");
|
|
@@ -1247,9 +1811,9 @@ describe("init", () => {
|
|
|
1247
1811
|
]);
|
|
1248
1812
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
1249
1813
|
["install"],
|
|
1250
|
-
["run", "rulesync:generate"],
|
|
1251
|
-
["run", "sync:globals:apply"],
|
|
1252
|
-
["run", "rulesync:check"],
|
|
1814
|
+
["run", "grunt:rulesync:generate"],
|
|
1815
|
+
["run", "grunt:sync:globals:apply"],
|
|
1816
|
+
["run", "grunt:rulesync:check"],
|
|
1253
1817
|
]);
|
|
1254
1818
|
});
|
|
1255
1819
|
|
|
@@ -1262,6 +1826,30 @@ describe("init", () => {
|
|
|
1262
1826
|
["merge", "stop"],
|
|
1263
1827
|
]);
|
|
1264
1828
|
});
|
|
1829
|
+
|
|
1830
|
+
it("warns when dest skill differs from packaged then force-overwrites; keeps extras", () => {
|
|
1831
|
+
const pkgRoot = stubPkgRoot();
|
|
1832
|
+
const pkgSkill = path.join(pkgRoot, ".rulesync", "skills", "parent");
|
|
1833
|
+
fs.mkdirSync(pkgSkill, { recursive: true });
|
|
1834
|
+
fs.writeFileSync(path.join(pkgSkill, "SKILL.md"), "grunt-parent\n");
|
|
1835
|
+
const dest = tmp("grunt-sk-conflict-");
|
|
1836
|
+
const destSkill = path.join(dest, ".rulesync", "skills", "parent");
|
|
1837
|
+
const destExtra = path.join(dest, ".rulesync", "skills", "my-extra");
|
|
1838
|
+
fs.mkdirSync(destSkill, { recursive: true });
|
|
1839
|
+
fs.mkdirSync(destExtra, { recursive: true });
|
|
1840
|
+
fs.writeFileSync(path.join(destSkill, "SKILL.md"), "custom-parent\n");
|
|
1841
|
+
fs.writeFileSync(path.join(destExtra, "SKILL.md"), "keep-me\n");
|
|
1842
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
1843
|
+
init(dest, { pkgRoot, execFileSync: vi.fn(), skipGlobals: true });
|
|
1844
|
+
expect(
|
|
1845
|
+
warnSpy.mock.calls.some(
|
|
1846
|
+
(c) => String(c[0]).includes("parent") && String(c[0]).includes("re-init overwrites"),
|
|
1847
|
+
),
|
|
1848
|
+
).toBe(true);
|
|
1849
|
+
warnSpy.mockRestore();
|
|
1850
|
+
expect(fs.readFileSync(path.join(destSkill, "SKILL.md"), "utf8")).toBe("grunt-parent\n");
|
|
1851
|
+
expect(fs.readFileSync(path.join(destExtra, "SKILL.md"), "utf8")).toBe("keep-me\n");
|
|
1852
|
+
});
|
|
1265
1853
|
});
|
|
1266
1854
|
|
|
1267
1855
|
describe("destAlreadyInited / shouldAutoSkipGlobals", () => {
|