@lovrozagar/grunt 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/su/SKILL.md +11 -0
- package/.claude/settings.json +11 -0
- package/.claude/skills/su/SKILL.md +11 -0
- package/.codex/hooks.json +11 -0
- package/.grok/hooks/orchestrate-parent.js +3 -8
- package/.grok/skills/su/SKILL.md +11 -0
- package/.rulesync/hooks.jsonc +8 -0
- package/.rulesync/reference/INDEX.md +3 -2
- package/.rulesync/reference/hooks.md +1 -1
- package/.rulesync/reference/law.md +1 -1
- package/.rulesync/reference/refs-map.md +1 -1
- package/.rulesync/reference/skills-map.md +1 -0
- package/.rulesync/rules/overview.md +2 -0
- package/.rulesync/skills/su/SKILL.md +11 -0
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +28 -0
- package/README.md +57 -23
- package/cli/grunt.mjs +99 -22
- package/cli/grunt.test.ts +168 -12
- package/cli/init.mjs +102 -18
- package/cli/init.test.ts +281 -2
- package/package.json +4 -1
- package/scripts/check-globals.mjs +13 -5
- package/scripts/doctor.mjs +62 -12
- package/scripts/folder-map.mjs +300 -0
- package/scripts/gate-fat-tools.mjs +9 -5
- package/scripts/hooks-union.mjs +15 -4
- package/scripts/package-manager.mjs +150 -0
- package/scripts/scrub-spawn-prompt.mjs +1 -1
- package/scripts/session-map.mjs +72 -0
package/cli/init.mjs
CHANGED
|
@@ -16,6 +16,15 @@ import {
|
|
|
16
16
|
remergeGuardedRoots,
|
|
17
17
|
withGuardedCheckInteriors,
|
|
18
18
|
} from "../scripts/guarded-md.mjs"
|
|
19
|
+
import {
|
|
20
|
+
PACKAGE_MANAGERS,
|
|
21
|
+
UNKNOWN_PACKAGE_MANAGER,
|
|
22
|
+
detectPackageManager,
|
|
23
|
+
installArgs,
|
|
24
|
+
isPackageManager,
|
|
25
|
+
runScriptArgs,
|
|
26
|
+
} from "../scripts/package-manager.mjs"
|
|
27
|
+
import { LAW_REL, emitMaps } from "../scripts/emit-maps.mjs"
|
|
19
28
|
|
|
20
29
|
export {
|
|
21
30
|
GUARDED_ROOT_FILES,
|
|
@@ -44,11 +53,13 @@ export const PRODUCT_SCRIPTS = [
|
|
|
44
53
|
"guarded-md.mjs",
|
|
45
54
|
"guarded-roots.mjs",
|
|
46
55
|
"emit-mcp-policy.mjs",
|
|
56
|
+
"folder-map.mjs",
|
|
47
57
|
"gate-fat-tools.mjs",
|
|
48
58
|
"hooks-union.mjs",
|
|
49
59
|
"pipeline.mjs",
|
|
50
60
|
"grunt-job.mjs",
|
|
51
61
|
"parse-need.mjs",
|
|
62
|
+
"package-manager.mjs",
|
|
52
63
|
"persist-handoff.mjs",
|
|
53
64
|
"persist-implementation.mjs",
|
|
54
65
|
"persist-tmp.mjs",
|
|
@@ -56,6 +67,7 @@ export const PRODUCT_SCRIPTS = [
|
|
|
56
67
|
"purge-global-mcps.mjs",
|
|
57
68
|
"scrub-spawn-prompt.mjs",
|
|
58
69
|
"scrub-text-lib.mjs",
|
|
70
|
+
"session-map.mjs",
|
|
59
71
|
"sync-global-settings.mjs",
|
|
60
72
|
"browser.mjs",
|
|
61
73
|
"speak.mjs",
|
|
@@ -70,6 +82,9 @@ export const PRODUCT_SCRIPTS = [
|
|
|
70
82
|
]
|
|
71
83
|
|
|
72
84
|
const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"]
|
|
85
|
+
/** Generated on dest from the dest tree (grunt refs + consumer extras). Never stamp the package copies. */
|
|
86
|
+
export const GENERATED_MAP_FILES = new Set(["INDEX.md", "skills-map.md", "refs-map.md"])
|
|
87
|
+
const COPY_ROOT_IF_MISSING = [".mcp.json"]
|
|
73
88
|
export const RETIRED_SKILLS = ["parent", "solo", "cascade"]
|
|
74
89
|
export const RETIRED_AGENTS = ["implementer", "thinker"]
|
|
75
90
|
/** Cumulative. Dest `scripts/<name>` deleted on init/upgrade even if no longer shipped. */
|
|
@@ -99,6 +114,7 @@ export const RESERVED_SKILLS = [
|
|
|
99
114
|
"listen",
|
|
100
115
|
"pickup",
|
|
101
116
|
"speak",
|
|
117
|
+
"su",
|
|
102
118
|
"tmp",
|
|
103
119
|
"write-plan",
|
|
104
120
|
]
|
|
@@ -116,9 +132,9 @@ const AGENT_MIRROR_DIRS = [
|
|
|
116
132
|
[".codex", "agents"],
|
|
117
133
|
]
|
|
118
134
|
const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"]
|
|
119
|
-
// .mcp.json is
|
|
120
|
-
// (
|
|
121
|
-
|
|
135
|
+
// .mcp.json: copy when dest is missing so first init has a root file. emit-mcp-policy
|
|
136
|
+
// (generate) still owns the merge from .rulesync/mcp-policy.jsonc.
|
|
137
|
+
export const GRUNT_PACKAGE = "@lovrozagar/grunt"
|
|
122
138
|
export const GRUNT_NPM_PREFIX = "grunt:"
|
|
123
139
|
export function toGruntScriptName(k) {
|
|
124
140
|
return k.startsWith(GRUNT_NPM_PREFIX) ? k : `${GRUNT_NPM_PREFIX}${k}`
|
|
@@ -130,7 +146,12 @@ export const LAUNCH_SCRIPTS = {
|
|
|
130
146
|
gemini: "gemini --yolo",
|
|
131
147
|
grok: "grok --yolo",
|
|
132
148
|
}
|
|
133
|
-
const OWNED_HOOK_FILES = [
|
|
149
|
+
const OWNED_HOOK_FILES = [
|
|
150
|
+
"scrub-spawn-prompt.mjs",
|
|
151
|
+
"gate-fat-tools.mjs",
|
|
152
|
+
"orchestrate-parent.js",
|
|
153
|
+
"session-map.mjs",
|
|
154
|
+
]
|
|
134
155
|
|
|
135
156
|
function sortKeys(obj) {
|
|
136
157
|
return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]))
|
|
@@ -279,7 +300,7 @@ export function mergeClaudeSettings(destRoot, pkgRoot) {
|
|
|
279
300
|
}
|
|
280
301
|
|
|
281
302
|
function looksGruntOwnedPrefix(prefix) {
|
|
282
|
-
return /rulesync|sync:globals|^npm run |guarded-roots\.mjs|hooks-union\.mjs|doctor\.mjs|sync-global-settings\.mjs|check-globals\.mjs|purge-global-mcps\.mjs/.test(
|
|
303
|
+
return /rulesync|sync:globals|^(?:npm|yarn|pnpm|bun) run |guarded-roots\.mjs|hooks-union\.mjs|doctor\.mjs|sync-global-settings\.mjs|check-globals\.mjs|purge-global-mcps\.mjs/.test(
|
|
283
304
|
prefix,
|
|
284
305
|
)
|
|
285
306
|
}
|
|
@@ -317,24 +338,47 @@ function mergeScriptValue(newSrc, cur) {
|
|
|
317
338
|
return newSrc
|
|
318
339
|
}
|
|
319
340
|
|
|
320
|
-
function
|
|
341
|
+
function rewritePackageManagerRunRefs(scripts, srcKeys) {
|
|
321
342
|
const keys = srcKeys
|
|
322
343
|
.filter((k) => k !== toGruntScriptName(k))
|
|
323
344
|
.sort((a, b) => b.length - a.length)
|
|
324
345
|
if (!keys.length) return
|
|
346
|
+
const pm = PACKAGE_MANAGERS.join("|")
|
|
325
347
|
for (const name of Object.keys(scripts)) {
|
|
326
348
|
let val = scripts[name]
|
|
327
349
|
if (typeof val !== "string") continue
|
|
328
350
|
for (const k of keys) {
|
|
329
351
|
val = val.replace(
|
|
330
|
-
new RegExp(`
|
|
331
|
-
|
|
352
|
+
new RegExp(`(${pm}) run ${escapeRe(k)}(?=$|\\s|;|&)`, "g"),
|
|
353
|
+
`$1 run ${toGruntScriptName(k)}`,
|
|
332
354
|
)
|
|
333
355
|
}
|
|
334
356
|
scripts[name] = val
|
|
335
357
|
}
|
|
336
358
|
}
|
|
337
359
|
|
|
360
|
+
export function placeGruntDevDependency(destPkg, srcPkg) {
|
|
361
|
+
const deps =
|
|
362
|
+
destPkg.dependencies && typeof destPkg.dependencies === "object"
|
|
363
|
+
? destPkg.dependencies
|
|
364
|
+
: null
|
|
365
|
+
destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
|
|
366
|
+
const fromDep = deps?.[GRUNT_PACKAGE]
|
|
367
|
+
const fromDev = destPkg.devDependencies[GRUNT_PACKAGE]
|
|
368
|
+
if (fromDep != null && deps) {
|
|
369
|
+
delete destPkg.dependencies[GRUNT_PACKAGE]
|
|
370
|
+
if (fromDev == null) destPkg.devDependencies[GRUNT_PACKAGE] = fromDep
|
|
371
|
+
if (Object.keys(destPkg.dependencies).length === 0) delete destPkg.dependencies
|
|
372
|
+
}
|
|
373
|
+
if (
|
|
374
|
+
destPkg.devDependencies[GRUNT_PACKAGE] == null &&
|
|
375
|
+
srcPkg?.name === GRUNT_PACKAGE &&
|
|
376
|
+
srcPkg.version
|
|
377
|
+
) {
|
|
378
|
+
destPkg.devDependencies[GRUNT_PACKAGE] = srcPkg.version
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
338
382
|
export function mergePackageJson(dest, pkgRoot) {
|
|
339
383
|
const destPath = path.join(dest, "package.json")
|
|
340
384
|
let destPkg = {}
|
|
@@ -377,18 +421,47 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
377
421
|
const staleKey = `grunt:yolo:${k}`
|
|
378
422
|
if (destPkg.scripts[staleKey] === v) delete destPkg.scripts[staleKey]
|
|
379
423
|
}
|
|
380
|
-
|
|
424
|
+
rewritePackageManagerRunRefs(destPkg.scripts, srcKeys)
|
|
381
425
|
destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
|
|
382
426
|
destPkg.devDependencies["smol-toml"] = srcPkg.devDependencies["smol-toml"]
|
|
383
427
|
destPkg.devDependencies["rulesync"] = srcPkg.devDependencies["rulesync"]
|
|
384
428
|
const clack = srcPkg.dependencies?.["@clack/prompts"]
|
|
385
429
|
if (clack) destPkg.devDependencies["@clack/prompts"] = clack
|
|
430
|
+
placeGruntDevDependency(destPkg, srcPkg)
|
|
386
431
|
destPkg.scripts = sortKeys(destPkg.scripts)
|
|
387
432
|
destPkg.devDependencies = sortKeys(destPkg.devDependencies)
|
|
388
433
|
fs.writeFileSync(destPath, `${JSON.stringify(destPkg, null, 2)}\n`)
|
|
389
434
|
}
|
|
390
435
|
|
|
391
|
-
|
|
436
|
+
function copyRootIfMissing(dest, pkgRoot) {
|
|
437
|
+
for (const file of COPY_ROOT_IF_MISSING) {
|
|
438
|
+
const src = path.join(pkgRoot, file)
|
|
439
|
+
const d = path.join(dest, file)
|
|
440
|
+
if (!fs.existsSync(src) || fs.existsSync(d)) continue
|
|
441
|
+
fs.copyFileSync(src, d)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function emitDestMaps(dest) {
|
|
446
|
+
if (!fs.existsSync(path.join(dest, LAW_REL))) return
|
|
447
|
+
const maps = emitMaps({ workspaceRoot: dest })
|
|
448
|
+
if (!maps.ok) throw new Error(maps.error || "emit-maps failed")
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function resolveInitPackageManager(dest, { packageManager, env = process.env } = {}) {
|
|
452
|
+
if (packageManager) {
|
|
453
|
+
const name = String(packageManager).toLowerCase()
|
|
454
|
+
if (!isPackageManager(name)) {
|
|
455
|
+
throw new Error(UNKNOWN_PACKAGE_MANAGER)
|
|
456
|
+
}
|
|
457
|
+
return name
|
|
458
|
+
}
|
|
459
|
+
const detected = detectPackageManager({ cwd: dest, env })
|
|
460
|
+
if (!detected.manager) throw new Error(UNKNOWN_PACKAGE_MANAGER)
|
|
461
|
+
return detected.manager
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false, applyGlobals, onPhase, packageManager, env = process.env } = {}) {
|
|
392
465
|
dest = path.resolve(dest)
|
|
393
466
|
const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
|
|
394
467
|
const skipGlobalsApply =
|
|
@@ -429,6 +502,12 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
429
502
|
filter: (s) => path.basename(s) !== "settings.json",
|
|
430
503
|
})
|
|
431
504
|
mergeClaudeSettings(dest, pkgRoot)
|
|
505
|
+
} else if (dir === ".rulesync") {
|
|
506
|
+
fs.cpSync(src, d, {
|
|
507
|
+
recursive: true,
|
|
508
|
+
force: true,
|
|
509
|
+
filter: (s) => !GENERATED_MAP_FILES.has(path.basename(s)),
|
|
510
|
+
})
|
|
432
511
|
} else {
|
|
433
512
|
fs.cpSync(src, d, { recursive: true, force: true })
|
|
434
513
|
}
|
|
@@ -453,26 +532,31 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
453
532
|
fs.mkdirSync(path.join(dest, ".tmp"), { recursive: true })
|
|
454
533
|
mergeGitignore(dest)
|
|
455
534
|
|
|
456
|
-
if (!self)
|
|
535
|
+
if (!self) {
|
|
536
|
+
mergePackageJson(dest, pkgRoot)
|
|
537
|
+
copyRootIfMissing(dest, pkgRoot)
|
|
538
|
+
emitDestMaps(dest)
|
|
539
|
+
}
|
|
457
540
|
})
|
|
458
541
|
|
|
459
542
|
// Self-skip: file/dir merge + .tmp + gitignore only. No package.json merge,
|
|
460
|
-
//
|
|
543
|
+
// install, or generate/sync/check (would mutate this package in-place).
|
|
461
544
|
if (self) return
|
|
462
545
|
|
|
463
|
-
const
|
|
546
|
+
const pm = resolveInitPackageManager(dest, { packageManager, env })
|
|
547
|
+
const runPm = (name, args) => {
|
|
464
548
|
onPhase?.(name, "start")
|
|
465
549
|
onPhase?.(name, "stop")
|
|
466
|
-
exec(
|
|
550
|
+
exec(pm, args, { cwd: dest, stdio: "inherit" })
|
|
467
551
|
}
|
|
468
|
-
|
|
552
|
+
runPm("install", installArgs(pm))
|
|
469
553
|
const guardedSnap = snapshotGuardedRoots(dest)
|
|
470
|
-
|
|
554
|
+
runPm("generate", runScriptArgs(pm, toGruntScriptName("rulesync:generate")))
|
|
471
555
|
remergeGuardedRoots(dest, guardedSnap)
|
|
472
556
|
if (!skipGlobalsApply) {
|
|
473
|
-
|
|
557
|
+
runPm("sync-globals", runScriptArgs(pm, toGruntScriptName("sync:globals:apply")))
|
|
474
558
|
}
|
|
475
559
|
withGuardedCheckInteriors(dest, () => {
|
|
476
|
-
|
|
560
|
+
runPm("check", runScriptArgs(pm, toGruntScriptName("rulesync:check")))
|
|
477
561
|
})
|
|
478
562
|
}
|
package/cli/init.test.ts
CHANGED
|
@@ -4,6 +4,18 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
const emitMaps = vi.hoisted(() => vi.fn());
|
|
9
|
+
const actualEmitMaps = vi.hoisted(() => ({ fn: null as null | ((...args: unknown[]) => unknown) }));
|
|
10
|
+
|
|
11
|
+
vi.mock("../scripts/emit-maps.mjs", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal<typeof import("../scripts/emit-maps.mjs")>();
|
|
13
|
+
actualEmitMaps.fn = actual.emitMaps;
|
|
14
|
+
emitMaps.mockImplementation((...args: unknown[]) =>
|
|
15
|
+
actual.emitMaps(...(args as Parameters<typeof actual.emitMaps>)),
|
|
16
|
+
);
|
|
17
|
+
return { ...actual, emitMaps };
|
|
18
|
+
});
|
|
7
19
|
import {
|
|
8
20
|
MAX_GUARDED_MARKDOWN_BYTES,
|
|
9
21
|
PRODUCT_SCRIPTS,
|
|
@@ -28,7 +40,10 @@ import {
|
|
|
28
40
|
mergeGitignore,
|
|
29
41
|
mergeGuardedContent,
|
|
30
42
|
mergeGuardedMarkdown,
|
|
43
|
+
GENERATED_MAP_FILES,
|
|
44
|
+
GRUNT_PACKAGE,
|
|
31
45
|
mergePackageJson,
|
|
46
|
+
placeGruntDevDependency,
|
|
32
47
|
remergeGuardedRoots,
|
|
33
48
|
samePath,
|
|
34
49
|
shouldAutoSkipGlobals,
|
|
@@ -37,6 +52,7 @@ import {
|
|
|
37
52
|
withGuardedCheckInteriors,
|
|
38
53
|
writeMergedGuardedFile,
|
|
39
54
|
} from "./init.mjs";
|
|
55
|
+
import { UNKNOWN_PACKAGE_MANAGER } from "../scripts/package-manager.mjs";
|
|
40
56
|
import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
|
|
41
57
|
|
|
42
58
|
const tmpDirs: string[] = [];
|
|
@@ -44,6 +60,10 @@ afterEach(() => {
|
|
|
44
60
|
for (const d of tmpDirs.splice(0)) {
|
|
45
61
|
fs.rmSync(d, { recursive: true, force: true });
|
|
46
62
|
}
|
|
63
|
+
emitMaps.mockReset();
|
|
64
|
+
emitMaps.mockImplementation((...args: unknown[]) =>
|
|
65
|
+
(actualEmitMaps.fn as (...a: unknown[]) => unknown)(...args),
|
|
66
|
+
);
|
|
47
67
|
});
|
|
48
68
|
|
|
49
69
|
function tmp(prefix: string) {
|
|
@@ -62,6 +82,17 @@ const SRC_CLAUDE_SETTINGS = {
|
|
|
62
82
|
deny: ["Agent(Explore)", "Agent(orchestrator)", "mcp__*"],
|
|
63
83
|
},
|
|
64
84
|
hooks: {
|
|
85
|
+
SessionStart: [
|
|
86
|
+
{
|
|
87
|
+
hooks: [
|
|
88
|
+
{
|
|
89
|
+
type: "command",
|
|
90
|
+
command: 'node "${ROOT}/scripts/session-map.mjs"',
|
|
91
|
+
timeout: 5,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
65
96
|
PreToolUse: [
|
|
66
97
|
{
|
|
67
98
|
matcher: "spawn",
|
|
@@ -1408,6 +1439,113 @@ describe("mergePackageJson", () => {
|
|
|
1408
1439
|
expect(out.scripts.check).toBe("npm run grunt:rulesync:check && npm run rulesync:check:raw");
|
|
1409
1440
|
});
|
|
1410
1441
|
|
|
1442
|
+
it("rewrites yarn/pnpm/bun run refs the same way as npm run", () => {
|
|
1443
|
+
const pkgRoot = stubPkgRoot({
|
|
1444
|
+
name: "fixture-pkg",
|
|
1445
|
+
scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
|
|
1446
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1447
|
+
});
|
|
1448
|
+
const dest = tmp("pj-pm-rewrite-");
|
|
1449
|
+
fs.writeFileSync(
|
|
1450
|
+
path.join(dest, "package.json"),
|
|
1451
|
+
JSON.stringify({
|
|
1452
|
+
scripts: {
|
|
1453
|
+
"rulesync:check": "node ./scripts/guarded-roots.mjs check",
|
|
1454
|
+
check: "pnpm run rulesync:check && yarn run rulesync:check && bun run rulesync:check",
|
|
1455
|
+
},
|
|
1456
|
+
}),
|
|
1457
|
+
);
|
|
1458
|
+
mergePackageJson(dest, pkgRoot);
|
|
1459
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1460
|
+
expect(out.scripts.check).toBe(
|
|
1461
|
+
"pnpm run grunt:rulesync:check && yarn run grunt:rulesync:check && bun run grunt:rulesync:check",
|
|
1462
|
+
);
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
it("moves @lovrozagar/grunt from dependencies to devDependencies", () => {
|
|
1466
|
+
const pkgRoot = stubPkgRoot({
|
|
1467
|
+
name: GRUNT_PACKAGE,
|
|
1468
|
+
version: "0.6.2",
|
|
1469
|
+
scripts: { "rulesync:generate": "gen" },
|
|
1470
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1471
|
+
});
|
|
1472
|
+
const dest = tmp("pj-grunt-dep-");
|
|
1473
|
+
fs.writeFileSync(
|
|
1474
|
+
path.join(dest, "package.json"),
|
|
1475
|
+
JSON.stringify({
|
|
1476
|
+
name: "app",
|
|
1477
|
+
dependencies: { [GRUNT_PACKAGE]: "0.6.2", lodash: "4" },
|
|
1478
|
+
scripts: {},
|
|
1479
|
+
}),
|
|
1480
|
+
);
|
|
1481
|
+
mergePackageJson(dest, pkgRoot);
|
|
1482
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1483
|
+
expect(out.dependencies).toEqual({ lodash: "4" });
|
|
1484
|
+
expect(out.devDependencies[GRUNT_PACKAGE]).toBe("0.6.2");
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
it("drops empty dependencies after moving grunt", () => {
|
|
1488
|
+
const pkgRoot = stubPkgRoot({
|
|
1489
|
+
name: GRUNT_PACKAGE,
|
|
1490
|
+
version: "0.7.0",
|
|
1491
|
+
scripts: {},
|
|
1492
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1493
|
+
});
|
|
1494
|
+
const dest = tmp("pj-grunt-onlydep-");
|
|
1495
|
+
fs.writeFileSync(
|
|
1496
|
+
path.join(dest, "package.json"),
|
|
1497
|
+
JSON.stringify({ dependencies: { [GRUNT_PACKAGE]: "0.6.2" } }),
|
|
1498
|
+
);
|
|
1499
|
+
mergePackageJson(dest, pkgRoot);
|
|
1500
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1501
|
+
expect(out.dependencies).toBeUndefined();
|
|
1502
|
+
expect(out.devDependencies[GRUNT_PACKAGE]).toBe("0.6.2");
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
it("adds missing grunt as a devDependency from the running package version", () => {
|
|
1506
|
+
const pkgRoot = stubPkgRoot({
|
|
1507
|
+
name: GRUNT_PACKAGE,
|
|
1508
|
+
version: "0.7.0",
|
|
1509
|
+
scripts: {},
|
|
1510
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1511
|
+
});
|
|
1512
|
+
const dest = tmp("pj-grunt-add-");
|
|
1513
|
+
fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ name: "app" }));
|
|
1514
|
+
mergePackageJson(dest, pkgRoot);
|
|
1515
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1516
|
+
expect(out.devDependencies[GRUNT_PACKAGE]).toBe("0.7.0");
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
it("keeps an existing grunt devDependency version", () => {
|
|
1520
|
+
const pkgRoot = stubPkgRoot({
|
|
1521
|
+
name: GRUNT_PACKAGE,
|
|
1522
|
+
version: "0.7.0",
|
|
1523
|
+
scripts: {},
|
|
1524
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1525
|
+
});
|
|
1526
|
+
const dest = tmp("pj-grunt-keep-");
|
|
1527
|
+
fs.writeFileSync(
|
|
1528
|
+
path.join(dest, "package.json"),
|
|
1529
|
+
JSON.stringify({
|
|
1530
|
+
devDependencies: { [GRUNT_PACKAGE]: "^0.6.2" },
|
|
1531
|
+
dependencies: { [GRUNT_PACKAGE]: "0.6.2" },
|
|
1532
|
+
}),
|
|
1533
|
+
);
|
|
1534
|
+
mergePackageJson(dest, pkgRoot);
|
|
1535
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1536
|
+
expect(out.dependencies).toBeUndefined();
|
|
1537
|
+
expect(out.devDependencies[GRUNT_PACKAGE]).toBe("^0.6.2");
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
it("placeGruntDevDependency is a no-op when src is not grunt", () => {
|
|
1541
|
+
const destPkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> } = {
|
|
1542
|
+
dependencies: { lodash: "4" },
|
|
1543
|
+
};
|
|
1544
|
+
placeGruntDevDependency(destPkg, { name: "fixture-pkg", version: "1.0.0" });
|
|
1545
|
+
expect(destPkg.dependencies).toEqual({ lodash: "4" });
|
|
1546
|
+
expect(destPkg.devDependencies?.[GRUNT_PACKAGE]).toBeUndefined();
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1411
1549
|
it("skips non-string dest scripts when rewriting npm run refs", () => {
|
|
1412
1550
|
const pkgRoot = stubPkgRoot({
|
|
1413
1551
|
name: "fixture-pkg",
|
|
@@ -1526,7 +1664,7 @@ describe("init", () => {
|
|
|
1526
1664
|
const pkgRoot = stubPkgRoot();
|
|
1527
1665
|
const dest = tmp("grunt-dest-");
|
|
1528
1666
|
const exec = vi.fn();
|
|
1529
|
-
init(dest, { pkgRoot, execFileSync: exec });
|
|
1667
|
+
init(dest, { pkgRoot, execFileSync: exec, packageManager: "npm" });
|
|
1530
1668
|
|
|
1531
1669
|
for (const d of COPY_DIRS) {
|
|
1532
1670
|
if (d === ".claude") continue;
|
|
@@ -1601,6 +1739,24 @@ describe("init", () => {
|
|
|
1601
1739
|
expect(settings.permissions.deny).toContain("mcp__*");
|
|
1602
1740
|
});
|
|
1603
1741
|
|
|
1742
|
+
it("re-init replaces a prior grunt session-map SessionStart group instead of duplicating it", () => {
|
|
1743
|
+
const pkgRoot = stubPkgRoot();
|
|
1744
|
+
const dest = tmp("grunt-rerun-map-");
|
|
1745
|
+
fs.mkdirSync(path.join(dest, ".claude"), { recursive: true });
|
|
1746
|
+
const mapGroup = {
|
|
1747
|
+
hooks: [{ type: "command", command: 'node "$X/scripts/session-map.mjs"', timeout: 5 }],
|
|
1748
|
+
};
|
|
1749
|
+
fs.writeFileSync(
|
|
1750
|
+
path.join(dest, ".claude", "settings.json"),
|
|
1751
|
+
JSON.stringify({ permissions: {}, hooks: { SessionStart: [mapGroup] } }),
|
|
1752
|
+
);
|
|
1753
|
+
init(dest, { pkgRoot, execFileSync: vi.fn() });
|
|
1754
|
+
const settings = JSON.parse(
|
|
1755
|
+
fs.readFileSync(path.join(dest, ".claude", "settings.json"), "utf8"),
|
|
1756
|
+
);
|
|
1757
|
+
expect(settings.hooks.SessionStart).toEqual(SRC_CLAUDE_SETTINGS.hooks.SessionStart);
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1604
1760
|
it("omitted execFileSync uses default; self-skip never invokes it", () => {
|
|
1605
1761
|
const pkgRoot = stubPkgRoot();
|
|
1606
1762
|
init(pkgRoot, { pkgRoot });
|
|
@@ -1645,6 +1801,7 @@ describe("init", () => {
|
|
|
1645
1801
|
"listen",
|
|
1646
1802
|
"pickup",
|
|
1647
1803
|
"speak",
|
|
1804
|
+
"su",
|
|
1648
1805
|
"tmp",
|
|
1649
1806
|
"write-plan",
|
|
1650
1807
|
]);
|
|
@@ -1741,7 +1898,7 @@ describe("init", () => {
|
|
|
1741
1898
|
const pkgRoot = stubPkgRoot();
|
|
1742
1899
|
const dest = tmp("grunt-skip-globals-");
|
|
1743
1900
|
const exec = vi.fn();
|
|
1744
|
-
init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true });
|
|
1901
|
+
init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true, packageManager: "npm" });
|
|
1745
1902
|
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
1746
1903
|
["npm", ["install"]],
|
|
1747
1904
|
["npm", ["run", "grunt:rulesync:generate"]],
|
|
@@ -1879,6 +2036,128 @@ describe("init", () => {
|
|
|
1879
2036
|
expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
1880
2037
|
});
|
|
1881
2038
|
|
|
2039
|
+
it("pnpm-lock.yaml dest installs with pnpm even if invocation is npm", () => {
|
|
2040
|
+
const pkgRoot = stubPkgRoot();
|
|
2041
|
+
const dest = tmp("grunt-pnpm-");
|
|
2042
|
+
fs.writeFileSync(path.join(dest, "pnpm-lock.yaml"), "lockfileVersion: 9\n");
|
|
2043
|
+
const exec = vi.fn();
|
|
2044
|
+
init(dest, {
|
|
2045
|
+
pkgRoot,
|
|
2046
|
+
execFileSync: exec,
|
|
2047
|
+
env: { npm_config_user_agent: "npm/10.0.0 node/v22" },
|
|
2048
|
+
});
|
|
2049
|
+
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
2050
|
+
["pnpm", ["install"]],
|
|
2051
|
+
["pnpm", ["run", "grunt:rulesync:generate"]],
|
|
2052
|
+
["pnpm", ["run", "grunt:sync:globals:apply"]],
|
|
2053
|
+
["pnpm", ["run", "grunt:rulesync:check"]],
|
|
2054
|
+
]);
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
it("packageManager option wins over dest lockfile", () => {
|
|
2058
|
+
const pkgRoot = stubPkgRoot();
|
|
2059
|
+
const dest = tmp("grunt-pm-opt-");
|
|
2060
|
+
fs.writeFileSync(path.join(dest, "yarn.lock"), "");
|
|
2061
|
+
const exec = vi.fn();
|
|
2062
|
+
init(dest, { pkgRoot, execFileSync: exec, packageManager: "bun" });
|
|
2063
|
+
expect(exec.mock.calls[0][0]).toBe("bun");
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
it("unknown manager throws and does not exec", () => {
|
|
2067
|
+
const pkgRoot = stubPkgRoot();
|
|
2068
|
+
const dest = tmp("grunt-pm-unknown-");
|
|
2069
|
+
const exec = vi.fn();
|
|
2070
|
+
expect(() =>
|
|
2071
|
+
init(dest, { pkgRoot, execFileSync: exec, env: {}, packageManager: undefined }),
|
|
2072
|
+
).toThrow(UNKNOWN_PACKAGE_MANAGER);
|
|
2073
|
+
expect(exec).not.toHaveBeenCalled();
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
it("invalid packageManager option throws", () => {
|
|
2077
|
+
const pkgRoot = stubPkgRoot();
|
|
2078
|
+
const dest = tmp("grunt-pm-bad-");
|
|
2079
|
+
const exec = vi.fn();
|
|
2080
|
+
expect(() => init(dest, { pkgRoot, execFileSync: exec, packageManager: "deno" })).toThrow(
|
|
2081
|
+
UNKNOWN_PACKAGE_MANAGER,
|
|
2082
|
+
);
|
|
2083
|
+
expect(exec).not.toHaveBeenCalled();
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
it("yarn.lock dest uses yarn run args without npm --", () => {
|
|
2087
|
+
const pkgRoot = stubPkgRoot();
|
|
2088
|
+
const dest = tmp("grunt-yarn-");
|
|
2089
|
+
fs.writeFileSync(path.join(dest, "yarn.lock"), "");
|
|
2090
|
+
const exec = vi.fn();
|
|
2091
|
+
init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true, env: {} });
|
|
2092
|
+
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
2093
|
+
["yarn", ["install"]],
|
|
2094
|
+
["yarn", ["run", "grunt:rulesync:generate"]],
|
|
2095
|
+
["yarn", ["run", "grunt:rulesync:check"]],
|
|
2096
|
+
]);
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
it("merges dest refs into INDEX and writes .mcp.json when missing", () => {
|
|
2100
|
+
const dest = tmp("grunt-maps-merge-");
|
|
2101
|
+
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
2102
|
+
fs.writeFileSync(
|
|
2103
|
+
path.join(dest, ".rulesync", "reference", "stripe.md"),
|
|
2104
|
+
"# Stripe\n\nConsumer stripe payments ref.\n",
|
|
2105
|
+
);
|
|
2106
|
+
const exec = vi.fn();
|
|
2107
|
+
init(dest, { execFileSync: exec, skipGlobals: true, packageManager: "npm" });
|
|
2108
|
+
expect(GENERATED_MAP_FILES.has("INDEX.md")).toBe(true);
|
|
2109
|
+
const index = fs.readFileSync(path.join(dest, ".rulesync", "reference", "INDEX.md"), "utf8");
|
|
2110
|
+
expect(index).toMatch(/stripe\.md/);
|
|
2111
|
+
expect(index).toMatch(/Consumer stripe payments ref/);
|
|
2112
|
+
expect(index).toMatch(/browser\.md/);
|
|
2113
|
+
const refsMap = fs.readFileSync(path.join(dest, ".rulesync", "reference", "refs-map.md"), "utf8");
|
|
2114
|
+
expect(refsMap).toMatch(/stripe\.md/);
|
|
2115
|
+
expect(fs.existsSync(path.join(dest, ".mcp.json"))).toBe(true);
|
|
2116
|
+
expect(JSON.parse(fs.readFileSync(path.join(dest, ".mcp.json"), "utf8"))).toEqual({
|
|
2117
|
+
mcpServers: {},
|
|
2118
|
+
});
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
it("keeps dest .mcp.json when already present", () => {
|
|
2122
|
+
const dest = tmp("grunt-mcp-keep-");
|
|
2123
|
+
fs.writeFileSync(
|
|
2124
|
+
path.join(dest, ".mcp.json"),
|
|
2125
|
+
`${JSON.stringify({ mcpServers: { tasks: { command: "npx" } } }, null, 2)}\n`,
|
|
2126
|
+
);
|
|
2127
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
|
|
2128
|
+
expect(JSON.parse(fs.readFileSync(path.join(dest, ".mcp.json"), "utf8"))).toEqual({
|
|
2129
|
+
mcpServers: { tasks: { command: "npx" } },
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
it("throws when dest emit-maps fails", () => {
|
|
2134
|
+
emitMaps.mockReturnValue({ ok: false, error: "maps boom" });
|
|
2135
|
+
const dest = tmp("grunt-maps-fail-");
|
|
2136
|
+
expect(() =>
|
|
2137
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" }),
|
|
2138
|
+
).toThrow("maps boom");
|
|
2139
|
+
emitMaps.mockReturnValue({ ok: false });
|
|
2140
|
+
const dest2 = tmp("grunt-maps-fail2-");
|
|
2141
|
+
expect(() =>
|
|
2142
|
+
init(dest2, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" }),
|
|
2143
|
+
).toThrow("emit-maps failed");
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
it("does not stamp package INDEX over dest; re-emits union", () => {
|
|
2147
|
+
const dest = tmp("grunt-index-keep-");
|
|
2148
|
+
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
2149
|
+
fs.writeFileSync(path.join(dest, ".rulesync", "reference", "owned.md"), "# Owned\n\nDest-only.\n");
|
|
2150
|
+
fs.writeFileSync(
|
|
2151
|
+
path.join(dest, ".rulesync", "reference", "INDEX.md"),
|
|
2152
|
+
"stale grunt-only index\n",
|
|
2153
|
+
);
|
|
2154
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
|
|
2155
|
+
const index = fs.readFileSync(path.join(dest, ".rulesync", "reference", "INDEX.md"), "utf8");
|
|
2156
|
+
expect(index).not.toBe("stale grunt-only index\n");
|
|
2157
|
+
expect(index).toMatch(/owned\.md/);
|
|
2158
|
+
expect(index).toMatch(/browser\.md/);
|
|
2159
|
+
});
|
|
2160
|
+
|
|
1882
2161
|
it("onPhase merge then npm phases stop before exec", () => {
|
|
1883
2162
|
const pkgRoot = stubPkgRoot();
|
|
1884
2163
|
const dest = tmp("grunt-onphase-");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/grunt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Multi-agent orchestrator SoT — deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -31,11 +31,13 @@
|
|
|
31
31
|
"scripts/guarded-md.mjs",
|
|
32
32
|
"scripts/guarded-roots.mjs",
|
|
33
33
|
"scripts/emit-mcp-policy.mjs",
|
|
34
|
+
"scripts/folder-map.mjs",
|
|
34
35
|
"scripts/gate-fat-tools.mjs",
|
|
35
36
|
"scripts/hooks-union.mjs",
|
|
36
37
|
"scripts/pipeline.mjs",
|
|
37
38
|
"scripts/grunt-job.mjs",
|
|
38
39
|
"scripts/parse-need.mjs",
|
|
40
|
+
"scripts/package-manager.mjs",
|
|
39
41
|
"scripts/persist-handoff.mjs",
|
|
40
42
|
"scripts/persist-implementation.mjs",
|
|
41
43
|
"scripts/persist-tmp.mjs",
|
|
@@ -43,6 +45,7 @@
|
|
|
43
45
|
"scripts/purge-global-mcps.mjs",
|
|
44
46
|
"scripts/scrub-spawn-prompt.mjs",
|
|
45
47
|
"scripts/scrub-text-lib.mjs",
|
|
48
|
+
"scripts/session-map.mjs",
|
|
46
49
|
"scripts/sync-global-settings.mjs",
|
|
47
50
|
"scripts/browser.mjs",
|
|
48
51
|
"scripts/speak.mjs",
|
|
@@ -4,8 +4,15 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { parse as parseToml } from "smol-toml";
|
|
7
|
+
import { detectPackageManager, runScriptLine } from "./package-manager.mjs";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
function applyHint(workspaceRoot) {
|
|
10
|
+
const { manager } = detectPackageManager({
|
|
11
|
+
cwd: workspaceRoot || process.cwd(),
|
|
12
|
+
env: process.env,
|
|
13
|
+
});
|
|
14
|
+
return runScriptLine(manager, "sync:globals:apply");
|
|
15
|
+
}
|
|
9
16
|
const PROJECT_REL = ".grok/config.toml";
|
|
10
17
|
const HOME_REL = ".grok/config.toml";
|
|
11
18
|
|
|
@@ -33,16 +40,17 @@ function parseTomlFile(abs, label) {
|
|
|
33
40
|
export function checkGlobals({ home, workspaceRoot } = {}) {
|
|
34
41
|
const homeDir = home || process.env.HOME;
|
|
35
42
|
const ws = workspaceRoot || process.cwd();
|
|
43
|
+
const hint = applyHint(ws);
|
|
36
44
|
if (!homeDir) {
|
|
37
|
-
return { ok: false, error: `HOME required; ${
|
|
45
|
+
return { ok: false, error: `HOME required; ${hint}` };
|
|
38
46
|
}
|
|
39
47
|
const homeConfig = path.resolve(homeDir, HOME_REL);
|
|
40
48
|
const homeParsed = parseTomlFile(homeConfig, "home ~/.grok/config.toml");
|
|
41
49
|
if (!homeParsed.ok) {
|
|
42
50
|
if (homeParsed.missing) {
|
|
43
|
-
return { ok: false, error: `missing ~/.grok/config.toml; ${
|
|
51
|
+
return { ok: false, error: `missing ~/.grok/config.toml; ${hint}` };
|
|
44
52
|
}
|
|
45
|
-
return { ok: false, error: `${homeParsed.error}; ${
|
|
53
|
+
return { ok: false, error: `${homeParsed.error}; ${hint}` };
|
|
46
54
|
}
|
|
47
55
|
const agentName = homeParsed.value.agent && homeParsed.value.agent.name;
|
|
48
56
|
const compaction =
|
|
@@ -50,7 +58,7 @@ export function checkGlobals({ home, workspaceRoot } = {}) {
|
|
|
50
58
|
if (agentName !== "orchestrator" || compaction !== true) {
|
|
51
59
|
return {
|
|
52
60
|
ok: false,
|
|
53
|
-
error: `home [agent].name must be orchestrator and [features].two_pass_compaction must be true; ${
|
|
61
|
+
error: `home [agent].name must be orchestrator and [features].two_pass_compaction must be true; ${hint}`,
|
|
54
62
|
};
|
|
55
63
|
}
|
|
56
64
|
|