@lovrozagar/grunt 0.3.11 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/grunt.md +3 -2
- package/.agents/agents/implementer.md +4 -2
- package/.agents/agents/orchestrator.md +19 -12
- package/.agents/agents/thinker.md +7 -2
- package/.agents/skills/browser/SKILL.md +17 -0
- package/.agents/skills/explain/SKILL.md +2 -0
- package/.agents/skills/handoff/SKILL.md +3 -3
- package/.agents/skills/pickup/SKILL.md +64 -0
- package/.agents/skills/solo/SKILL.md +2 -2
- package/.claude/agents/grunt.md +3 -2
- package/.claude/agents/implementer.md +6 -2
- package/.claude/agents/orchestrator.md +19 -12
- package/.claude/agents/thinker.md +9 -2
- package/.claude/skills/browser/SKILL.md +17 -0
- package/.claude/skills/explain/SKILL.md +2 -0
- package/.claude/skills/handoff/SKILL.md +3 -3
- package/.claude/skills/pickup/SKILL.md +64 -0
- package/.claude/skills/solo/SKILL.md +2 -2
- package/.codex/agents/grunt.toml +3 -2
- package/.codex/agents/implementer.toml +4 -2
- package/.codex/agents/orchestrator.toml +19 -12
- package/.codex/agents/thinker.toml +7 -2
- package/.grok/agents/grunt.md +3 -2
- package/.grok/agents/implementer.md +4 -2
- package/.grok/agents/orchestrator.md +19 -12
- package/.grok/agents/thinker.md +7 -2
- package/.grok/hooks/orchestrate-parent.js +26 -10
- package/.grok/skills/browser/SKILL.md +17 -0
- package/.grok/skills/explain/SKILL.md +2 -0
- package/.grok/skills/handoff/SKILL.md +3 -3
- package/.grok/skills/pickup/SKILL.md +64 -0
- package/.grok/skills/solo/SKILL.md +2 -2
- package/.rulesync/reference/browser.md +59 -0
- package/.rulesync/reference/cascade.md +13 -6
- package/.rulesync/reference/hooks.md +2 -2
- package/.rulesync/reference/map.md +6 -3
- package/.rulesync/reference/output.md +38 -1
- package/.rulesync/reference/rtk.md +4 -1
- package/.rulesync/rules/CLAUDE.md +18 -11
- package/.rulesync/rules/overview.md +18 -11
- package/.rulesync/skills/browser/SKILL.md +19 -0
- package/.rulesync/skills/explain/SKILL.md +2 -0
- package/.rulesync/skills/handoff/SKILL.md +3 -3
- package/.rulesync/skills/pickup/SKILL.md +64 -0
- package/.rulesync/skills/solo/SKILL.md +2 -2
- package/.rulesync/subagents/grunt.md +3 -2
- package/.rulesync/subagents/implementer.md +4 -2
- package/.rulesync/subagents/orchestrator.md +19 -12
- package/.rulesync/subagents/thinker.md +7 -2
- package/AGENTS.md +20 -11
- package/CLAUDE.md +20 -11
- package/README.md +40 -10
- package/cli/grunt.mjs +2 -2
- package/cli/grunt.test.ts +3 -3
- package/cli/init.mjs +196 -26
- package/cli/init.test.ts +444 -21
- package/package.json +11 -4
- package/scripts/browser.mjs +764 -0
- package/scripts/doctor.mjs +252 -0
- package/scripts/emit-gemini.mjs +7 -1
- package/scripts/guarded-roots.mjs +83 -0
package/cli/init.test.ts
CHANGED
|
@@ -3,15 +3,27 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import {
|
|
6
|
+
MAX_GUARDED_MARKDOWN_BYTES,
|
|
7
|
+
composeGuardedMarkdown,
|
|
6
8
|
destAlreadyInited,
|
|
9
|
+
extractGruntBody,
|
|
10
|
+
GUARDED_ROOT_FILES,
|
|
11
|
+
guardedMarkdownDrift,
|
|
12
|
+
healGuardedRootFile,
|
|
7
13
|
init,
|
|
8
14
|
mergeClaudeSettings,
|
|
9
15
|
mergeGitignore,
|
|
16
|
+
mergeGuardedContent,
|
|
10
17
|
mergeGuardedMarkdown,
|
|
11
18
|
mergePackageJson,
|
|
19
|
+
remergeGuardedRoots,
|
|
12
20
|
samePath,
|
|
13
21
|
shouldAutoSkipGlobals,
|
|
22
|
+
snapshotGuardedRoots,
|
|
23
|
+
withGuardedCheckInteriors,
|
|
24
|
+
writeMergedGuardedFile,
|
|
14
25
|
} from "./init.mjs";
|
|
26
|
+
import { runGuardedRoots } from "../scripts/guarded-roots.mjs";
|
|
15
27
|
|
|
16
28
|
const tmpDirs: string[] = [];
|
|
17
29
|
afterEach(() => {
|
|
@@ -32,6 +44,7 @@ const PRODUCT_FILES = [
|
|
|
32
44
|
"check-globals.mjs",
|
|
33
45
|
"emit-agent-shell-tools.mjs",
|
|
34
46
|
"emit-gemini.mjs",
|
|
47
|
+
"guarded-roots.mjs",
|
|
35
48
|
"emit-mcp-policy.mjs",
|
|
36
49
|
"gate-fat-tools.mjs",
|
|
37
50
|
"hooks-union.mjs",
|
|
@@ -44,6 +57,8 @@ const PRODUCT_FILES = [
|
|
|
44
57
|
"scrub-text-lib.mjs",
|
|
45
58
|
"sync-global-settings.mjs",
|
|
46
59
|
"telemetry.mjs",
|
|
60
|
+
"browser.mjs",
|
|
61
|
+
"doctor.mjs",
|
|
47
62
|
];
|
|
48
63
|
|
|
49
64
|
const SRC_CLAUDE_SETTINGS = {
|
|
@@ -201,22 +216,39 @@ describe("mergeGuardedMarkdown", () => {
|
|
|
201
216
|
);
|
|
202
217
|
});
|
|
203
218
|
|
|
204
|
-
it("dest exists without sentinel:
|
|
219
|
+
it("dest exists without sentinel: prepends wrapped grunt, keeps full old body", () => {
|
|
205
220
|
const pkgRoot = stubPkgRoot();
|
|
206
221
|
const dest = tmp("md-nosentinel-");
|
|
207
222
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
208
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
209
223
|
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
210
224
|
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
211
|
-
"
|
|
225
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
212
226
|
);
|
|
213
|
-
expect(fs.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("alreadyInited unmarked still prepends; skip is not safe", () => {
|
|
231
|
+
const pkgRoot = stubPkgRoot();
|
|
232
|
+
const dest = tmp("md-inited-nosent-");
|
|
233
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
234
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md", { alreadyInited: true });
|
|
235
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
236
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
237
|
+
);
|
|
238
|
+
expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
|
|
217
239
|
});
|
|
218
240
|
|
|
219
|
-
it("dest
|
|
241
|
+
it("empty dest file is treated as missing", () => {
|
|
242
|
+
const pkgRoot = stubPkgRoot();
|
|
243
|
+
const dest = tmp("md-empty-");
|
|
244
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "");
|
|
245
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
246
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
247
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("dest exists with sentinel: grunt block at top, outside kept at bottom", () => {
|
|
220
252
|
const pkgRoot = stubPkgRoot();
|
|
221
253
|
const dest = tmp("md-sentinel-");
|
|
222
254
|
fs.writeFileSync(
|
|
@@ -225,10 +257,368 @@ describe("mergeGuardedMarkdown", () => {
|
|
|
225
257
|
);
|
|
226
258
|
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
227
259
|
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
228
|
-
"
|
|
260
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nbefore\nafter\n",
|
|
229
261
|
);
|
|
230
262
|
expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
|
|
231
263
|
});
|
|
264
|
+
|
|
265
|
+
it("collapses duplicate sentinel pairs to one top block", () => {
|
|
266
|
+
const pkgRoot = stubPkgRoot();
|
|
267
|
+
const dest = tmp("md-dup-sent-");
|
|
268
|
+
fs.writeFileSync(
|
|
269
|
+
path.join(dest, "CLAUDE.md"),
|
|
270
|
+
"<!-- grunt:begin -->\nold-a\n<!-- grunt:end -->\nkeep-a\n<!-- grunt:begin -->\nold-b\n<!-- grunt:end -->\nkeep-b\n",
|
|
271
|
+
);
|
|
272
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
273
|
+
const out = fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8");
|
|
274
|
+
expect(out).toBe(
|
|
275
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nkeep-a\nkeep-b\n",
|
|
276
|
+
);
|
|
277
|
+
expect(out.split("<!-- grunt:begin -->")).toHaveLength(2);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("second merge is idempotent and does not double-prepend", () => {
|
|
281
|
+
const pkgRoot = stubPkgRoot();
|
|
282
|
+
const dest = tmp("md-idem-");
|
|
283
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
284
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
285
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
286
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
287
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("does not fuzzy-dedupe unmarked protocol-looking user text", () => {
|
|
292
|
+
const pkgRoot = stubPkgRoot();
|
|
293
|
+
const dest = tmp("md-proto-");
|
|
294
|
+
const user = "Voice: `.rulesync/reference/output.md` — cite once.\nspawn grunt\n";
|
|
295
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), user);
|
|
296
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
297
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
298
|
+
`<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n${user}`,
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("preserves CRLF from dest", () => {
|
|
303
|
+
const pkgRoot = stubPkgRoot();
|
|
304
|
+
const dest = tmp("md-crlf-");
|
|
305
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "user line\r\n");
|
|
306
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
307
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
308
|
+
"<!-- grunt:begin -->\r\nCLAUDE.md content\r\n<!-- grunt:end -->\r\nuser line\r\n",
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("unmarked dest equal to grunt body wraps without duplicating", () => {
|
|
313
|
+
const pkgRoot = stubPkgRoot();
|
|
314
|
+
const dest = tmp("md-equal-");
|
|
315
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "CLAUDE.md content\n");
|
|
316
|
+
mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md");
|
|
317
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
318
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\n",
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("binary dest is left untouched", () => {
|
|
323
|
+
const pkgRoot = stubPkgRoot();
|
|
324
|
+
const dest = tmp("md-bin-");
|
|
325
|
+
const raw = Buffer.from([0x68, 0x69, 0x00, 0xff]);
|
|
326
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), raw);
|
|
327
|
+
expect(mergeGuardedMarkdown(dest, pkgRoot, "CLAUDE.md")).toBe("aborted-unsafe");
|
|
328
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"))).toEqual(raw);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("leaves nested AGENTS.md alone", () => {
|
|
332
|
+
const pkgRoot = stubPkgRoot();
|
|
333
|
+
const dest = tmp("md-nested-");
|
|
334
|
+
fs.mkdirSync(path.join(dest, "pkg"), { recursive: true });
|
|
335
|
+
fs.writeFileSync(path.join(dest, "pkg", "AGENTS.md"), "nested user\n");
|
|
336
|
+
mergeGuardedMarkdown(dest, pkgRoot, "AGENTS.md");
|
|
337
|
+
expect(fs.readFileSync(path.join(dest, "pkg", "AGENTS.md"), "utf8")).toBe("nested user\n");
|
|
338
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
339
|
+
"<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\n",
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("snapshot/remerge guarded roots", () => {
|
|
345
|
+
it("remerge after clobber keeps user bottom and refreshes grunt interior", () => {
|
|
346
|
+
const dest = tmp("snap-clobber-");
|
|
347
|
+
fs.writeFileSync(
|
|
348
|
+
path.join(dest, "AGENTS.md"),
|
|
349
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nconsumer agents\n",
|
|
350
|
+
);
|
|
351
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), "user gemini\n");
|
|
352
|
+
const snap = snapshotGuardedRoots(dest);
|
|
353
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
|
|
354
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
|
|
355
|
+
remergeGuardedRoots(dest, snap);
|
|
356
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
357
|
+
"<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\nconsumer agents\n",
|
|
358
|
+
);
|
|
359
|
+
expect(fs.readFileSync(path.join(dest, "GEMINI.md"), "utf8")).toBe(
|
|
360
|
+
"<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\nuser gemini\n",
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("withGuardedCheckInteriors is restored even if check throws", () => {
|
|
365
|
+
const dest = tmp("check-int-");
|
|
366
|
+
const live =
|
|
367
|
+
"<!-- grunt:begin -->\ngrunt body\n<!-- grunt:end -->\nuser bottom\n";
|
|
368
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), live);
|
|
369
|
+
expect(() =>
|
|
370
|
+
withGuardedCheckInteriors(dest, () => {
|
|
371
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe("grunt body\n");
|
|
372
|
+
throw new Error("check failed");
|
|
373
|
+
}),
|
|
374
|
+
).toThrow("check failed");
|
|
375
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(live);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("compose/extract/drift/write helpers cover remaining branches", () => {
|
|
379
|
+
expect(composeGuardedMarkdown("body", null)).toBe(
|
|
380
|
+
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
|
|
381
|
+
);
|
|
382
|
+
expect(composeGuardedMarkdown("body", "\n\n")).toBe(
|
|
383
|
+
"<!-- grunt:begin -->\nbody\n<!-- grunt:end -->\n",
|
|
384
|
+
);
|
|
385
|
+
expect(extractGruntBody("no markers")).toBeNull();
|
|
386
|
+
expect(
|
|
387
|
+
extractGruntBody(
|
|
388
|
+
"<!-- grunt:begin -->\na\n<!-- grunt:end -->\n<!-- grunt:begin -->\nb\n<!-- grunt:end -->\n",
|
|
389
|
+
),
|
|
390
|
+
).toBe("a\nb");
|
|
391
|
+
expect(mergeGuardedContent(null, "g")).toBe(
|
|
392
|
+
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
|
|
393
|
+
);
|
|
394
|
+
expect(mergeGuardedContent("", "a\r\nb")).toBe(
|
|
395
|
+
"<!-- grunt:begin -->\r\na\r\nb\r\n<!-- grunt:end -->\r\n",
|
|
396
|
+
);
|
|
397
|
+
expect(composeGuardedMarkdown("b", "\r\n")).toBe(
|
|
398
|
+
"<!-- grunt:begin -->\nb\n<!-- grunt:end -->\n",
|
|
399
|
+
);
|
|
400
|
+
expect(mergeGuardedContent("keep\n", "g\r\n")).toBe(
|
|
401
|
+
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\nkeep\n",
|
|
402
|
+
);
|
|
403
|
+
expect(mergeGuardedContent("<!-- grunt:begin --> only", "g")).toContain("only");
|
|
404
|
+
const dest = tmp("md-helpers-");
|
|
405
|
+
const missing = path.join(dest, "nope.md");
|
|
406
|
+
expect(guardedMarkdownDrift(missing, "g")).toBe(true);
|
|
407
|
+
writeMergedGuardedFile(missing, "g");
|
|
408
|
+
expect(fs.readFileSync(missing, "utf8")).toBe(
|
|
409
|
+
"<!-- grunt:begin -->\ng\n<!-- grunt:end -->\n",
|
|
410
|
+
);
|
|
411
|
+
expect(guardedMarkdownDrift(missing, "g")).toBe(false);
|
|
412
|
+
expect(guardedMarkdownDrift(missing, "other")).toBe(true);
|
|
413
|
+
fs.writeFileSync(missing, "plain\n");
|
|
414
|
+
expect(guardedMarkdownDrift(missing, "plain\n")).toBe(false);
|
|
415
|
+
expect(guardedMarkdownDrift(missing, "x")).toBe(true);
|
|
416
|
+
const bin = path.join(dest, "bin.md");
|
|
417
|
+
fs.writeFileSync(bin, Buffer.from([0]));
|
|
418
|
+
expect(guardedMarkdownDrift(bin, "g")).toBe(true);
|
|
419
|
+
const huge = path.join(dest, "huge.md");
|
|
420
|
+
fs.writeFileSync(huge, Buffer.alloc(MAX_GUARDED_MARKDOWN_BYTES + 1, 65));
|
|
421
|
+
expect(writeMergedGuardedFile(huge, "g")).toBe("aborted-unsafe");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("remerge restores unsafe, missing, binary current, and created files", () => {
|
|
425
|
+
const dest = tmp("remerge-edges-");
|
|
426
|
+
const agents = path.join(dest, "AGENTS.md");
|
|
427
|
+
const claude = path.join(dest, "CLAUDE.md");
|
|
428
|
+
const gemini = path.join(dest, "GEMINI.md");
|
|
429
|
+
const rawBuf = Buffer.from([0x41, 0x00]);
|
|
430
|
+
fs.writeFileSync(agents, rawBuf);
|
|
431
|
+
fs.writeFileSync(claude, "keep claude\n");
|
|
432
|
+
const snap = snapshotGuardedRoots(dest);
|
|
433
|
+
expect(snap["AGENTS.md"].unsafe).toBe("binary");
|
|
434
|
+
fs.writeFileSync(agents, "clobbered\n");
|
|
435
|
+
fs.rmSync(claude);
|
|
436
|
+
fs.writeFileSync(gemini, "@AGENTS.md\n");
|
|
437
|
+
const binClaude = path.join(dest, "CLAUDE.md");
|
|
438
|
+
remergeGuardedRoots(dest, snap);
|
|
439
|
+
expect(fs.readFileSync(agents)).toEqual(rawBuf);
|
|
440
|
+
expect(fs.readFileSync(claude, "utf8")).toBe("keep claude\n");
|
|
441
|
+
expect(fs.readFileSync(gemini, "utf8")).toBe(
|
|
442
|
+
"<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\n",
|
|
443
|
+
);
|
|
444
|
+
fs.writeFileSync(claude, Buffer.from([0x00]));
|
|
445
|
+
remergeGuardedRoots(dest, snap);
|
|
446
|
+
expect(fs.readFileSync(binClaude, "utf8")).toBe("keep claude\n");
|
|
447
|
+
remergeGuardedRoots(dest, {});
|
|
448
|
+
fs.writeFileSync(gemini, Buffer.from([0x00]));
|
|
449
|
+
remergeGuardedRoots(dest, {});
|
|
450
|
+
expect(fs.readFileSync(gemini)).toEqual(Buffer.from([0x00]));
|
|
451
|
+
fs.rmSync(gemini);
|
|
452
|
+
remergeGuardedRoots(dest, {});
|
|
453
|
+
expect(fs.existsSync(gemini)).toBe(false);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("remerge clears user when unmarked snap equals new grunt body", () => {
|
|
457
|
+
const dest = tmp("remerge-eq-");
|
|
458
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "same\n");
|
|
459
|
+
const snap = snapshotGuardedRoots(dest);
|
|
460
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "same\n");
|
|
461
|
+
remergeGuardedRoots(dest, snap);
|
|
462
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe("same\n");
|
|
463
|
+
fs.writeFileSync(
|
|
464
|
+
path.join(dest, "AGENTS.md"),
|
|
465
|
+
"<!-- grunt:begin -->\nsame\n<!-- grunt:end -->\n",
|
|
466
|
+
);
|
|
467
|
+
remergeGuardedRoots(dest, snap);
|
|
468
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
469
|
+
"<!-- grunt:begin -->\nsame\n<!-- grunt:end -->\n",
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("withGuardedCheckInteriors skips unmarked and unsafe then restores", () => {
|
|
474
|
+
const dest = tmp("check-skip-");
|
|
475
|
+
const live = "plain user\n";
|
|
476
|
+
const rawBuf = Buffer.from([0x00, 0x01]);
|
|
477
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), live);
|
|
478
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), rawBuf);
|
|
479
|
+
const ret = withGuardedCheckInteriors(dest, () => {
|
|
480
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(live);
|
|
481
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"))).toEqual(rawBuf);
|
|
482
|
+
return 7;
|
|
483
|
+
});
|
|
484
|
+
expect(ret).toBe(7);
|
|
485
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(live);
|
|
486
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"))).toEqual(rawBuf);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("runGuardedRoots generate rematches after inner clobber", () => {
|
|
490
|
+
const dest = tmp("guarded-run-");
|
|
491
|
+
fs.writeFileSync(
|
|
492
|
+
path.join(dest, "AGENTS.md"),
|
|
493
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nkeep me\n",
|
|
494
|
+
);
|
|
495
|
+
const exec = vi.fn(() => {
|
|
496
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "fresh ssot\n");
|
|
497
|
+
});
|
|
498
|
+
runGuardedRoots("generate", { cwd: dest, exec });
|
|
499
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
500
|
+
expect(String(exec.mock.calls[0][0])).toContain("rulesync:generate:raw");
|
|
501
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
502
|
+
"<!-- grunt:begin -->\nfresh ssot\n<!-- grunt:end -->\nkeep me\n",
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("healGuardedRootFile rematches unmarked clobber and keeps later user edits", () => {
|
|
507
|
+
const dest = tmp("heal-watch-");
|
|
508
|
+
fs.writeFileSync(
|
|
509
|
+
path.join(dest, "AGENTS.md"),
|
|
510
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nkeep me\n",
|
|
511
|
+
);
|
|
512
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "user claude\n");
|
|
513
|
+
const snap = snapshotGuardedRoots(dest);
|
|
514
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch gen 1\n");
|
|
515
|
+
healGuardedRootFile(dest, "AGENTS.md", snap);
|
|
516
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
517
|
+
"<!-- grunt:begin -->\nwatch gen 1\n<!-- grunt:end -->\nkeep me\n",
|
|
518
|
+
);
|
|
519
|
+
fs.writeFileSync(
|
|
520
|
+
path.join(dest, "AGENTS.md"),
|
|
521
|
+
"<!-- grunt:begin -->\nwatch gen 1\n<!-- grunt:end -->\nedited bottom\n",
|
|
522
|
+
);
|
|
523
|
+
healGuardedRootFile(dest, "AGENTS.md", snap);
|
|
524
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch gen 2\n");
|
|
525
|
+
healGuardedRootFile(dest, "AGENTS.md", snap);
|
|
526
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
527
|
+
"<!-- grunt:begin -->\nwatch gen 2\n<!-- grunt:end -->\nedited bottom\n",
|
|
528
|
+
);
|
|
529
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "watch claude\n");
|
|
530
|
+
healGuardedRootFile(dest, "CLAUDE.md", snap);
|
|
531
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
532
|
+
"<!-- grunt:begin -->\nwatch claude\n<!-- grunt:end -->\nuser claude\n",
|
|
533
|
+
);
|
|
534
|
+
healGuardedRootFile(dest, "README.md", snap);
|
|
535
|
+
expect(GUARDED_ROOT_FILES).toEqual(["AGENTS.md", "CLAUDE.md", "GEMINI.md"]);
|
|
536
|
+
const eq = tmp("heal-eq-");
|
|
537
|
+
fs.writeFileSync(path.join(eq, "AGENTS.md"), "same\n");
|
|
538
|
+
const eqSnap = snapshotGuardedRoots(eq);
|
|
539
|
+
healGuardedRootFile(eq, "AGENTS.md", eqSnap);
|
|
540
|
+
expect(fs.readFileSync(path.join(eq, "AGENTS.md"), "utf8")).toBe("same\n");
|
|
541
|
+
fs.writeFileSync(path.join(eq, "AGENTS.md"), "same");
|
|
542
|
+
healGuardedRootFile(eq, "AGENTS.md", eqSnap);
|
|
543
|
+
expect(fs.readFileSync(path.join(eq, "AGENTS.md"), "utf8")).toBe(
|
|
544
|
+
"<!-- grunt:begin -->\nsame\n<!-- grunt:end -->\n",
|
|
545
|
+
);
|
|
546
|
+
fs.writeFileSync(path.join(eq, "GEMINI.md"), "@AGENTS.md\n");
|
|
547
|
+
healGuardedRootFile(eq, "GEMINI.md", {});
|
|
548
|
+
expect(fs.readFileSync(path.join(eq, "GEMINI.md"), "utf8")).toBe(
|
|
549
|
+
"<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\n",
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("healGuardedRootFile restores unsafe clobber and ignores missing", () => {
|
|
554
|
+
const dest = tmp("heal-unsafe-");
|
|
555
|
+
const rawBuf = Buffer.from([0x00, 0x02]);
|
|
556
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), rawBuf);
|
|
557
|
+
const snap = snapshotGuardedRoots(dest);
|
|
558
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "clobbered\n");
|
|
559
|
+
healGuardedRootFile(dest, "AGENTS.md", snap);
|
|
560
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"))).toEqual(rawBuf);
|
|
561
|
+
healGuardedRootFile(dest, "GEMINI.md", snap);
|
|
562
|
+
expect(fs.existsSync(path.join(dest, "GEMINI.md"))).toBe(false);
|
|
563
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "ok\n");
|
|
564
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), Buffer.from([0x00]));
|
|
565
|
+
const snap2 = snapshotGuardedRoots(dest);
|
|
566
|
+
snap2["CLAUDE.md"] = { raw: "ok\n", user: "ok\n" };
|
|
567
|
+
healGuardedRootFile(dest, "CLAUDE.md", snap2);
|
|
568
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe("ok\n");
|
|
569
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), Buffer.from([0x00]));
|
|
570
|
+
healGuardedRootFile(dest, "GEMINI.md", {});
|
|
571
|
+
expect(fs.readFileSync(path.join(dest, "GEMINI.md"))).toEqual(Buffer.from([0x00]));
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("runGuardedRoots watch rematches after each inner clobber", () => {
|
|
575
|
+
const dest = tmp("guarded-watch-");
|
|
576
|
+
fs.writeFileSync(
|
|
577
|
+
path.join(dest, "AGENTS.md"),
|
|
578
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nkeep me\n",
|
|
579
|
+
);
|
|
580
|
+
let heal;
|
|
581
|
+
const exec = vi.fn(() => {
|
|
582
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch1\n");
|
|
583
|
+
heal("AGENTS.md");
|
|
584
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
585
|
+
"<!-- grunt:begin -->\nwatch1\n<!-- grunt:end -->\nkeep me\n",
|
|
586
|
+
);
|
|
587
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "watch2\n");
|
|
588
|
+
heal("AGENTS.md");
|
|
589
|
+
});
|
|
590
|
+
const stop = vi.fn();
|
|
591
|
+
runGuardedRoots("watch", {
|
|
592
|
+
cwd: dest,
|
|
593
|
+
exec,
|
|
594
|
+
attachWatchers: ({ heal: h }) => {
|
|
595
|
+
heal = h;
|
|
596
|
+
return stop;
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
expect(exec).toHaveBeenCalledTimes(1);
|
|
600
|
+
expect(String(exec.mock.calls[0][0])).toContain("rulesync:watch:raw");
|
|
601
|
+
expect(stop).toHaveBeenCalledTimes(1);
|
|
602
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
603
|
+
"<!-- grunt:begin -->\nwatch2\n<!-- grunt:end -->\nkeep me\n",
|
|
604
|
+
);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("runGuardedRoots watch default watchers still rematch on exit", () => {
|
|
608
|
+
const dest = tmp("guarded-watch-def-");
|
|
609
|
+
fs.writeFileSync(
|
|
610
|
+
path.join(dest, "AGENTS.md"),
|
|
611
|
+
"<!-- grunt:begin -->\nold\n<!-- grunt:end -->\nkeep me\n",
|
|
612
|
+
);
|
|
613
|
+
const exec = vi.fn(() => {
|
|
614
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "from-watch\n");
|
|
615
|
+
});
|
|
616
|
+
runGuardedRoots("watch", { cwd: dest, exec });
|
|
617
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
618
|
+
"<!-- grunt:begin -->\nfrom-watch\n<!-- grunt:end -->\nkeep me\n",
|
|
619
|
+
);
|
|
620
|
+
expect(() => runGuardedRoots("nope")).toThrow(/generate\|check\|watch/);
|
|
621
|
+
});
|
|
232
622
|
});
|
|
233
623
|
|
|
234
624
|
describe("mergeClaudeSettings", () => {
|
|
@@ -779,7 +1169,7 @@ describe("init", () => {
|
|
|
779
1169
|
]);
|
|
780
1170
|
});
|
|
781
1171
|
|
|
782
|
-
it("re-run
|
|
1172
|
+
it("re-run alreadyInited unmarked still merges live files; stale *.grunt.md unused", () => {
|
|
783
1173
|
const pkgRoot = stubPkgRoot();
|
|
784
1174
|
const dest = tmp("grunt-rerun-nosent-");
|
|
785
1175
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
@@ -788,34 +1178,67 @@ describe("init", () => {
|
|
|
788
1178
|
fs.writeFileSync(path.join(dest, "scripts", "telemetry.mjs"), "existing");
|
|
789
1179
|
fs.writeFileSync(path.join(dest, "CLAUDE.grunt.md"), "stale side");
|
|
790
1180
|
const exec = vi.fn();
|
|
791
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
792
1181
|
init(dest, { pkgRoot, execFileSync: exec });
|
|
793
|
-
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
794
|
-
|
|
1182
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
1183
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
1184
|
+
);
|
|
1185
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
1186
|
+
"<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
1187
|
+
);
|
|
795
1188
|
expect(fs.readFileSync(path.join(dest, "CLAUDE.grunt.md"), "utf8")).toBe("stale side");
|
|
796
1189
|
expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
|
|
797
|
-
expect(logSpy.mock.calls.filter((c) => String(c[0]).includes("lack markers"))).toHaveLength(1);
|
|
798
|
-
logSpy.mockRestore();
|
|
799
1190
|
});
|
|
800
1191
|
|
|
801
|
-
it("first init without sentinel
|
|
1192
|
+
it("first init without sentinel prepends grunt and keeps user bottom", () => {
|
|
802
1193
|
const pkgRoot = stubPkgRoot();
|
|
803
1194
|
const dest = tmp("grunt-first-nosent-");
|
|
804
1195
|
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "hand-written consumer doc\n");
|
|
805
1196
|
fs.writeFileSync(path.join(dest, "AGENTS.md"), "hand-written consumer doc\n");
|
|
806
1197
|
const exec = vi.fn();
|
|
807
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
808
1198
|
init(dest, { pkgRoot, execFileSync: exec });
|
|
809
|
-
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
810
|
-
|
|
811
|
-
|
|
1199
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
1200
|
+
"<!-- grunt:begin -->\nCLAUDE.md content\n<!-- grunt:end -->\nhand-written consumer doc\n",
|
|
1201
|
+
);
|
|
1202
|
+
expect(fs.existsSync(path.join(dest, "CLAUDE.grunt.md"))).toBe(false);
|
|
1203
|
+
expect(fs.existsSync(path.join(dest, "AGENTS.grunt.md"))).toBe(false);
|
|
812
1204
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
813
1205
|
["install"],
|
|
814
1206
|
["run", "rulesync:generate"],
|
|
815
1207
|
["run", "sync:globals:apply"],
|
|
816
1208
|
["run", "rulesync:check"],
|
|
817
1209
|
]);
|
|
818
|
-
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it("generate clobber after merge keeps user bottom (idempotent re-init)", () => {
|
|
1213
|
+
const pkgRoot = stubPkgRoot();
|
|
1214
|
+
const dest = tmp("grunt-gen-clobber-");
|
|
1215
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "consumer agents\n");
|
|
1216
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "consumer claude\n");
|
|
1217
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), "consumer gemini\n");
|
|
1218
|
+
const exec = vi.fn((_cmd, args) => {
|
|
1219
|
+
if (Array.isArray(args) && args.includes("rulesync:generate")) {
|
|
1220
|
+
fs.writeFileSync(path.join(dest, "AGENTS.md"), "generated agents\n");
|
|
1221
|
+
fs.writeFileSync(path.join(dest, "CLAUDE.md"), "generated claude\n");
|
|
1222
|
+
fs.writeFileSync(path.join(dest, "GEMINI.md"), "@AGENTS.md\n");
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
init(dest, { pkgRoot, execFileSync: exec });
|
|
1226
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
1227
|
+
"<!-- grunt:begin -->\ngenerated agents\n<!-- grunt:end -->\nconsumer agents\n",
|
|
1228
|
+
);
|
|
1229
|
+
expect(fs.readFileSync(path.join(dest, "CLAUDE.md"), "utf8")).toBe(
|
|
1230
|
+
"<!-- grunt:begin -->\ngenerated claude\n<!-- grunt:end -->\nconsumer claude\n",
|
|
1231
|
+
);
|
|
1232
|
+
expect(fs.readFileSync(path.join(dest, "GEMINI.md"), "utf8")).toBe(
|
|
1233
|
+
"<!-- grunt:begin -->\n@AGENTS.md\n<!-- grunt:end -->\nconsumer gemini\n",
|
|
1234
|
+
);
|
|
1235
|
+
init(dest, { pkgRoot, execFileSync: vi.fn() });
|
|
1236
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8")).toBe(
|
|
1237
|
+
"<!-- grunt:begin -->\nAGENTS.md content\n<!-- grunt:end -->\nconsumer agents\n",
|
|
1238
|
+
);
|
|
1239
|
+
expect(fs.readFileSync(path.join(dest, "AGENTS.md"), "utf8").split("consumer agents").length).toBe(
|
|
1240
|
+
2,
|
|
1241
|
+
);
|
|
819
1242
|
});
|
|
820
1243
|
|
|
821
1244
|
it("onPhase merge then npm phases stop before exec", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/grunt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"scripts/check-globals.mjs",
|
|
28
28
|
"scripts/emit-agent-shell-tools.mjs",
|
|
29
29
|
"scripts/emit-gemini.mjs",
|
|
30
|
+
"scripts/guarded-roots.mjs",
|
|
30
31
|
"scripts/emit-mcp-policy.mjs",
|
|
31
32
|
"scripts/gate-fat-tools.mjs",
|
|
32
33
|
"scripts/hooks-union.mjs",
|
|
@@ -39,6 +40,8 @@
|
|
|
39
40
|
"scripts/scrub-text-lib.mjs",
|
|
40
41
|
"scripts/sync-global-settings.mjs",
|
|
41
42
|
"scripts/telemetry.mjs",
|
|
43
|
+
"scripts/browser.mjs",
|
|
44
|
+
"scripts/doctor.mjs",
|
|
42
45
|
"scripts/scrub-text",
|
|
43
46
|
".rulesync",
|
|
44
47
|
".grok",
|
|
@@ -59,11 +62,15 @@
|
|
|
59
62
|
"@clack/prompts": "^0.11.0"
|
|
60
63
|
},
|
|
61
64
|
"scripts": {
|
|
62
|
-
"rulesync:generate": "
|
|
63
|
-
"rulesync:
|
|
64
|
-
"rulesync:
|
|
65
|
+
"rulesync:generate": "node ./scripts/guarded-roots.mjs generate",
|
|
66
|
+
"rulesync:generate:raw": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks && node scripts/emit-mcp-policy.mjs && node scripts/emit-gemini.mjs && node scripts/emit-agent-shell-tools.mjs && node scripts/hooks-union.mjs",
|
|
67
|
+
"rulesync:watch": "node ./scripts/guarded-roots.mjs watch",
|
|
68
|
+
"rulesync:watch:raw": "node scripts/emit-mcp-policy.mjs && node scripts/emit-gemini.mjs && node scripts/emit-agent-shell-tools.mjs && rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills --watch",
|
|
69
|
+
"rulesync:check": "node ./scripts/guarded-roots.mjs check",
|
|
70
|
+
"rulesync:check:raw": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,skills --check && rulesync generate -t codexcli,antigravity-cli,grokcli -f subagents --check && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks --check && node scripts/emit-mcp-policy.mjs --check && node scripts/emit-gemini.mjs --check && node scripts/emit-agent-shell-tools.mjs --check && node scripts/check-globals.mjs && node scripts/hooks-union.mjs --check",
|
|
65
71
|
"hooks:union": "node scripts/hooks-union.mjs",
|
|
66
72
|
"hooks:check": "node scripts/hooks-union.mjs --check",
|
|
73
|
+
"doctor": "node ./scripts/doctor.mjs",
|
|
67
74
|
"rulesync:doctor": "rulesync doctor",
|
|
68
75
|
"sync:globals": "node scripts/sync-global-settings.mjs",
|
|
69
76
|
"sync:globals:check": "node scripts/check-globals.mjs",
|