@lovrozagar/grunt 0.6.2 → 0.7.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/CHANGELOG.md +13 -0
- package/README.md +32 -22
- package/cli/grunt.mjs +93 -22
- package/cli/grunt.test.ts +149 -12
- package/cli/init.mjs +93 -17
- package/cli/init.test.ts +251 -2
- package/package.json +2 -1
- package/scripts/check-globals.mjs +13 -5
- package/scripts/doctor.mjs +62 -12
- package/scripts/package-manager.mjs +150 -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,
|
|
@@ -49,6 +58,7 @@ export const PRODUCT_SCRIPTS = [
|
|
|
49
58
|
"pipeline.mjs",
|
|
50
59
|
"grunt-job.mjs",
|
|
51
60
|
"parse-need.mjs",
|
|
61
|
+
"package-manager.mjs",
|
|
52
62
|
"persist-handoff.mjs",
|
|
53
63
|
"persist-implementation.mjs",
|
|
54
64
|
"persist-tmp.mjs",
|
|
@@ -70,6 +80,9 @@ export const PRODUCT_SCRIPTS = [
|
|
|
70
80
|
]
|
|
71
81
|
|
|
72
82
|
const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"]
|
|
83
|
+
/** Generated on dest from the dest tree (grunt refs + consumer extras). Never stamp the package copies. */
|
|
84
|
+
export const GENERATED_MAP_FILES = new Set(["INDEX.md", "skills-map.md", "refs-map.md"])
|
|
85
|
+
const COPY_ROOT_IF_MISSING = [".mcp.json"]
|
|
73
86
|
export const RETIRED_SKILLS = ["parent", "solo", "cascade"]
|
|
74
87
|
export const RETIRED_AGENTS = ["implementer", "thinker"]
|
|
75
88
|
/** Cumulative. Dest `scripts/<name>` deleted on init/upgrade even if no longer shipped. */
|
|
@@ -116,9 +129,9 @@ const AGENT_MIRROR_DIRS = [
|
|
|
116
129
|
[".codex", "agents"],
|
|
117
130
|
]
|
|
118
131
|
const GUARDED_MD_FILES = ["AGENTS.md", "CLAUDE.md"]
|
|
119
|
-
// .mcp.json is
|
|
120
|
-
// (
|
|
121
|
-
|
|
132
|
+
// .mcp.json: copy when dest is missing so first init has a root file. emit-mcp-policy
|
|
133
|
+
// (generate) still owns the merge from .rulesync/mcp-policy.jsonc.
|
|
134
|
+
export const GRUNT_PACKAGE = "@lovrozagar/grunt"
|
|
122
135
|
export const GRUNT_NPM_PREFIX = "grunt:"
|
|
123
136
|
export function toGruntScriptName(k) {
|
|
124
137
|
return k.startsWith(GRUNT_NPM_PREFIX) ? k : `${GRUNT_NPM_PREFIX}${k}`
|
|
@@ -279,7 +292,7 @@ export function mergeClaudeSettings(destRoot, pkgRoot) {
|
|
|
279
292
|
}
|
|
280
293
|
|
|
281
294
|
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(
|
|
295
|
+
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
296
|
prefix,
|
|
284
297
|
)
|
|
285
298
|
}
|
|
@@ -317,24 +330,47 @@ function mergeScriptValue(newSrc, cur) {
|
|
|
317
330
|
return newSrc
|
|
318
331
|
}
|
|
319
332
|
|
|
320
|
-
function
|
|
333
|
+
function rewritePackageManagerRunRefs(scripts, srcKeys) {
|
|
321
334
|
const keys = srcKeys
|
|
322
335
|
.filter((k) => k !== toGruntScriptName(k))
|
|
323
336
|
.sort((a, b) => b.length - a.length)
|
|
324
337
|
if (!keys.length) return
|
|
338
|
+
const pm = PACKAGE_MANAGERS.join("|")
|
|
325
339
|
for (const name of Object.keys(scripts)) {
|
|
326
340
|
let val = scripts[name]
|
|
327
341
|
if (typeof val !== "string") continue
|
|
328
342
|
for (const k of keys) {
|
|
329
343
|
val = val.replace(
|
|
330
|
-
new RegExp(`
|
|
331
|
-
|
|
344
|
+
new RegExp(`(${pm}) run ${escapeRe(k)}(?=$|\\s|;|&)`, "g"),
|
|
345
|
+
`$1 run ${toGruntScriptName(k)}`,
|
|
332
346
|
)
|
|
333
347
|
}
|
|
334
348
|
scripts[name] = val
|
|
335
349
|
}
|
|
336
350
|
}
|
|
337
351
|
|
|
352
|
+
export function placeGruntDevDependency(destPkg, srcPkg) {
|
|
353
|
+
const deps =
|
|
354
|
+
destPkg.dependencies && typeof destPkg.dependencies === "object"
|
|
355
|
+
? destPkg.dependencies
|
|
356
|
+
: null
|
|
357
|
+
destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
|
|
358
|
+
const fromDep = deps?.[GRUNT_PACKAGE]
|
|
359
|
+
const fromDev = destPkg.devDependencies[GRUNT_PACKAGE]
|
|
360
|
+
if (fromDep != null && deps) {
|
|
361
|
+
delete destPkg.dependencies[GRUNT_PACKAGE]
|
|
362
|
+
if (fromDev == null) destPkg.devDependencies[GRUNT_PACKAGE] = fromDep
|
|
363
|
+
if (Object.keys(destPkg.dependencies).length === 0) delete destPkg.dependencies
|
|
364
|
+
}
|
|
365
|
+
if (
|
|
366
|
+
destPkg.devDependencies[GRUNT_PACKAGE] == null &&
|
|
367
|
+
srcPkg?.name === GRUNT_PACKAGE &&
|
|
368
|
+
srcPkg.version
|
|
369
|
+
) {
|
|
370
|
+
destPkg.devDependencies[GRUNT_PACKAGE] = srcPkg.version
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
338
374
|
export function mergePackageJson(dest, pkgRoot) {
|
|
339
375
|
const destPath = path.join(dest, "package.json")
|
|
340
376
|
let destPkg = {}
|
|
@@ -377,18 +413,47 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
377
413
|
const staleKey = `grunt:yolo:${k}`
|
|
378
414
|
if (destPkg.scripts[staleKey] === v) delete destPkg.scripts[staleKey]
|
|
379
415
|
}
|
|
380
|
-
|
|
416
|
+
rewritePackageManagerRunRefs(destPkg.scripts, srcKeys)
|
|
381
417
|
destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
|
|
382
418
|
destPkg.devDependencies["smol-toml"] = srcPkg.devDependencies["smol-toml"]
|
|
383
419
|
destPkg.devDependencies["rulesync"] = srcPkg.devDependencies["rulesync"]
|
|
384
420
|
const clack = srcPkg.dependencies?.["@clack/prompts"]
|
|
385
421
|
if (clack) destPkg.devDependencies["@clack/prompts"] = clack
|
|
422
|
+
placeGruntDevDependency(destPkg, srcPkg)
|
|
386
423
|
destPkg.scripts = sortKeys(destPkg.scripts)
|
|
387
424
|
destPkg.devDependencies = sortKeys(destPkg.devDependencies)
|
|
388
425
|
fs.writeFileSync(destPath, `${JSON.stringify(destPkg, null, 2)}\n`)
|
|
389
426
|
}
|
|
390
427
|
|
|
391
|
-
|
|
428
|
+
function copyRootIfMissing(dest, pkgRoot) {
|
|
429
|
+
for (const file of COPY_ROOT_IF_MISSING) {
|
|
430
|
+
const src = path.join(pkgRoot, file)
|
|
431
|
+
const d = path.join(dest, file)
|
|
432
|
+
if (!fs.existsSync(src) || fs.existsSync(d)) continue
|
|
433
|
+
fs.copyFileSync(src, d)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function emitDestMaps(dest) {
|
|
438
|
+
if (!fs.existsSync(path.join(dest, LAW_REL))) return
|
|
439
|
+
const maps = emitMaps({ workspaceRoot: dest })
|
|
440
|
+
if (!maps.ok) throw new Error(maps.error || "emit-maps failed")
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function resolveInitPackageManager(dest, { packageManager, env = process.env } = {}) {
|
|
444
|
+
if (packageManager) {
|
|
445
|
+
const name = String(packageManager).toLowerCase()
|
|
446
|
+
if (!isPackageManager(name)) {
|
|
447
|
+
throw new Error(UNKNOWN_PACKAGE_MANAGER)
|
|
448
|
+
}
|
|
449
|
+
return name
|
|
450
|
+
}
|
|
451
|
+
const detected = detectPackageManager({ cwd: dest, env })
|
|
452
|
+
if (!detected.manager) throw new Error(UNKNOWN_PACKAGE_MANAGER)
|
|
453
|
+
return detected.manager
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync, skipGlobals = false, applyGlobals, onPhase, packageManager, env = process.env } = {}) {
|
|
392
457
|
dest = path.resolve(dest)
|
|
393
458
|
const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
|
|
394
459
|
const skipGlobalsApply =
|
|
@@ -429,6 +494,12 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
429
494
|
filter: (s) => path.basename(s) !== "settings.json",
|
|
430
495
|
})
|
|
431
496
|
mergeClaudeSettings(dest, pkgRoot)
|
|
497
|
+
} else if (dir === ".rulesync") {
|
|
498
|
+
fs.cpSync(src, d, {
|
|
499
|
+
recursive: true,
|
|
500
|
+
force: true,
|
|
501
|
+
filter: (s) => !GENERATED_MAP_FILES.has(path.basename(s)),
|
|
502
|
+
})
|
|
432
503
|
} else {
|
|
433
504
|
fs.cpSync(src, d, { recursive: true, force: true })
|
|
434
505
|
}
|
|
@@ -453,26 +524,31 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
453
524
|
fs.mkdirSync(path.join(dest, ".tmp"), { recursive: true })
|
|
454
525
|
mergeGitignore(dest)
|
|
455
526
|
|
|
456
|
-
if (!self)
|
|
527
|
+
if (!self) {
|
|
528
|
+
mergePackageJson(dest, pkgRoot)
|
|
529
|
+
copyRootIfMissing(dest, pkgRoot)
|
|
530
|
+
emitDestMaps(dest)
|
|
531
|
+
}
|
|
457
532
|
})
|
|
458
533
|
|
|
459
534
|
// Self-skip: file/dir merge + .tmp + gitignore only. No package.json merge,
|
|
460
|
-
//
|
|
535
|
+
// install, or generate/sync/check (would mutate this package in-place).
|
|
461
536
|
if (self) return
|
|
462
537
|
|
|
463
|
-
const
|
|
538
|
+
const pm = resolveInitPackageManager(dest, { packageManager, env })
|
|
539
|
+
const runPm = (name, args) => {
|
|
464
540
|
onPhase?.(name, "start")
|
|
465
541
|
onPhase?.(name, "stop")
|
|
466
|
-
exec(
|
|
542
|
+
exec(pm, args, { cwd: dest, stdio: "inherit" })
|
|
467
543
|
}
|
|
468
|
-
|
|
544
|
+
runPm("install", installArgs(pm))
|
|
469
545
|
const guardedSnap = snapshotGuardedRoots(dest)
|
|
470
|
-
|
|
546
|
+
runPm("generate", runScriptArgs(pm, toGruntScriptName("rulesync:generate")))
|
|
471
547
|
remergeGuardedRoots(dest, guardedSnap)
|
|
472
548
|
if (!skipGlobalsApply) {
|
|
473
|
-
|
|
549
|
+
runPm("sync-globals", runScriptArgs(pm, toGruntScriptName("sync:globals:apply")))
|
|
474
550
|
}
|
|
475
551
|
withGuardedCheckInteriors(dest, () => {
|
|
476
|
-
|
|
552
|
+
runPm("check", runScriptArgs(pm, toGruntScriptName("rulesync:check")))
|
|
477
553
|
})
|
|
478
554
|
}
|
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) {
|
|
@@ -1408,6 +1428,113 @@ describe("mergePackageJson", () => {
|
|
|
1408
1428
|
expect(out.scripts.check).toBe("npm run grunt:rulesync:check && npm run rulesync:check:raw");
|
|
1409
1429
|
});
|
|
1410
1430
|
|
|
1431
|
+
it("rewrites yarn/pnpm/bun run refs the same way as npm run", () => {
|
|
1432
|
+
const pkgRoot = stubPkgRoot({
|
|
1433
|
+
name: "fixture-pkg",
|
|
1434
|
+
scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
|
|
1435
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1436
|
+
});
|
|
1437
|
+
const dest = tmp("pj-pm-rewrite-");
|
|
1438
|
+
fs.writeFileSync(
|
|
1439
|
+
path.join(dest, "package.json"),
|
|
1440
|
+
JSON.stringify({
|
|
1441
|
+
scripts: {
|
|
1442
|
+
"rulesync:check": "node ./scripts/guarded-roots.mjs check",
|
|
1443
|
+
check: "pnpm run rulesync:check && yarn run rulesync:check && bun run rulesync:check",
|
|
1444
|
+
},
|
|
1445
|
+
}),
|
|
1446
|
+
);
|
|
1447
|
+
mergePackageJson(dest, pkgRoot);
|
|
1448
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1449
|
+
expect(out.scripts.check).toBe(
|
|
1450
|
+
"pnpm run grunt:rulesync:check && yarn run grunt:rulesync:check && bun run grunt:rulesync:check",
|
|
1451
|
+
);
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
it("moves @lovrozagar/grunt from dependencies to devDependencies", () => {
|
|
1455
|
+
const pkgRoot = stubPkgRoot({
|
|
1456
|
+
name: GRUNT_PACKAGE,
|
|
1457
|
+
version: "0.6.2",
|
|
1458
|
+
scripts: { "rulesync:generate": "gen" },
|
|
1459
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1460
|
+
});
|
|
1461
|
+
const dest = tmp("pj-grunt-dep-");
|
|
1462
|
+
fs.writeFileSync(
|
|
1463
|
+
path.join(dest, "package.json"),
|
|
1464
|
+
JSON.stringify({
|
|
1465
|
+
name: "app",
|
|
1466
|
+
dependencies: { [GRUNT_PACKAGE]: "0.6.2", lodash: "4" },
|
|
1467
|
+
scripts: {},
|
|
1468
|
+
}),
|
|
1469
|
+
);
|
|
1470
|
+
mergePackageJson(dest, pkgRoot);
|
|
1471
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1472
|
+
expect(out.dependencies).toEqual({ lodash: "4" });
|
|
1473
|
+
expect(out.devDependencies[GRUNT_PACKAGE]).toBe("0.6.2");
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it("drops empty dependencies after moving grunt", () => {
|
|
1477
|
+
const pkgRoot = stubPkgRoot({
|
|
1478
|
+
name: GRUNT_PACKAGE,
|
|
1479
|
+
version: "0.7.0",
|
|
1480
|
+
scripts: {},
|
|
1481
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1482
|
+
});
|
|
1483
|
+
const dest = tmp("pj-grunt-onlydep-");
|
|
1484
|
+
fs.writeFileSync(
|
|
1485
|
+
path.join(dest, "package.json"),
|
|
1486
|
+
JSON.stringify({ dependencies: { [GRUNT_PACKAGE]: "0.6.2" } }),
|
|
1487
|
+
);
|
|
1488
|
+
mergePackageJson(dest, pkgRoot);
|
|
1489
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1490
|
+
expect(out.dependencies).toBeUndefined();
|
|
1491
|
+
expect(out.devDependencies[GRUNT_PACKAGE]).toBe("0.6.2");
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
it("adds missing grunt as a devDependency from the running package version", () => {
|
|
1495
|
+
const pkgRoot = stubPkgRoot({
|
|
1496
|
+
name: GRUNT_PACKAGE,
|
|
1497
|
+
version: "0.7.0",
|
|
1498
|
+
scripts: {},
|
|
1499
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1500
|
+
});
|
|
1501
|
+
const dest = tmp("pj-grunt-add-");
|
|
1502
|
+
fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ name: "app" }));
|
|
1503
|
+
mergePackageJson(dest, pkgRoot);
|
|
1504
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1505
|
+
expect(out.devDependencies[GRUNT_PACKAGE]).toBe("0.7.0");
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
it("keeps an existing grunt devDependency version", () => {
|
|
1509
|
+
const pkgRoot = stubPkgRoot({
|
|
1510
|
+
name: GRUNT_PACKAGE,
|
|
1511
|
+
version: "0.7.0",
|
|
1512
|
+
scripts: {},
|
|
1513
|
+
devDependencies: { "smol-toml": "^1.8.0", rulesync: "latest" },
|
|
1514
|
+
});
|
|
1515
|
+
const dest = tmp("pj-grunt-keep-");
|
|
1516
|
+
fs.writeFileSync(
|
|
1517
|
+
path.join(dest, "package.json"),
|
|
1518
|
+
JSON.stringify({
|
|
1519
|
+
devDependencies: { [GRUNT_PACKAGE]: "^0.6.2" },
|
|
1520
|
+
dependencies: { [GRUNT_PACKAGE]: "0.6.2" },
|
|
1521
|
+
}),
|
|
1522
|
+
);
|
|
1523
|
+
mergePackageJson(dest, pkgRoot);
|
|
1524
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1525
|
+
expect(out.dependencies).toBeUndefined();
|
|
1526
|
+
expect(out.devDependencies[GRUNT_PACKAGE]).toBe("^0.6.2");
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
it("placeGruntDevDependency is a no-op when src is not grunt", () => {
|
|
1530
|
+
const destPkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> } = {
|
|
1531
|
+
dependencies: { lodash: "4" },
|
|
1532
|
+
};
|
|
1533
|
+
placeGruntDevDependency(destPkg, { name: "fixture-pkg", version: "1.0.0" });
|
|
1534
|
+
expect(destPkg.dependencies).toEqual({ lodash: "4" });
|
|
1535
|
+
expect(destPkg.devDependencies?.[GRUNT_PACKAGE]).toBeUndefined();
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1411
1538
|
it("skips non-string dest scripts when rewriting npm run refs", () => {
|
|
1412
1539
|
const pkgRoot = stubPkgRoot({
|
|
1413
1540
|
name: "fixture-pkg",
|
|
@@ -1526,7 +1653,7 @@ describe("init", () => {
|
|
|
1526
1653
|
const pkgRoot = stubPkgRoot();
|
|
1527
1654
|
const dest = tmp("grunt-dest-");
|
|
1528
1655
|
const exec = vi.fn();
|
|
1529
|
-
init(dest, { pkgRoot, execFileSync: exec });
|
|
1656
|
+
init(dest, { pkgRoot, execFileSync: exec, packageManager: "npm" });
|
|
1530
1657
|
|
|
1531
1658
|
for (const d of COPY_DIRS) {
|
|
1532
1659
|
if (d === ".claude") continue;
|
|
@@ -1741,7 +1868,7 @@ describe("init", () => {
|
|
|
1741
1868
|
const pkgRoot = stubPkgRoot();
|
|
1742
1869
|
const dest = tmp("grunt-skip-globals-");
|
|
1743
1870
|
const exec = vi.fn();
|
|
1744
|
-
init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true });
|
|
1871
|
+
init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true, packageManager: "npm" });
|
|
1745
1872
|
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
1746
1873
|
["npm", ["install"]],
|
|
1747
1874
|
["npm", ["run", "grunt:rulesync:generate"]],
|
|
@@ -1879,6 +2006,128 @@ describe("init", () => {
|
|
|
1879
2006
|
expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
1880
2007
|
});
|
|
1881
2008
|
|
|
2009
|
+
it("pnpm-lock.yaml dest installs with pnpm even if invocation is npm", () => {
|
|
2010
|
+
const pkgRoot = stubPkgRoot();
|
|
2011
|
+
const dest = tmp("grunt-pnpm-");
|
|
2012
|
+
fs.writeFileSync(path.join(dest, "pnpm-lock.yaml"), "lockfileVersion: 9\n");
|
|
2013
|
+
const exec = vi.fn();
|
|
2014
|
+
init(dest, {
|
|
2015
|
+
pkgRoot,
|
|
2016
|
+
execFileSync: exec,
|
|
2017
|
+
env: { npm_config_user_agent: "npm/10.0.0 node/v22" },
|
|
2018
|
+
});
|
|
2019
|
+
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
2020
|
+
["pnpm", ["install"]],
|
|
2021
|
+
["pnpm", ["run", "grunt:rulesync:generate"]],
|
|
2022
|
+
["pnpm", ["run", "grunt:sync:globals:apply"]],
|
|
2023
|
+
["pnpm", ["run", "grunt:rulesync:check"]],
|
|
2024
|
+
]);
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
it("packageManager option wins over dest lockfile", () => {
|
|
2028
|
+
const pkgRoot = stubPkgRoot();
|
|
2029
|
+
const dest = tmp("grunt-pm-opt-");
|
|
2030
|
+
fs.writeFileSync(path.join(dest, "yarn.lock"), "");
|
|
2031
|
+
const exec = vi.fn();
|
|
2032
|
+
init(dest, { pkgRoot, execFileSync: exec, packageManager: "bun" });
|
|
2033
|
+
expect(exec.mock.calls[0][0]).toBe("bun");
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
it("unknown manager throws and does not exec", () => {
|
|
2037
|
+
const pkgRoot = stubPkgRoot();
|
|
2038
|
+
const dest = tmp("grunt-pm-unknown-");
|
|
2039
|
+
const exec = vi.fn();
|
|
2040
|
+
expect(() =>
|
|
2041
|
+
init(dest, { pkgRoot, execFileSync: exec, env: {}, packageManager: undefined }),
|
|
2042
|
+
).toThrow(UNKNOWN_PACKAGE_MANAGER);
|
|
2043
|
+
expect(exec).not.toHaveBeenCalled();
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
it("invalid packageManager option throws", () => {
|
|
2047
|
+
const pkgRoot = stubPkgRoot();
|
|
2048
|
+
const dest = tmp("grunt-pm-bad-");
|
|
2049
|
+
const exec = vi.fn();
|
|
2050
|
+
expect(() => init(dest, { pkgRoot, execFileSync: exec, packageManager: "deno" })).toThrow(
|
|
2051
|
+
UNKNOWN_PACKAGE_MANAGER,
|
|
2052
|
+
);
|
|
2053
|
+
expect(exec).not.toHaveBeenCalled();
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
it("yarn.lock dest uses yarn run args without npm --", () => {
|
|
2057
|
+
const pkgRoot = stubPkgRoot();
|
|
2058
|
+
const dest = tmp("grunt-yarn-");
|
|
2059
|
+
fs.writeFileSync(path.join(dest, "yarn.lock"), "");
|
|
2060
|
+
const exec = vi.fn();
|
|
2061
|
+
init(dest, { pkgRoot, execFileSync: exec, skipGlobals: true, env: {} });
|
|
2062
|
+
expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
|
|
2063
|
+
["yarn", ["install"]],
|
|
2064
|
+
["yarn", ["run", "grunt:rulesync:generate"]],
|
|
2065
|
+
["yarn", ["run", "grunt:rulesync:check"]],
|
|
2066
|
+
]);
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
it("merges dest refs into INDEX and writes .mcp.json when missing", () => {
|
|
2070
|
+
const dest = tmp("grunt-maps-merge-");
|
|
2071
|
+
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
2072
|
+
fs.writeFileSync(
|
|
2073
|
+
path.join(dest, ".rulesync", "reference", "stripe.md"),
|
|
2074
|
+
"# Stripe\n\nConsumer stripe payments ref.\n",
|
|
2075
|
+
);
|
|
2076
|
+
const exec = vi.fn();
|
|
2077
|
+
init(dest, { execFileSync: exec, skipGlobals: true, packageManager: "npm" });
|
|
2078
|
+
expect(GENERATED_MAP_FILES.has("INDEX.md")).toBe(true);
|
|
2079
|
+
const index = fs.readFileSync(path.join(dest, ".rulesync", "reference", "INDEX.md"), "utf8");
|
|
2080
|
+
expect(index).toMatch(/stripe\.md/);
|
|
2081
|
+
expect(index).toMatch(/Consumer stripe payments ref/);
|
|
2082
|
+
expect(index).toMatch(/browser\.md/);
|
|
2083
|
+
const refsMap = fs.readFileSync(path.join(dest, ".rulesync", "reference", "refs-map.md"), "utf8");
|
|
2084
|
+
expect(refsMap).toMatch(/stripe\.md/);
|
|
2085
|
+
expect(fs.existsSync(path.join(dest, ".mcp.json"))).toBe(true);
|
|
2086
|
+
expect(JSON.parse(fs.readFileSync(path.join(dest, ".mcp.json"), "utf8"))).toEqual({
|
|
2087
|
+
mcpServers: {},
|
|
2088
|
+
});
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
it("keeps dest .mcp.json when already present", () => {
|
|
2092
|
+
const dest = tmp("grunt-mcp-keep-");
|
|
2093
|
+
fs.writeFileSync(
|
|
2094
|
+
path.join(dest, ".mcp.json"),
|
|
2095
|
+
`${JSON.stringify({ mcpServers: { tasks: { command: "npx" } } }, null, 2)}\n`,
|
|
2096
|
+
);
|
|
2097
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
|
|
2098
|
+
expect(JSON.parse(fs.readFileSync(path.join(dest, ".mcp.json"), "utf8"))).toEqual({
|
|
2099
|
+
mcpServers: { tasks: { command: "npx" } },
|
|
2100
|
+
});
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
it("throws when dest emit-maps fails", () => {
|
|
2104
|
+
emitMaps.mockReturnValue({ ok: false, error: "maps boom" });
|
|
2105
|
+
const dest = tmp("grunt-maps-fail-");
|
|
2106
|
+
expect(() =>
|
|
2107
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" }),
|
|
2108
|
+
).toThrow("maps boom");
|
|
2109
|
+
emitMaps.mockReturnValue({ ok: false });
|
|
2110
|
+
const dest2 = tmp("grunt-maps-fail2-");
|
|
2111
|
+
expect(() =>
|
|
2112
|
+
init(dest2, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" }),
|
|
2113
|
+
).toThrow("emit-maps failed");
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2116
|
+
it("does not stamp package INDEX over dest; re-emits union", () => {
|
|
2117
|
+
const dest = tmp("grunt-index-keep-");
|
|
2118
|
+
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
2119
|
+
fs.writeFileSync(path.join(dest, ".rulesync", "reference", "owned.md"), "# Owned\n\nDest-only.\n");
|
|
2120
|
+
fs.writeFileSync(
|
|
2121
|
+
path.join(dest, ".rulesync", "reference", "INDEX.md"),
|
|
2122
|
+
"stale grunt-only index\n",
|
|
2123
|
+
);
|
|
2124
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
|
|
2125
|
+
const index = fs.readFileSync(path.join(dest, ".rulesync", "reference", "INDEX.md"), "utf8");
|
|
2126
|
+
expect(index).not.toBe("stale grunt-only index\n");
|
|
2127
|
+
expect(index).toMatch(/owned\.md/);
|
|
2128
|
+
expect(index).toMatch(/browser\.md/);
|
|
2129
|
+
});
|
|
2130
|
+
|
|
1882
2131
|
it("onPhase merge then npm phases stop before exec", () => {
|
|
1883
2132
|
const pkgRoot = stubPkgRoot();
|
|
1884
2133
|
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.7.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",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"scripts/pipeline.mjs",
|
|
37
37
|
"scripts/grunt-job.mjs",
|
|
38
38
|
"scripts/parse-need.mjs",
|
|
39
|
+
"scripts/package-manager.mjs",
|
|
39
40
|
"scripts/persist-handoff.mjs",
|
|
40
41
|
"scripts/persist-implementation.mjs",
|
|
41
42
|
"scripts/persist-tmp.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
|
|