@lovrozagar/grunt 0.9.3 → 0.9.5
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/.rulesync/reference/INDEX.md +1 -1
- package/.rulesync/reference/law.md +1 -1
- package/CHANGELOG.md +8 -0
- package/cli/init.mjs +93 -7
- package/cli/init.test.ts +241 -21
- package/package.json +3 -3
- package/scripts/doctor.mjs +15 -2
- package/scripts/guarded-md.mjs +28 -0
- package/scripts/listen.mjs +2 -1
- package/scripts/pipeline.mjs +84 -2
- package/scripts/session-map.mjs +1 -1
- package/scripts/speak.mjs +2 -0
|
@@ -10,7 +10,7 @@ Reserved (do not reuse in consumer custom skills): `ask` `auto` `browser` `clasp
|
|
|
10
10
|
|
|
11
11
|
Override: same name → one SSOT path (`.rulesync/skills/<name>/`). Re-init / `grunt upgrade` force-refresh overwrites grunt-owned names; consumer extras kept. Maps `origin` badge ≠ content picker.
|
|
12
12
|
|
|
13
|
-
Upgrade copies the current package trees and product scripts, then deletes cumulative retired grunt-owned names (skills `parent` `solo` `cascade`; agents `implementer` `thinker`; scripts `telemetry.mjs` `grunt-config.mjs`; paths `.grok/parent.md` `.grok/skills/shared` `.rulesync/grunt.config.jsonc` `.rulesync/grunt.config.local.jsonc` `.rulesync/grunt.config.local.jsonc.example`) and any reserved skill dir this package no longer ships. `fs.cpSync` does not remove dest extras; prune is what drops them. Consumer skills/refs/scripts not on those lists stay.
|
|
13
|
+
Upgrade copies the current package trees and product scripts, then deletes cumulative retired grunt-owned names (skills `parent` `solo` `cascade`; agents `implementer` `thinker`; scripts `telemetry.mjs` `grunt-config.mjs`; paths `.grok/parent.md` `.grok/skills/shared` `.rulesync/grunt.config.jsonc` `.rulesync/grunt.config.local.jsonc` `.rulesync/grunt.config.local.jsonc.example`) and any reserved skill dir this package no longer ships. `fs.cpSync` does not remove dest extras; prune is what drops them. Consumer skills/refs/scripts not on those lists stay. Extra `##` headings in shared reference docs (e.g. `law.md`) are kept. `grunt:*` package scripts that differ from a known grunt default stay; `grunt:test:e2e` is added only when a vitest e2e config exists. Check steps omitted from dest `scripts/pipeline.mjs` seed `.rulesync/grunt.pipeline.jsonc`. doctor/listen/speak call `scripts/lib/env.mjs` `loadEnv` when that file exists.
|
|
14
14
|
|
|
15
15
|
# Skills
|
|
16
16
|
|
|
@@ -8,4 +8,4 @@ Reserved (do not reuse in consumer custom skills): `ask` `auto` `browser` `clasp
|
|
|
8
8
|
|
|
9
9
|
Override: same name → one SSOT path (`.rulesync/skills/<name>/`). Re-init / `grunt upgrade` force-refresh overwrites grunt-owned names; consumer extras kept. Maps `origin` badge ≠ content picker.
|
|
10
10
|
|
|
11
|
-
Upgrade copies the current package trees and product scripts, then deletes cumulative retired grunt-owned names (skills `parent` `solo` `cascade`; agents `implementer` `thinker`; scripts `telemetry.mjs` `grunt-config.mjs`; paths `.grok/parent.md` `.grok/skills/shared` `.rulesync/grunt.config.jsonc` `.rulesync/grunt.config.local.jsonc` `.rulesync/grunt.config.local.jsonc.example`) and any reserved skill dir this package no longer ships. `fs.cpSync` does not remove dest extras; prune is what drops them. Consumer skills/refs/scripts not on those lists stay.
|
|
11
|
+
Upgrade copies the current package trees and product scripts, then deletes cumulative retired grunt-owned names (skills `parent` `solo` `cascade`; agents `implementer` `thinker`; scripts `telemetry.mjs` `grunt-config.mjs`; paths `.grok/parent.md` `.grok/skills/shared` `.rulesync/grunt.config.jsonc` `.rulesync/grunt.config.local.jsonc` `.rulesync/grunt.config.local.jsonc.example`) and any reserved skill dir this package no longer ships. `fs.cpSync` does not remove dest extras; prune is what drops them. Consumer skills/refs/scripts not on those lists stay. Extra `##` headings in shared reference docs (e.g. `law.md`) are kept. `grunt:*` package scripts that differ from a known grunt default stay; `grunt:test:e2e` is added only when a vitest e2e config exists. Check steps omitted from dest `scripts/pipeline.mjs` seed `.rulesync/grunt.pipeline.jsonc`. doctor/listen/speak call `scripts/lib/env.mjs` `loadEnv` when that file exists.
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `grunt upgrade` keeps extra `##` sections in shared reference docs such as `law.md`, so consumer INDEX content survives the copy
|
|
13
|
+
- Custom `grunt:*` package scripts that are not a known grunt default stay; `grunt:test:e2e` is added only when a vitest e2e config exists
|
|
14
|
+
- Check steps a consumer omitted from `scripts/pipeline.mjs` seed `.rulesync/grunt.pipeline.jsonc` and are skipped; a failed `grunt:rulesync:check` lists every failing step and points at that overlay
|
|
15
|
+
- doctor, listen, and speak call `scripts/lib/env.mjs` `loadEnv` when that file exists, so consumers do not need to patch those scripts
|
|
16
|
+
- `scripts/session-map.mjs` no longer trips ESLint `no-regex-spaces`
|
|
17
|
+
|
|
10
18
|
## [0.9.1] - 2026-10-04
|
|
11
19
|
|
|
12
20
|
### Fixed
|
package/cli/init.mjs
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
snapshotGuardedRoots,
|
|
16
16
|
remergeGuardedRoots,
|
|
17
17
|
withGuardedCheckInteriors,
|
|
18
|
+
mergeReferenceMarkdown,
|
|
18
19
|
} from "../scripts/guarded-md.mjs"
|
|
20
|
+
import {
|
|
21
|
+
COMMANDS as PIPELINE_COMMANDS,
|
|
22
|
+
PIPELINE_CONFIG_REL,
|
|
23
|
+
inferPipelineSkip,
|
|
24
|
+
} from "../scripts/pipeline.mjs"
|
|
19
25
|
import {
|
|
20
26
|
PACKAGE_MANAGERS,
|
|
21
27
|
UNKNOWN_PACKAGE_MANAGER,
|
|
@@ -41,6 +47,8 @@ export {
|
|
|
41
47
|
remergeGuardedRoots,
|
|
42
48
|
healGuardedRootFile,
|
|
43
49
|
withGuardedCheckInteriors,
|
|
50
|
+
extraH2Sections,
|
|
51
|
+
mergeReferenceMarkdown,
|
|
44
52
|
} from "../scripts/guarded-md.mjs"
|
|
45
53
|
|
|
46
54
|
const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
@@ -147,6 +155,23 @@ export const LAUNCH_SCRIPTS = {
|
|
|
147
155
|
gemini: "gemini --yolo",
|
|
148
156
|
grok: "grok --yolo",
|
|
149
157
|
}
|
|
158
|
+
/** Exact prior grunt defaults. A dest script equal to one of these migrates to the current value. */
|
|
159
|
+
export const HISTORICAL_SCRIPT_DEFAULTS = {
|
|
160
|
+
antigravity: ["antigravity"],
|
|
161
|
+
claude: ["claude"],
|
|
162
|
+
codex: ["codex"],
|
|
163
|
+
gemini: ["gemini"],
|
|
164
|
+
grok: ["grok"],
|
|
165
|
+
}
|
|
166
|
+
export const E2E_VITEST_CONFIGS = [
|
|
167
|
+
"vitest.e2e.config.ts",
|
|
168
|
+
"vitest.e2e.config.mts",
|
|
169
|
+
"vitest.e2e.config.js",
|
|
170
|
+
"vitest.e2e.config.mjs",
|
|
171
|
+
"vitest.e2e.config.cjs",
|
|
172
|
+
]
|
|
173
|
+
export const REFERENCE_REL = ".rulesync/reference"
|
|
174
|
+
export { PIPELINE_CONFIG_REL }
|
|
150
175
|
const OWNED_HOOK_FILES = [
|
|
151
176
|
"scrub-spawn-prompt.mjs",
|
|
152
177
|
"gate-fat-tools.mjs",
|
|
@@ -333,11 +358,61 @@ function extraOwnedSuffix(cur, newSrc) {
|
|
|
333
358
|
return null
|
|
334
359
|
}
|
|
335
360
|
|
|
336
|
-
function mergeScriptValue(newSrc, cur) {
|
|
361
|
+
function mergeScriptValue(newSrc, cur, historical = []) {
|
|
337
362
|
if (cur.startsWith(newSrc)) return cur
|
|
338
363
|
const suffix = extraOwnedSuffix(cur, newSrc)
|
|
339
364
|
if (suffix != null) return newSrc + suffix
|
|
340
|
-
return newSrc
|
|
365
|
+
if (historical.includes(cur)) return newSrc
|
|
366
|
+
if (looksGruntOwnedPrefix(cur)) return newSrc
|
|
367
|
+
return cur
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function historicalFor(key) {
|
|
371
|
+
return HISTORICAL_SCRIPT_DEFAULTS[key] || []
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function hasE2eConfig(dest) {
|
|
375
|
+
return E2E_VITEST_CONFIGS.some((name) => fs.existsSync(path.join(dest, name)))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function snapshotReferenceDocs(dest) {
|
|
379
|
+
const dir = path.join(dest, REFERENCE_REL)
|
|
380
|
+
const snap = {}
|
|
381
|
+
if (!fs.existsSync(dir)) return snap
|
|
382
|
+
for (const name of fs.readdirSync(dir)) {
|
|
383
|
+
if (!name.endsWith(".md") || GENERATED_MAP_FILES.has(name)) continue
|
|
384
|
+
const abs = path.join(dir, name)
|
|
385
|
+
if (!fs.statSync(abs).isFile()) continue
|
|
386
|
+
snap[name] = fs.readFileSync(abs, "utf8")
|
|
387
|
+
}
|
|
388
|
+
return snap
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function remergeReferenceDocs(dest, snap, pkgRoot) {
|
|
392
|
+
const pkgDir = path.join(pkgRoot, REFERENCE_REL)
|
|
393
|
+
const destDir = path.join(dest, REFERENCE_REL)
|
|
394
|
+
for (const [name, existing] of Object.entries(snap)) {
|
|
395
|
+
const pkgFile = path.join(pkgDir, name)
|
|
396
|
+
if (!fs.existsSync(pkgFile)) continue
|
|
397
|
+
const incoming = fs.readFileSync(pkgFile, "utf8")
|
|
398
|
+
fs.mkdirSync(destDir, { recursive: true })
|
|
399
|
+
fs.writeFileSync(path.join(destDir, name), mergeReferenceMarkdown(existing, incoming))
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function seedPipelineConfig(dest, destPipelineSrc) {
|
|
404
|
+
if (!destPipelineSrc || !/export const COMMANDS/.test(destPipelineSrc)) return false
|
|
405
|
+
const abs = path.join(dest, PIPELINE_CONFIG_REL)
|
|
406
|
+
if (fs.existsSync(abs)) return false
|
|
407
|
+
const overlay = {}
|
|
408
|
+
for (const mode of Object.keys(PIPELINE_COMMANDS)) {
|
|
409
|
+
const skip = inferPipelineSkip(destPipelineSrc, mode)
|
|
410
|
+
if (skip.length) overlay[mode] = { skip }
|
|
411
|
+
}
|
|
412
|
+
if (!Object.keys(overlay).length) return false
|
|
413
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true })
|
|
414
|
+
fs.writeFileSync(abs, `${JSON.stringify(overlay, null, 2)}\n`)
|
|
415
|
+
return true
|
|
341
416
|
}
|
|
342
417
|
|
|
343
418
|
function rewritePackageManagerRunRefs(scripts, srcKeys) {
|
|
@@ -394,17 +469,19 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
394
469
|
const srcKeys = []
|
|
395
470
|
for (const [k, v] of Object.entries(srcPkg.scripts || {})) {
|
|
396
471
|
if (k === "test" || k.endsWith(":raw") || k in LAUNCH_SCRIPTS) continue
|
|
397
|
-
srcKeys.push(k)
|
|
398
472
|
const destKey = toGruntScriptName(k)
|
|
399
473
|
const current = destPkg.scripts[destKey]
|
|
400
474
|
const legacy = destPkg.scripts[k]
|
|
475
|
+
if (k === "test:e2e" && current == null && legacy == null && !hasE2eConfig(dest)) continue
|
|
476
|
+
srcKeys.push(k)
|
|
477
|
+
const historical = historicalFor(k)
|
|
401
478
|
const ownedLegacy =
|
|
402
479
|
legacy != null &&
|
|
403
480
|
(legacy.startsWith(v) || extraOwnedSuffix(legacy, v) != null || looksGruntOwnedPrefix(legacy))
|
|
404
481
|
if (current != null) {
|
|
405
|
-
destPkg.scripts[destKey] = mergeScriptValue(v, current)
|
|
482
|
+
destPkg.scripts[destKey] = mergeScriptValue(v, current, historical)
|
|
406
483
|
} else if (ownedLegacy) {
|
|
407
|
-
destPkg.scripts[destKey] = mergeScriptValue(v, legacy)
|
|
484
|
+
destPkg.scripts[destKey] = mergeScriptValue(v, legacy, historical)
|
|
408
485
|
} else {
|
|
409
486
|
destPkg.scripts[destKey] = v
|
|
410
487
|
}
|
|
@@ -419,13 +496,13 @@ export function mergePackageJson(dest, pkgRoot) {
|
|
|
419
496
|
for (const [k, v] of Object.entries(LAUNCH_SCRIPTS)) {
|
|
420
497
|
const destKey = toGruntScriptName(k)
|
|
421
498
|
const current = destPkg.scripts[destKey]
|
|
422
|
-
destPkg.scripts[destKey] = current != null ? mergeScriptValue(v, current) : v
|
|
499
|
+
destPkg.scripts[destKey] = current != null ? mergeScriptValue(v, current, historicalFor(k)) : v
|
|
423
500
|
const staleKey = `grunt:yolo:${k}`
|
|
424
501
|
if (destPkg.scripts[staleKey] === v) delete destPkg.scripts[staleKey]
|
|
425
502
|
}
|
|
426
503
|
rewritePackageManagerRunRefs(destPkg.scripts, srcKeys)
|
|
427
504
|
destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
|
|
428
|
-
destPkg.devDependencies["smol-toml"] = srcPkg.
|
|
505
|
+
destPkg.devDependencies["smol-toml"] = srcPkg.dependencies["smol-toml"]
|
|
429
506
|
destPkg.devDependencies["rulesync"] = srcPkg.devDependencies["rulesync"]
|
|
430
507
|
const clack = srcPkg.dependencies?.["@clack/prompts"]
|
|
431
508
|
if (clack) destPkg.devDependencies["@clack/prompts"] = clack
|
|
@@ -507,6 +584,13 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
507
584
|
}
|
|
508
585
|
}
|
|
509
586
|
|
|
587
|
+
const refSnap = self ? {} : snapshotReferenceDocs(dest)
|
|
588
|
+
let destPipelineSrc = ""
|
|
589
|
+
if (!self) {
|
|
590
|
+
const destPipeline = path.join(dest, "scripts", "pipeline.mjs")
|
|
591
|
+
if (fs.existsSync(destPipeline)) destPipelineSrc = fs.readFileSync(destPipeline, "utf8")
|
|
592
|
+
}
|
|
593
|
+
|
|
510
594
|
for (const dir of COPY_DIRS) {
|
|
511
595
|
const src = path.join(pkgRoot, dir)
|
|
512
596
|
const d = path.join(dest, dir)
|
|
@@ -540,6 +624,8 @@ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileS
|
|
|
540
624
|
mergeGitignore(dest)
|
|
541
625
|
|
|
542
626
|
if (!self) {
|
|
627
|
+
remergeReferenceDocs(dest, refSnap, pkgRoot)
|
|
628
|
+
seedPipelineConfig(dest, destPipelineSrc)
|
|
543
629
|
mergePackageJson(dest, pkgRoot)
|
|
544
630
|
copyRootIfMissing(dest, pkgRoot)
|
|
545
631
|
emitDestMaps(dest)
|
package/cli/init.test.ts
CHANGED
|
@@ -37,6 +37,14 @@ import {
|
|
|
37
37
|
RETIRED_PATHS,
|
|
38
38
|
RESERVED_SKILLS,
|
|
39
39
|
LAUNCH_SCRIPTS,
|
|
40
|
+
HISTORICAL_SCRIPT_DEFAULTS,
|
|
41
|
+
E2E_VITEST_CONFIGS,
|
|
42
|
+
extraH2Sections,
|
|
43
|
+
mergeReferenceMarkdown,
|
|
44
|
+
snapshotReferenceDocs,
|
|
45
|
+
remergeReferenceDocs,
|
|
46
|
+
seedPipelineConfig,
|
|
47
|
+
PIPELINE_CONFIG_REL,
|
|
40
48
|
mergeClaudeSettings,
|
|
41
49
|
mergeGitignore,
|
|
42
50
|
mergeGuardedContent,
|
|
@@ -55,6 +63,7 @@ import {
|
|
|
55
63
|
} from "./init.mjs";
|
|
56
64
|
import { UNKNOWN_PACKAGE_MANAGER } from "../scripts/package-manager.mjs";
|
|
57
65
|
import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
|
|
66
|
+
import { COMMANDS as PIPELINE_COMMANDS } from "../scripts/pipeline.mjs";
|
|
58
67
|
|
|
59
68
|
const tmpDirs: string[] = [];
|
|
60
69
|
afterEach(() => {
|
|
@@ -137,8 +146,10 @@ function stubPkgRoot(pkg: Record<string, unknown> = {
|
|
|
137
146
|
alpha: "a",
|
|
138
147
|
"rulesync:generate": "gen",
|
|
139
148
|
},
|
|
140
|
-
|
|
149
|
+
dependencies: {
|
|
141
150
|
"smol-toml": "^1.8.0",
|
|
151
|
+
},
|
|
152
|
+
devDependencies: {
|
|
142
153
|
rulesync: "latest",
|
|
143
154
|
zzz: "1",
|
|
144
155
|
},
|
|
@@ -1115,7 +1126,8 @@ describe("mergePackageJson", () => {
|
|
|
1115
1126
|
const pkgRoot = stubPkgRoot({
|
|
1116
1127
|
name: "fixture-pkg",
|
|
1117
1128
|
scripts: { "rulesync:generate": "rulesync generate -t bar" },
|
|
1118
|
-
|
|
1129
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1130
|
+
devDependencies: { rulesync: "latest" },
|
|
1119
1131
|
});
|
|
1120
1132
|
const dest = tmp("pj-suffix-");
|
|
1121
1133
|
fs.writeFileSync(
|
|
@@ -1140,7 +1152,8 @@ describe("mergePackageJson", () => {
|
|
|
1140
1152
|
scripts: {
|
|
1141
1153
|
"rulesync:generate": "rulesync generate -t bar && node scripts/emit.mjs",
|
|
1142
1154
|
},
|
|
1143
|
-
|
|
1155
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1156
|
+
devDependencies: { rulesync: "latest" },
|
|
1144
1157
|
});
|
|
1145
1158
|
const dest = tmp("pj-multi-suffix-");
|
|
1146
1159
|
fs.writeFileSync(
|
|
@@ -1163,7 +1176,8 @@ describe("mergePackageJson", () => {
|
|
|
1163
1176
|
const pkgRoot = stubPkgRoot({
|
|
1164
1177
|
name: "fixture-pkg",
|
|
1165
1178
|
scripts: { alpha: "a" },
|
|
1166
|
-
|
|
1179
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1180
|
+
devDependencies: { rulesync: "latest" },
|
|
1167
1181
|
});
|
|
1168
1182
|
const dest = tmp("pj-nonowned-suffix-");
|
|
1169
1183
|
fs.writeFileSync(
|
|
@@ -1183,7 +1197,8 @@ describe("mergePackageJson", () => {
|
|
|
1183
1197
|
const pkgRoot = stubPkgRoot({
|
|
1184
1198
|
name: "fixture-pkg",
|
|
1185
1199
|
scripts: { "rulesync:generate": "rulesync generate -t bar" },
|
|
1186
|
-
|
|
1200
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1201
|
+
devDependencies: { rulesync: "latest" },
|
|
1187
1202
|
});
|
|
1188
1203
|
const dest = tmp("pj-semi-");
|
|
1189
1204
|
fs.writeFileSync(
|
|
@@ -1205,7 +1220,8 @@ describe("mergePackageJson", () => {
|
|
|
1205
1220
|
const pkgRoot = stubPkgRoot({
|
|
1206
1221
|
name: "fixture-pkg",
|
|
1207
1222
|
scripts: { "rulesync:generate": "rulesync generate -t bar" },
|
|
1208
|
-
|
|
1223
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1224
|
+
devDependencies: { rulesync: "latest" },
|
|
1209
1225
|
});
|
|
1210
1226
|
const dest = tmp("pj-equal-");
|
|
1211
1227
|
fs.writeFileSync(
|
|
@@ -1222,7 +1238,8 @@ describe("mergePackageJson", () => {
|
|
|
1222
1238
|
const pkgRoot = stubPkgRoot({
|
|
1223
1239
|
name: "fixture-pkg",
|
|
1224
1240
|
scripts: { "rulesync:generate": "rulesync generate -t bar" },
|
|
1225
|
-
|
|
1241
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1242
|
+
devDependencies: { rulesync: "latest" },
|
|
1226
1243
|
});
|
|
1227
1244
|
const dest = tmp("pj-prefix-");
|
|
1228
1245
|
const cur = "rulesync generate -t bar && node scripts/codex-sync.mjs";
|
|
@@ -1266,7 +1283,8 @@ describe("mergePackageJson", () => {
|
|
|
1266
1283
|
"rulesync:check:raw": "echo check-raw",
|
|
1267
1284
|
"rulesync:watch:raw": "echo watch-raw",
|
|
1268
1285
|
},
|
|
1269
|
-
|
|
1286
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1287
|
+
devDependencies: { rulesync: "latest" },
|
|
1270
1288
|
});
|
|
1271
1289
|
const dest = tmp("pj-raw-src-");
|
|
1272
1290
|
mergePackageJson(dest, pkgRoot);
|
|
@@ -1282,7 +1300,8 @@ describe("mergePackageJson", () => {
|
|
|
1282
1300
|
const pkgRoot = stubPkgRoot({
|
|
1283
1301
|
name: "fixture-pkg",
|
|
1284
1302
|
scripts: { "rulesync:generate": "node ./scripts/guarded-roots.mjs generate" },
|
|
1285
|
-
|
|
1303
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1304
|
+
devDependencies: { rulesync: "latest" },
|
|
1286
1305
|
});
|
|
1287
1306
|
const dest = tmp("pj-raw-dest-");
|
|
1288
1307
|
fs.writeFileSync(
|
|
@@ -1301,7 +1320,8 @@ describe("mergePackageJson", () => {
|
|
|
1301
1320
|
const pkgRoot = stubPkgRoot({
|
|
1302
1321
|
name: "fixture-pkg",
|
|
1303
1322
|
scripts: { "rulesync:generate": "rulesync generate -t bar", "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply" },
|
|
1304
|
-
|
|
1323
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1324
|
+
devDependencies: { rulesync: "latest" },
|
|
1305
1325
|
});
|
|
1306
1326
|
const dest = tmp("pj-owned-");
|
|
1307
1327
|
fs.writeFileSync(
|
|
@@ -1329,7 +1349,8 @@ describe("mergePackageJson", () => {
|
|
|
1329
1349
|
it("src without scripts uses empty object", () => {
|
|
1330
1350
|
const pkgRoot = stubPkgRoot({
|
|
1331
1351
|
name: "no-scripts",
|
|
1332
|
-
|
|
1352
|
+
dependencies: { "smol-toml": "1" },
|
|
1353
|
+
devDependencies: { rulesync: "2" },
|
|
1333
1354
|
});
|
|
1334
1355
|
const dest = tmp("pj-noscripts-");
|
|
1335
1356
|
mergePackageJson(dest, pkgRoot);
|
|
@@ -1362,7 +1383,8 @@ describe("mergePackageJson", () => {
|
|
|
1362
1383
|
"purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
|
|
1363
1384
|
test: "vitest run --coverage",
|
|
1364
1385
|
},
|
|
1365
|
-
|
|
1386
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1387
|
+
devDependencies: { rulesync: "latest" },
|
|
1366
1388
|
});
|
|
1367
1389
|
const dest = tmp("pj-autorun-");
|
|
1368
1390
|
fs.writeFileSync(
|
|
@@ -1429,7 +1451,8 @@ describe("mergePackageJson", () => {
|
|
|
1429
1451
|
const pkgRoot = stubPkgRoot({
|
|
1430
1452
|
name: "fixture-pkg",
|
|
1431
1453
|
scripts: { doctor: "node ./scripts/doctor.mjs" },
|
|
1432
|
-
|
|
1454
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1455
|
+
devDependencies: { rulesync: "latest" },
|
|
1433
1456
|
});
|
|
1434
1457
|
const dest = tmp("pj-custom-doctor-");
|
|
1435
1458
|
fs.writeFileSync(
|
|
@@ -1449,7 +1472,8 @@ describe("mergePackageJson", () => {
|
|
|
1449
1472
|
const pkgRoot = stubPkgRoot({
|
|
1450
1473
|
name: "fixture-pkg",
|
|
1451
1474
|
scripts: { "rulesync:generate": "rulesync generate -t bar" },
|
|
1452
|
-
|
|
1475
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1476
|
+
devDependencies: { rulesync: "latest" },
|
|
1453
1477
|
});
|
|
1454
1478
|
const dest = tmp("pj-both-");
|
|
1455
1479
|
fs.writeFileSync(
|
|
@@ -1473,7 +1497,8 @@ describe("mergePackageJson", () => {
|
|
|
1473
1497
|
const pkgRoot = stubPkgRoot({
|
|
1474
1498
|
name: "fixture-pkg",
|
|
1475
1499
|
scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
|
|
1476
|
-
|
|
1500
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1501
|
+
devDependencies: { rulesync: "latest" },
|
|
1477
1502
|
});
|
|
1478
1503
|
const dest = tmp("pj-raw-rewrite-");
|
|
1479
1504
|
fs.writeFileSync(
|
|
@@ -1498,7 +1523,8 @@ describe("mergePackageJson", () => {
|
|
|
1498
1523
|
const pkgRoot = stubPkgRoot({
|
|
1499
1524
|
name: "fixture-pkg",
|
|
1500
1525
|
scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
|
|
1501
|
-
|
|
1526
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1527
|
+
devDependencies: { rulesync: "latest" },
|
|
1502
1528
|
});
|
|
1503
1529
|
const dest = tmp("pj-pm-rewrite-");
|
|
1504
1530
|
fs.writeFileSync(
|
|
@@ -1522,7 +1548,8 @@ describe("mergePackageJson", () => {
|
|
|
1522
1548
|
name: GRUNT_PACKAGE,
|
|
1523
1549
|
version: "0.6.2",
|
|
1524
1550
|
scripts: { "rulesync:generate": "gen" },
|
|
1525
|
-
|
|
1551
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1552
|
+
devDependencies: { rulesync: "latest" },
|
|
1526
1553
|
});
|
|
1527
1554
|
const dest = tmp("pj-grunt-dep-");
|
|
1528
1555
|
fs.writeFileSync(
|
|
@@ -1544,7 +1571,8 @@ describe("mergePackageJson", () => {
|
|
|
1544
1571
|
name: GRUNT_PACKAGE,
|
|
1545
1572
|
version: "0.7.0",
|
|
1546
1573
|
scripts: {},
|
|
1547
|
-
|
|
1574
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1575
|
+
devDependencies: { rulesync: "latest" },
|
|
1548
1576
|
});
|
|
1549
1577
|
const dest = tmp("pj-grunt-onlydep-");
|
|
1550
1578
|
fs.writeFileSync(
|
|
@@ -1562,7 +1590,8 @@ describe("mergePackageJson", () => {
|
|
|
1562
1590
|
name: GRUNT_PACKAGE,
|
|
1563
1591
|
version: "0.7.0",
|
|
1564
1592
|
scripts: {},
|
|
1565
|
-
|
|
1593
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1594
|
+
devDependencies: { rulesync: "latest" },
|
|
1566
1595
|
});
|
|
1567
1596
|
const dest = tmp("pj-grunt-add-");
|
|
1568
1597
|
fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ name: "app" }));
|
|
@@ -1576,7 +1605,8 @@ describe("mergePackageJson", () => {
|
|
|
1576
1605
|
name: GRUNT_PACKAGE,
|
|
1577
1606
|
version: "0.7.0",
|
|
1578
1607
|
scripts: {},
|
|
1579
|
-
|
|
1608
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1609
|
+
devDependencies: { rulesync: "latest" },
|
|
1580
1610
|
});
|
|
1581
1611
|
const dest = tmp("pj-grunt-keep-");
|
|
1582
1612
|
fs.writeFileSync(
|
|
@@ -1605,7 +1635,8 @@ describe("mergePackageJson", () => {
|
|
|
1605
1635
|
const pkgRoot = stubPkgRoot({
|
|
1606
1636
|
name: "fixture-pkg",
|
|
1607
1637
|
scripts: { "rulesync:check": "node ./scripts/guarded-roots.mjs check" },
|
|
1608
|
-
|
|
1638
|
+
dependencies: { "smol-toml": "^1.8.0" },
|
|
1639
|
+
devDependencies: { rulesync: "latest" },
|
|
1609
1640
|
});
|
|
1610
1641
|
const dest = tmp("pj-nonstr-");
|
|
1611
1642
|
fs.writeFileSync(
|
|
@@ -1692,6 +1723,158 @@ describe("mergePackageJson", () => {
|
|
|
1692
1723
|
expect(pkg.scripts[k]).toBe(v);
|
|
1693
1724
|
}
|
|
1694
1725
|
});
|
|
1726
|
+
|
|
1727
|
+
it("keeps customized grunt launchers that are not a known default", () => {
|
|
1728
|
+
const pkgRoot = stubPkgRoot();
|
|
1729
|
+
const dest = tmp("pj-keep-custom-launch-");
|
|
1730
|
+
const wrapped = {
|
|
1731
|
+
"grunt:antigravity": "node scripts/with-env.mjs agy --dangerously-skip-permissions",
|
|
1732
|
+
"grunt:claude": "node scripts/with-env.mjs claude --dangerously-skip-permissions",
|
|
1733
|
+
"grunt:codex": "node scripts/with-env.mjs codex --dangerously-bypass-approvals-and-sandbox",
|
|
1734
|
+
"grunt:gemini": "node scripts/with-env.mjs gemini --yolo",
|
|
1735
|
+
"grunt:grok": "node scripts/with-env.mjs grok --yolo",
|
|
1736
|
+
};
|
|
1737
|
+
fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ scripts: wrapped }));
|
|
1738
|
+
mergePackageJson(dest, pkgRoot);
|
|
1739
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1740
|
+
expect(out.scripts["grunt:antigravity"]).toBe(wrapped["grunt:antigravity"]);
|
|
1741
|
+
expect(out.scripts["grunt:claude"]).toBe(wrapped["grunt:claude"]);
|
|
1742
|
+
expect(out.scripts["grunt:codex"]).toBe(wrapped["grunt:codex"]);
|
|
1743
|
+
expect(out.scripts["grunt:gemini"]).toBe(wrapped["grunt:gemini"]);
|
|
1744
|
+
expect(out.scripts["grunt:grok"]).toBe(wrapped["grunt:grok"]);
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
it("migrates a historical bare launcher to the current default", () => {
|
|
1748
|
+
const pkgRoot = stubPkgRoot();
|
|
1749
|
+
const dest = tmp("pj-hist-launch-");
|
|
1750
|
+
fs.writeFileSync(
|
|
1751
|
+
path.join(dest, "package.json"),
|
|
1752
|
+
JSON.stringify({ scripts: { "grunt:antigravity": "antigravity" } }),
|
|
1753
|
+
);
|
|
1754
|
+
mergePackageJson(dest, pkgRoot);
|
|
1755
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1756
|
+
expect(out.scripts["grunt:antigravity"]).toBe(LAUNCH_SCRIPTS.antigravity);
|
|
1757
|
+
expect(HISTORICAL_SCRIPT_DEFAULTS.antigravity).toContain("antigravity");
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
it("does not add grunt:test:e2e when dest has no e2e vitest config", () => {
|
|
1761
|
+
const dest = tmp("pj-noe2e-");
|
|
1762
|
+
fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ name: "app", scripts: {} }));
|
|
1763
|
+
mergePackageJson(dest, PKG_ROOT);
|
|
1764
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1765
|
+
expect(out.scripts["grunt:test:e2e"]).toBeUndefined();
|
|
1766
|
+
expect(out.scripts["grunt:rulesync:check"]).toBe("node ./scripts/guarded-roots.mjs check");
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
it("adds grunt:test:e2e when dest has a vitest e2e config", () => {
|
|
1770
|
+
const dest = tmp("pj-e2e-");
|
|
1771
|
+
fs.writeFileSync(path.join(dest, "package.json"), JSON.stringify({ name: "app", scripts: {} }));
|
|
1772
|
+
fs.writeFileSync(path.join(dest, E2E_VITEST_CONFIGS[0]), "export default {}\n");
|
|
1773
|
+
mergePackageJson(dest, PKG_ROOT);
|
|
1774
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1775
|
+
expect(out.scripts["grunt:test:e2e"]).toBe("vitest run -c vitest.e2e.config.ts");
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
it("keeps an existing grunt:test:e2e even without an e2e config", () => {
|
|
1779
|
+
const dest = tmp("pj-e2e-keep-");
|
|
1780
|
+
fs.writeFileSync(
|
|
1781
|
+
path.join(dest, "package.json"),
|
|
1782
|
+
JSON.stringify({ scripts: { "grunt:test:e2e": "node scripts/e2e.mjs" } }),
|
|
1783
|
+
);
|
|
1784
|
+
mergePackageJson(dest, PKG_ROOT);
|
|
1785
|
+
const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
|
|
1786
|
+
expect(out.scripts["grunt:test:e2e"]).toBe("node scripts/e2e.mjs");
|
|
1787
|
+
});
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
describe("reference markdown merge", () => {
|
|
1791
|
+
it("extraH2Sections keeps titles incoming does not have, including CRLF", () => {
|
|
1792
|
+
const existing = "# Law\r\n\r\n## Skills naming\r\n\r\nstock\r\n\r\n## Autorun\r\n\r\ngo-live\r\n";
|
|
1793
|
+
const incoming = "# Law\n\n## Skills naming\n\nstock plus su\n";
|
|
1794
|
+
expect(extraH2Sections(existing, incoming)).toBe("## Autorun\n\ngo-live");
|
|
1795
|
+
expect(extraH2Sections("# Law\n\nno headings\n", incoming)).toBe("");
|
|
1796
|
+
expect(extraH2Sections("## \nempty title\n## Autorun\nkeep\n", incoming)).toBe("## Autorun\nkeep");
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
it("mergeReferenceMarkdown appends extras and normalizes a trailing newline", () => {
|
|
1800
|
+
const incoming = "# Law\n\n## Skills naming\n\nstock";
|
|
1801
|
+
expect(mergeReferenceMarkdown("", incoming)).toBe("# Law\n\n## Skills naming\n\nstock\n");
|
|
1802
|
+
expect(mergeReferenceMarkdown("# Law\n\n## Skills naming\n\nstock\n\n## Autorun\n\nX\n", incoming)).toBe(
|
|
1803
|
+
"# Law\n\n## Skills naming\n\nstock\n\n## Autorun\n\nX\n",
|
|
1804
|
+
);
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
it("snapshotReferenceDocs skips generated maps, non-md, and directories", () => {
|
|
1808
|
+
const dest = tmp("ref-snap-");
|
|
1809
|
+
expect(snapshotReferenceDocs(dest)).toEqual({});
|
|
1810
|
+
const dir = path.join(dest, ".rulesync", "reference");
|
|
1811
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1812
|
+
fs.writeFileSync(path.join(dir, "law.md"), "# Law\n");
|
|
1813
|
+
fs.writeFileSync(path.join(dir, "INDEX.md"), "generated\n");
|
|
1814
|
+
fs.writeFileSync(path.join(dir, "notes.txt"), "nope\n");
|
|
1815
|
+
fs.mkdirSync(path.join(dir, "nested.md"));
|
|
1816
|
+
expect(snapshotReferenceDocs(dest)).toEqual({ "law.md": "# Law\n" });
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
it("remergeReferenceDocs writes extras for packaged refs and leaves consumer-only files", () => {
|
|
1820
|
+
const pkgRoot = stubPkgRoot();
|
|
1821
|
+
const pkgRef = path.join(pkgRoot, ".rulesync", "reference");
|
|
1822
|
+
fs.mkdirSync(pkgRef, { recursive: true });
|
|
1823
|
+
fs.writeFileSync(path.join(pkgRef, "law.md"), "# Law\n\n## Skills naming\n\nstock\n");
|
|
1824
|
+
const dest = tmp("ref-remerge-");
|
|
1825
|
+
const destRef = path.join(dest, ".rulesync", "reference");
|
|
1826
|
+
fs.mkdirSync(destRef, { recursive: true });
|
|
1827
|
+
fs.writeFileSync(
|
|
1828
|
+
path.join(destRef, "law.md"),
|
|
1829
|
+
"# Law\n\n## Skills naming\n\nold\n\n## Autorun\n\nkeep\n",
|
|
1830
|
+
);
|
|
1831
|
+
fs.writeFileSync(path.join(destRef, "stripe.md"), "# Stripe\n");
|
|
1832
|
+
remergeReferenceDocs(
|
|
1833
|
+
dest,
|
|
1834
|
+
{
|
|
1835
|
+
"law.md": fs.readFileSync(path.join(destRef, "law.md"), "utf8"),
|
|
1836
|
+
"stripe.md": "# Stripe\n",
|
|
1837
|
+
},
|
|
1838
|
+
pkgRoot,
|
|
1839
|
+
);
|
|
1840
|
+
expect(fs.readFileSync(path.join(destRef, "law.md"), "utf8")).toBe(
|
|
1841
|
+
"# Law\n\n## Skills naming\n\nstock\n\n## Autorun\n\nkeep\n",
|
|
1842
|
+
);
|
|
1843
|
+
expect(fs.readFileSync(path.join(destRef, "stripe.md"), "utf8")).toBe("# Stripe\n");
|
|
1844
|
+
});
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
describe("pipeline seed", () => {
|
|
1848
|
+
it("writes skip overlay from dest pipeline and does not overwrite an existing file", () => {
|
|
1849
|
+
const dest = tmp("pipe-seed-");
|
|
1850
|
+
const skipped = PIPELINE_COMMANDS.check[1];
|
|
1851
|
+
const src = `export const COMMANDS = {
|
|
1852
|
+
check: [
|
|
1853
|
+
${PIPELINE_COMMANDS.check.filter((c) => c !== skipped).map((c) => JSON.stringify(c)).join(",\n ")}
|
|
1854
|
+
],
|
|
1855
|
+
};
|
|
1856
|
+
`;
|
|
1857
|
+
expect(seedPipelineConfig(dest, src)).toBe(true);
|
|
1858
|
+
const cfg = path.join(dest, PIPELINE_CONFIG_REL);
|
|
1859
|
+
expect(JSON.parse(fs.readFileSync(cfg, "utf8"))).toEqual({ check: { skip: [skipped] } });
|
|
1860
|
+
fs.writeFileSync(cfg, '{"check":{"skip":["keep"]}}\n');
|
|
1861
|
+
expect(seedPipelineConfig(dest, src)).toBe(false);
|
|
1862
|
+
expect(JSON.parse(fs.readFileSync(cfg, "utf8"))).toEqual({ check: { skip: ["keep"] } });
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
it("skips seed when dest is not a COMMANDS module or matches stock", () => {
|
|
1866
|
+
const dest = tmp("pipe-seed-skip-");
|
|
1867
|
+
expect(seedPipelineConfig(dest, "")).toBe(false);
|
|
1868
|
+
expect(seedPipelineConfig(dest, "console.log(1)\n")).toBe(false);
|
|
1869
|
+
const stock = `export const COMMANDS = {
|
|
1870
|
+
generate: [${PIPELINE_COMMANDS.generate.map((c) => JSON.stringify(c)).join(", ")}],
|
|
1871
|
+
check: [${PIPELINE_COMMANDS.check.map((c) => JSON.stringify(c)).join(", ")}],
|
|
1872
|
+
watch: [${PIPELINE_COMMANDS.watch.map((c) => JSON.stringify(c)).join(", ")}],
|
|
1873
|
+
};
|
|
1874
|
+
`;
|
|
1875
|
+
expect(seedPipelineConfig(dest, stock)).toBe(false);
|
|
1876
|
+
expect(fs.existsSync(path.join(dest, PIPELINE_CONFIG_REL))).toBe(false);
|
|
1877
|
+
});
|
|
1695
1878
|
});
|
|
1696
1879
|
|
|
1697
1880
|
describe("init", () => {
|
|
@@ -2198,6 +2381,43 @@ describe("init", () => {
|
|
|
2198
2381
|
).toThrow("emit-maps failed");
|
|
2199
2382
|
});
|
|
2200
2383
|
|
|
2384
|
+
it("keeps extra ## sections in dest law.md and INDEX", () => {
|
|
2385
|
+
const dest = tmp("grunt-law-extra-");
|
|
2386
|
+
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
2387
|
+
const pkgLaw = fs.readFileSync(path.join(PKG_ROOT, ".rulesync", "reference", "law.md"), "utf8");
|
|
2388
|
+
fs.writeFileSync(
|
|
2389
|
+
path.join(dest, ".rulesync", "reference", "law.md"),
|
|
2390
|
+
`${pkgLaw.trimEnd()}\n\n## Autorun\n\npush → go-live/\n`,
|
|
2391
|
+
);
|
|
2392
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
|
|
2393
|
+
const law = fs.readFileSync(path.join(dest, ".rulesync", "reference", "law.md"), "utf8");
|
|
2394
|
+
expect(law).toMatch(/## Skills naming/);
|
|
2395
|
+
expect(law).toMatch(/## Autorun/);
|
|
2396
|
+
expect(law).toMatch(/push → go-live\//);
|
|
2397
|
+
const index = fs.readFileSync(path.join(dest, ".rulesync", "reference", "INDEX.md"), "utf8");
|
|
2398
|
+
expect(index).toMatch(/## Autorun/);
|
|
2399
|
+
expect(index).toMatch(/push → go-live\//);
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
it("seeds grunt.pipeline.jsonc from dest pipeline omissions during init", () => {
|
|
2403
|
+
const dest = tmp("grunt-pipe-init-");
|
|
2404
|
+
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
|
|
2405
|
+
const skipped = PIPELINE_COMMANDS.check.find((c) => c.includes("subagents"));
|
|
2406
|
+
const src = `export const COMMANDS = {
|
|
2407
|
+
check: [
|
|
2408
|
+
${PIPELINE_COMMANDS.check.filter((c) => c !== skipped).map((c) => JSON.stringify(c)).join(",\n ")}
|
|
2409
|
+
],
|
|
2410
|
+
};
|
|
2411
|
+
`;
|
|
2412
|
+
fs.writeFileSync(path.join(dest, "scripts", "pipeline.mjs"), src);
|
|
2413
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
|
|
2414
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(dest, PIPELINE_CONFIG_REL), "utf8"));
|
|
2415
|
+
expect(cfg.check.skip).toEqual([skipped]);
|
|
2416
|
+
expect(fs.readFileSync(path.join(dest, "scripts", "pipeline.mjs"), "utf8")).toContain(
|
|
2417
|
+
"subagents --check",
|
|
2418
|
+
);
|
|
2419
|
+
});
|
|
2420
|
+
|
|
2201
2421
|
it("does not stamp package INDEX over dest; re-emits union", () => {
|
|
2202
2422
|
const dest = tmp("grunt-index-keep-");
|
|
2203
2423
|
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/grunt",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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",
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@clack/prompts": "^0.11.0",
|
|
79
|
-
"cross-spawn": "^7.0.6"
|
|
79
|
+
"cross-spawn": "^7.0.6",
|
|
80
|
+
"smol-toml": "^1.8.0"
|
|
80
81
|
},
|
|
81
82
|
"scripts": {
|
|
82
83
|
"rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
|
|
@@ -104,7 +105,6 @@
|
|
|
104
105
|
"@types/node": "^24.13.3",
|
|
105
106
|
"@vitest/coverage-v8": "^3.2.7",
|
|
106
107
|
"rulesync": "16.17.0",
|
|
107
|
-
"smol-toml": "^1.8.0",
|
|
108
108
|
"tar": "^7.5.22",
|
|
109
109
|
"typescript": "^5.9.3",
|
|
110
110
|
"verdaccio": "^6.10.5",
|
package/scripts/doctor.mjs
CHANGED
|
@@ -444,7 +444,17 @@ export function runDoctor({
|
|
|
444
444
|
return { code: missingRequired.length || missingMaps.length ? 1 : 0, stdout, stderr: "" };
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
|
|
447
|
+
/** Load dest `scripts/lib/env.mjs` `loadEnv` when that file exists next to this script. */
|
|
448
|
+
export async function loadOptionalConsumerEnv(baseHref = import.meta.url) {
|
|
449
|
+
const file = fileURLToPath(new URL("./lib/env.mjs", baseHref));
|
|
450
|
+
if (!fs.existsSync(file)) return false;
|
|
451
|
+
const { loadEnv } = await import(pathToFileURL(file).href);
|
|
452
|
+
if (typeof loadEnv === "function") loadEnv();
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function main() {
|
|
457
|
+
await loadOptionalConsumerEnv();
|
|
448
458
|
const r = runDoctor();
|
|
449
459
|
if (r.stdout) process.stdout.write(r.stdout);
|
|
450
460
|
if (r.stderr) process.stderr.write(r.stderr);
|
|
@@ -454,5 +464,8 @@ function main() {
|
|
|
454
464
|
const thisFile = fileURLToPath(import.meta.url);
|
|
455
465
|
const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
456
466
|
if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
|
|
457
|
-
main()
|
|
467
|
+
main().catch((err) => {
|
|
468
|
+
process.stderr.write((err && err.message ? err.message : String(err)) + "\n");
|
|
469
|
+
process.exit(1);
|
|
470
|
+
});
|
|
458
471
|
}
|
package/scripts/guarded-md.mjs
CHANGED
|
@@ -111,6 +111,34 @@ export function mergeGuardedContent(existingText, gruntBody) {
|
|
|
111
111
|
return composeGuardedMarkdown(gruntBody, user, nl);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** `##` blocks in `existing` whose titles are not in `incoming`. */
|
|
115
|
+
export function extraH2Sections(existing, incoming) {
|
|
116
|
+
const incomingTitles = new Set(
|
|
117
|
+
[...String(incoming ?? "").matchAll(/^## (.+)$/gm)].map((m) => m[1].trim()),
|
|
118
|
+
);
|
|
119
|
+
const text = String(existing ?? "").replace(/\r\n/g, "\n");
|
|
120
|
+
const matches = [...text.matchAll(/^## /gm)];
|
|
121
|
+
if (!matches.length) return "";
|
|
122
|
+
const chunks = [];
|
|
123
|
+
for (let i = 0; i < matches.length; i++) {
|
|
124
|
+
const start = matches[i].index;
|
|
125
|
+
const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
|
|
126
|
+
const block = text.slice(start, end).replace(/(?:\n)+$/, "");
|
|
127
|
+
const title = block.match(/^## (.+)$/m)?.[1].trim();
|
|
128
|
+
if (title && !incomingTitles.has(title)) chunks.push(block);
|
|
129
|
+
}
|
|
130
|
+
return chunks.join("\n\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Grunt body plus consumer-only `##` sections. No sentinels (INDEX inlines law.md). */
|
|
134
|
+
export function mergeReferenceMarkdown(existing, incoming) {
|
|
135
|
+
const src = String(incoming ?? "");
|
|
136
|
+
const extras = extraH2Sections(existing ?? "", src);
|
|
137
|
+
const base = src.replace(/(?:\r?\n)+$/, "");
|
|
138
|
+
if (!extras.trim()) return `${base}\n`;
|
|
139
|
+
return `${base}\n\n${extras.replace(/(?:\n)+$/, "")}\n`;
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
export function writeMergedGuardedFile(destPath, gruntBody) {
|
|
115
143
|
if (!fs.existsSync(destPath)) {
|
|
116
144
|
fs.writeFileSync(destPath, mergeGuardedContent("", gruntBody));
|
package/scripts/listen.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { whichBin } from "./doctor.mjs";
|
|
8
|
+
import { loadOptionalConsumerEnv, whichBin } from "./doctor.mjs";
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_MODEL = "whisper-1";
|
|
11
11
|
export const DEFAULT_GGML = "ggml-base.en.bin";
|
|
@@ -576,6 +576,7 @@ function waitPidExit(pid, killFn, timeoutMs = 8000) {
|
|
|
576
576
|
|
|
577
577
|
export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
578
578
|
const { _, flags } = parseArgv(argv);
|
|
579
|
+
if (!opts.env) await loadOptionalConsumerEnv();
|
|
579
580
|
const env = opts.env || process.env;
|
|
580
581
|
const home = opts.home || str(env.HOME || env.USERPROFILE, os.homedir());
|
|
581
582
|
const cwd = str(flags.cwd || opts.cwd, process.cwd());
|
package/scripts/pipeline.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** Inner generate/check/watch chain. Called by guarded-roots; not a public npm script. */
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { stripJsonc } from "./jsonc.mjs";
|
|
8
|
+
|
|
9
|
+
export const PIPELINE_CONFIG_REL = ".rulesync/grunt.pipeline.jsonc";
|
|
6
10
|
|
|
7
11
|
export const COMMANDS = {
|
|
8
12
|
generate: [
|
|
@@ -41,12 +45,90 @@ export function envWithLocalBin(cwd, env = process.env) {
|
|
|
41
45
|
return merged;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
function asStringList(v) {
|
|
49
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x) : [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function quotedCommandsInBlock(src, key) {
|
|
53
|
+
const m = String(src ?? "").match(new RegExp(`${key}:\\s*\\[([\\s\\S]*?)\\]`, "m"));
|
|
54
|
+
if (!m) return [];
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
57
|
+
const code = line.replace(/\/\/.*$/, "").trim();
|
|
58
|
+
if (!code) continue;
|
|
59
|
+
for (const x of code.matchAll(/"((?:\\.|[^"\\])*)"/g)) {
|
|
60
|
+
out.push(x[1].replace(/\\"/g, '"'));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function inferPipelineSkip(src, mode) {
|
|
67
|
+
const stock = COMMANDS[mode];
|
|
68
|
+
if (!stock || !src || !new RegExp(`${mode}:\\s*\\[`).test(src)) return [];
|
|
69
|
+
const active = new Set(quotedCommandsInBlock(src, mode));
|
|
70
|
+
return stock.filter((c) => !active.has(c));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function loadPipelineOverlay(cwd, mode, warn = (m) => process.stderr.write(`${m}\n`)) {
|
|
74
|
+
const abs = path.join(cwd, PIPELINE_CONFIG_REL);
|
|
75
|
+
if (!fs.existsSync(abs)) return { skip: [], extra: [] };
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = JSON.parse(stripJsonc(fs.readFileSync(abs, "utf8")));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
warn(`pipeline: ignoring ${PIPELINE_CONFIG_REL}: ${err.message}`);
|
|
81
|
+
return { skip: [], extra: [] };
|
|
82
|
+
}
|
|
83
|
+
const section = raw && typeof raw === "object" && !Array.isArray(raw) ? raw[mode] : null;
|
|
84
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
85
|
+
return { skip: [], extra: [] };
|
|
86
|
+
}
|
|
87
|
+
return { skip: asStringList(section.skip), extra: asStringList(section.extra) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolvePipelineCommands(mode, cwd, warn) {
|
|
91
|
+
const stock = COMMANDS[mode];
|
|
92
|
+
if (!stock) return null;
|
|
93
|
+
const { skip, extra } = loadPipelineOverlay(cwd, mode, warn);
|
|
94
|
+
const skipSet = new Set(skip);
|
|
95
|
+
return [...stock.filter((c) => !skipSet.has(c)), ...extra];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatCheckFailures(failures) {
|
|
99
|
+
const n = failures.length;
|
|
100
|
+
const lines = [`pipeline check failed (${n} step${n === 1 ? "" : "s"}):`];
|
|
101
|
+
for (const { cmd, err } of failures) {
|
|
102
|
+
lines.push(`- ${cmd}`);
|
|
103
|
+
const msg = err && err.message ? err.message : String(err);
|
|
104
|
+
for (const line of msg.split(/\r?\n/)) {
|
|
105
|
+
if (line) lines.push(` ${line}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
lines.push(
|
|
109
|
+
"If a consumer post-step rewrites this output (hooks-union, codex-sync), add the stock command to check.skip in .rulesync/grunt.pipeline.jsonc",
|
|
110
|
+
);
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function runPipeline(mode, { cwd = process.cwd(), exec = execSync, warn } = {}) {
|
|
115
|
+
const cmds = resolvePipelineCommands(mode, cwd, warn);
|
|
46
116
|
if (!cmds) {
|
|
47
117
|
throw new Error("usage: pipeline.mjs generate|check|watch");
|
|
48
118
|
}
|
|
49
119
|
const env = envWithLocalBin(cwd);
|
|
120
|
+
if (mode === "check") {
|
|
121
|
+
const failures = [];
|
|
122
|
+
for (const cmd of cmds) {
|
|
123
|
+
try {
|
|
124
|
+
exec(cmd, { cwd, stdio: "inherit", shell: true, env });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
failures.push({ cmd, err });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (failures.length) throw new Error(formatCheckFailures(failures));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
50
132
|
for (const cmd of cmds) {
|
|
51
133
|
exec(cmd, { cwd, stdio: "inherit", shell: true, env });
|
|
52
134
|
}
|
package/scripts/session-map.mjs
CHANGED
|
@@ -63,7 +63,7 @@ function main() {
|
|
|
63
63
|
JSON.stringify({
|
|
64
64
|
hookSpecificOutput: {
|
|
65
65
|
hookEventName: "SessionStart",
|
|
66
|
-
additionalContext: `${header(/
|
|
66
|
+
additionalContext: `${header(/ {2}…$/m.test(out.text) ? out.depth : null)}\n${out.text}`,
|
|
67
67
|
},
|
|
68
68
|
}),
|
|
69
69
|
);
|
package/scripts/speak.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { loadOptionalConsumerEnv } from "./doctor.mjs";
|
|
8
9
|
|
|
9
10
|
export const DEFAULT_ELEVEN_VOICE = "JBFqnCBsd6RMkjVDRZzb";
|
|
10
11
|
export const DEFAULT_ELEVEN_MODEL = "eleven_multilingual_v2";
|
|
@@ -246,6 +247,7 @@ export async function say({
|
|
|
246
247
|
|
|
247
248
|
export async function main(argv = process.argv.slice(2), opts = {}) {
|
|
248
249
|
const { _, flags } = parseArgv(argv);
|
|
250
|
+
if (!opts.env) await loadOptionalConsumerEnv();
|
|
249
251
|
const env = opts.env || process.env;
|
|
250
252
|
const home = opts.home || os.homedir();
|
|
251
253
|
const cwd = opts.cwd || process.cwd();
|