@lovrozagar/grunt 0.1.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.
Files changed (79) hide show
  1. package/.agents/agents/grunt.md +36 -0
  2. package/.agents/agents/implementer.md +40 -0
  3. package/.agents/agents/orchestrator.md +50 -0
  4. package/.agents/agents/thinker.md +27 -0
  5. package/.agents/hooks.json +26 -0
  6. package/.agents/mcp_config.json +3 -0
  7. package/.claude/agents/grunt.md +44 -0
  8. package/.claude/agents/implementer.md +46 -0
  9. package/.claude/agents/orchestrator.md +55 -0
  10. package/.claude/agents/thinker.md +34 -0
  11. package/.claude/settings.json +36 -0
  12. package/.codex/agents/grunt.toml +30 -0
  13. package/.codex/agents/implementer.toml +34 -0
  14. package/.codex/agents/orchestrator.toml +45 -0
  15. package/.codex/agents/thinker.toml +23 -0
  16. package/.codex/config.toml +2 -0
  17. package/.codex/hooks.json +26 -0
  18. package/.grok/agents/grunt.md +31 -0
  19. package/.grok/agents/implementer.md +35 -0
  20. package/.grok/agents/orchestrator.md +47 -0
  21. package/.grok/agents/thinker.md +25 -0
  22. package/.grok/config.toml +6 -0
  23. package/.grok/global-settings.toml +35 -0
  24. package/.grok/hooks/orchestrate-parent.js +476 -0
  25. package/.grok/hooks/orchestrate-parent.json +60 -0
  26. package/.grok/hooks/rtk-grok.js +137 -0
  27. package/.grok/hooks/rtk.json +16 -0
  28. package/.grok/hooks/rulesync.json +3 -0
  29. package/.grok/parent.md +1 -0
  30. package/.grok/roles/grunt.toml +5 -0
  31. package/.grok/roles/implementer.toml +5 -0
  32. package/.grok/roles/thinker.toml +5 -0
  33. package/.grok/skills/explain/SKILL.md +15 -0
  34. package/.grok/skills/implement-plan/SKILL.md +91 -0
  35. package/.grok/skills/parent/SKILL.md +12 -0
  36. package/.grok/skills/shared/plan-format.md +158 -0
  37. package/.grok/skills/write-plan/SKILL.md +84 -0
  38. package/.mcp.json +3 -0
  39. package/.rulesync/global-settings/antigravity.json +1 -0
  40. package/.rulesync/global-settings/claude.json +1 -0
  41. package/.rulesync/global-settings/codex.toml +1 -0
  42. package/.rulesync/global-settings/manifest.json +37 -0
  43. package/.rulesync/hooks.jsonc +22 -0
  44. package/.rulesync/mcp-policy.jsonc +15 -0
  45. package/.rulesync/reference/cascade.md +85 -0
  46. package/.rulesync/reference/hooks.md +19 -0
  47. package/.rulesync/reference/map.md +57 -0
  48. package/.rulesync/reference/rtk.md +23 -0
  49. package/.rulesync/rules/CLAUDE.md +13 -0
  50. package/.rulesync/rules/overview.md +23 -0
  51. package/.rulesync/skills/explain/SKILL.md +15 -0
  52. package/.rulesync/skills/parent/SKILL.md +12 -0
  53. package/.rulesync/skills/terse/SKILL.md +8 -0
  54. package/.rulesync/subagents/grunt.md +51 -0
  55. package/.rulesync/subagents/implementer.md +54 -0
  56. package/.rulesync/subagents/orchestrator.md +69 -0
  57. package/.rulesync/subagents/thinker.md +44 -0
  58. package/AGENTS.md +12 -0
  59. package/CLAUDE.md +7 -0
  60. package/LICENSE +21 -0
  61. package/README.md +56 -0
  62. package/bin/grunt.js +4 -0
  63. package/cli/grunt.mjs +54 -0
  64. package/cli/grunt.test.ts +117 -0
  65. package/cli/init.mjs +113 -0
  66. package/cli/init.test.ts +268 -0
  67. package/package.json +77 -0
  68. package/scripts/check-globals.mjs +98 -0
  69. package/scripts/emit-mcp-policy.mjs +434 -0
  70. package/scripts/gate-fat-tools.mjs +485 -0
  71. package/scripts/grunt-job.mjs +482 -0
  72. package/scripts/parse-need.mjs +75 -0
  73. package/scripts/persist-plan.mjs +309 -0
  74. package/scripts/purge-global-mcps.mjs +291 -0
  75. package/scripts/scrub-spawn-prompt.mjs +219 -0
  76. package/scripts/scrub-text +58 -0
  77. package/scripts/scrub-text-lib.mjs +210 -0
  78. package/scripts/sync-global-settings.mjs +239 -0
  79. package/scripts/telemetry.mjs +38 -0
package/cli/grunt.mjs ADDED
@@ -0,0 +1,54 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { readFileSync } from "node:fs"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import { init } from "./init.mjs"
6
+
7
+ const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
8
+
9
+ const USAGE = `Usage: grunt [command]
10
+
11
+ Default (no command): init — full setup
12
+
13
+ Commands:
14
+ init Full setup: merge SoT, npm install, rulesync:generate, sync:globals:apply, rulesync:check
15
+ generate npm run rulesync:generate
16
+ check npm run rulesync:check
17
+ help Show this help
18
+ version Print package version
19
+ `
20
+
21
+ function pkgVersion() {
22
+ const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"))
23
+ return pkg.version
24
+ }
25
+
26
+ function npmRun(script) {
27
+ execFileSync("npm", ["run", script], { cwd: process.cwd(), stdio: "inherit" })
28
+ }
29
+
30
+ export function start() {
31
+ const [cmd] = process.argv.slice(2)
32
+ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
33
+ process.stdout.write(USAGE)
34
+ return
35
+ }
36
+ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
37
+ process.stdout.write(`${pkgVersion()}\n`)
38
+ return
39
+ }
40
+ if (!cmd || cmd === "init") {
41
+ init(process.cwd())
42
+ return
43
+ }
44
+ if (cmd === "generate") {
45
+ npmRun("rulesync:generate")
46
+ return
47
+ }
48
+ if (cmd === "check") {
49
+ npmRun("rulesync:check")
50
+ return
51
+ }
52
+ process.stdout.write(USAGE)
53
+ process.exitCode = 1
54
+ }
@@ -0,0 +1,117 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const init = vi.hoisted(() => vi.fn());
7
+ const execFileSync = vi.hoisted(() => vi.fn());
8
+
9
+ vi.mock("./init.mjs", () => ({ init }));
10
+ vi.mock("node:child_process", () => ({ execFileSync }));
11
+
12
+ import { start } from "./grunt.mjs";
13
+
14
+ const USAGE = `Usage: grunt [command]
15
+
16
+ Default (no command): init — full setup
17
+
18
+ Commands:
19
+ init Full setup: merge SoT, npm install, rulesync:generate, sync:globals:apply, rulesync:check
20
+ generate npm run rulesync:generate
21
+ check npm run rulesync:check
22
+ help Show this help
23
+ version Print package version
24
+ `;
25
+
26
+ const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
27
+ const version = JSON.parse(
28
+ fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"),
29
+ ).version as string;
30
+
31
+ describe("start", () => {
32
+ let argv: string[];
33
+ let exitCode: typeof process.exitCode;
34
+ let stdoutWrite: typeof process.stdout.write;
35
+ const chunks: string[] = [];
36
+
37
+ beforeEach(() => {
38
+ argv = process.argv.slice();
39
+ exitCode = process.exitCode;
40
+ process.exitCode = 0;
41
+ chunks.length = 0;
42
+ stdoutWrite = process.stdout.write.bind(process.stdout);
43
+ process.stdout.write = ((buf: string | Uint8Array) => {
44
+ chunks.push(String(buf));
45
+ return true;
46
+ }) as typeof process.stdout.write;
47
+ init.mockReset();
48
+ execFileSync.mockReset();
49
+ });
50
+
51
+ afterEach(() => {
52
+ process.argv = argv;
53
+ process.exitCode = exitCode;
54
+ process.stdout.write = stdoutWrite;
55
+ });
56
+
57
+ it.each(["help", "--help", "-h"])("%s writes usage", (cmd) => {
58
+ process.argv = ["node", "grunt", cmd];
59
+ start();
60
+ expect(chunks.join("")).toBe(USAGE);
61
+ expect(process.exitCode).toBe(0);
62
+ expect(init).not.toHaveBeenCalled();
63
+ expect(execFileSync).not.toHaveBeenCalled();
64
+ });
65
+
66
+ it.each(["version", "--version", "-v"])("%s writes package version", (cmd) => {
67
+ process.argv = ["node", "grunt", cmd];
68
+ start();
69
+ expect(chunks.join("")).toBe(`${version}\n`);
70
+ expect(process.exitCode).toBe(0);
71
+ });
72
+
73
+ it("no-arg runs init on cwd", () => {
74
+ process.argv = ["node", "grunt"];
75
+ start();
76
+ expect(init).toHaveBeenCalledOnce();
77
+ expect(init).toHaveBeenCalledWith(process.cwd());
78
+ expect(execFileSync).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it("init runs init on cwd", () => {
82
+ process.argv = ["node", "grunt", "init"];
83
+ start();
84
+ expect(init).toHaveBeenCalledOnce();
85
+ expect(init).toHaveBeenCalledWith(process.cwd());
86
+ });
87
+
88
+ it("generate npm-runs rulesync:generate", () => {
89
+ process.argv = ["node", "grunt", "generate"];
90
+ start();
91
+ expect(execFileSync).toHaveBeenCalledOnce();
92
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:generate"], {
93
+ cwd: process.cwd(),
94
+ stdio: "inherit",
95
+ });
96
+ expect(init).not.toHaveBeenCalled();
97
+ });
98
+
99
+ it("check npm-runs rulesync:check", () => {
100
+ process.argv = ["node", "grunt", "check"];
101
+ start();
102
+ expect(execFileSync).toHaveBeenCalledOnce();
103
+ expect(execFileSync).toHaveBeenCalledWith("npm", ["run", "rulesync:check"], {
104
+ cwd: process.cwd(),
105
+ stdio: "inherit",
106
+ });
107
+ });
108
+
109
+ it("unknown writes usage and exitCode 1", () => {
110
+ process.argv = ["node", "grunt", "nope"];
111
+ start();
112
+ expect(chunks.join("")).toBe(USAGE);
113
+ expect(process.exitCode).toBe(1);
114
+ expect(init).not.toHaveBeenCalled();
115
+ expect(execFileSync).not.toHaveBeenCalled();
116
+ });
117
+ });
package/cli/init.mjs ADDED
@@ -0,0 +1,113 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import fs from "node:fs"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..")
7
+
8
+ const PRODUCT_SCRIPTS = [
9
+ "check-globals.mjs",
10
+ "emit-mcp-policy.mjs",
11
+ "gate-fat-tools.mjs",
12
+ "grunt-job.mjs",
13
+ "parse-need.mjs",
14
+ "persist-plan.mjs",
15
+ "purge-global-mcps.mjs",
16
+ "scrub-spawn-prompt.mjs",
17
+ "scrub-text-lib.mjs",
18
+ "sync-global-settings.mjs",
19
+ "telemetry.mjs",
20
+ "scrub-text",
21
+ ]
22
+
23
+ const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"]
24
+ const COPY_FILES = ["AGENTS.md", "CLAUDE.md", ".mcp.json"]
25
+
26
+ function sortKeys(obj) {
27
+ return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]))
28
+ }
29
+
30
+ export function samePath(a, b) {
31
+ try {
32
+ return fs.realpathSync(a) === fs.realpathSync(b)
33
+ } catch {
34
+ return path.resolve(a) === path.resolve(b)
35
+ }
36
+ }
37
+
38
+ export function mergeGitignore(dest) {
39
+ const gi = path.join(dest, ".gitignore")
40
+ if (!fs.existsSync(gi)) {
41
+ fs.writeFileSync(gi, ".tmp/\n")
42
+ return
43
+ }
44
+ const text = fs.readFileSync(gi, "utf8")
45
+ const has = text.split(/\r?\n/).some((line) => /^\.tmp\/?$/.test(line))
46
+ if (has) return
47
+ const prefix = text.endsWith("\n") || text.length === 0 ? "" : "\n"
48
+ fs.appendFileSync(gi, `${prefix}.tmp/\n`)
49
+ }
50
+
51
+ export function mergePackageJson(dest, pkgRoot) {
52
+ const destPath = path.join(dest, "package.json")
53
+ let destPkg = {}
54
+ if (fs.existsSync(destPath)) {
55
+ destPkg = JSON.parse(fs.readFileSync(destPath, "utf8"))
56
+ }
57
+ if (destPkg.name === "@lovrozagar/grunt") return
58
+
59
+ const srcPkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"))
60
+ destPkg.scripts = { ...(destPkg.scripts || {}) }
61
+ for (const [k, v] of Object.entries(srcPkg.scripts || {})) {
62
+ if (k === "test") continue
63
+ destPkg.scripts[k] = v
64
+ }
65
+ destPkg.devDependencies = { ...(destPkg.devDependencies || {}) }
66
+ destPkg.devDependencies["smol-toml"] = srcPkg.devDependencies["smol-toml"]
67
+ destPkg.devDependencies["rulesync"] = srcPkg.devDependencies["rulesync"]
68
+ destPkg.scripts = sortKeys(destPkg.scripts)
69
+ destPkg.devDependencies = sortKeys(destPkg.devDependencies)
70
+ fs.writeFileSync(destPath, `${JSON.stringify(destPkg, null, 2)}\n`)
71
+ }
72
+
73
+ export function init(dest, { pkgRoot: pkgRootOpt, execFileSync: exec = execFileSync } = {}) {
74
+ dest = path.resolve(dest)
75
+ const pkgRoot = path.resolve(pkgRootOpt ?? PKG_ROOT)
76
+
77
+ for (const dir of COPY_DIRS) {
78
+ const src = path.join(pkgRoot, dir)
79
+ const d = path.join(dest, dir)
80
+ fs.mkdirSync(d, { recursive: true })
81
+ if (!samePath(src, d)) fs.cpSync(src, d, { recursive: true, force: true })
82
+ }
83
+
84
+ for (const file of COPY_FILES) {
85
+ const src = path.join(pkgRoot, file)
86
+ const d = path.join(dest, file)
87
+ if (!samePath(src, d)) fs.copyFileSync(src, d)
88
+ }
89
+
90
+ fs.mkdirSync(path.join(dest, "scripts"), { recursive: true })
91
+ for (const name of PRODUCT_SCRIPTS) {
92
+ const src = path.join(pkgRoot, "scripts", name)
93
+ const d = path.join(dest, "scripts", name)
94
+ if (samePath(src, d)) continue
95
+ const st = fs.statSync(src)
96
+ if (st.isDirectory()) fs.cpSync(src, d, { recursive: true, force: true })
97
+ else fs.copyFileSync(src, d)
98
+ }
99
+
100
+ fs.mkdirSync(path.join(dest, ".tmp"), { recursive: true })
101
+ mergeGitignore(dest)
102
+
103
+ const self = samePath(dest, pkgRoot)
104
+ // Self-skip: file/dir merge + .tmp + gitignore only. No package.json merge,
105
+ // npm install, or generate/sync/check (would mutate this package in-place).
106
+ if (self) return
107
+
108
+ mergePackageJson(dest, pkgRoot)
109
+ exec("npm", ["install"], { cwd: dest, stdio: "inherit" })
110
+ exec("npm", ["run", "rulesync:generate"], { cwd: dest, stdio: "inherit" })
111
+ exec("npm", ["run", "sync:globals:apply"], { cwd: dest, stdio: "inherit" })
112
+ exec("npm", ["run", "rulesync:check"], { cwd: dest, stdio: "inherit" })
113
+ }
@@ -0,0 +1,268 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import {
6
+ init,
7
+ mergeGitignore,
8
+ mergePackageJson,
9
+ samePath,
10
+ } from "./init.mjs";
11
+
12
+ const tmpDirs: string[] = [];
13
+ afterEach(() => {
14
+ for (const d of tmpDirs.splice(0)) {
15
+ fs.rmSync(d, { recursive: true, force: true });
16
+ }
17
+ });
18
+
19
+ function tmp(prefix: string) {
20
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
21
+ tmpDirs.push(dir);
22
+ return dir;
23
+ }
24
+
25
+ const COPY_DIRS = [".rulesync", ".grok", ".codex", ".claude", ".agents"];
26
+ const COPY_FILES = ["AGENTS.md", "CLAUDE.md", ".mcp.json"];
27
+ const PRODUCT_FILES = [
28
+ "check-globals.mjs",
29
+ "emit-mcp-policy.mjs",
30
+ "gate-fat-tools.mjs",
31
+ "grunt-job.mjs",
32
+ "parse-need.mjs",
33
+ "persist-plan.mjs",
34
+ "purge-global-mcps.mjs",
35
+ "scrub-spawn-prompt.mjs",
36
+ "scrub-text-lib.mjs",
37
+ "sync-global-settings.mjs",
38
+ "telemetry.mjs",
39
+ ];
40
+
41
+ function stubPkgRoot(pkg: Record<string, unknown> = {
42
+ name: "fixture-pkg",
43
+ scripts: {
44
+ test: "skip-me",
45
+ zeta: "z",
46
+ alpha: "a",
47
+ "rulesync:generate": "gen",
48
+ },
49
+ devDependencies: {
50
+ "smol-toml": "^1.8.0",
51
+ rulesync: "latest",
52
+ zzz: "1",
53
+ },
54
+ }) {
55
+ const root = tmp("grunt-pkg-");
56
+ for (const d of COPY_DIRS) {
57
+ fs.mkdirSync(path.join(root, d));
58
+ fs.writeFileSync(path.join(root, d, "marker"), d);
59
+ }
60
+ for (const f of COPY_FILES) {
61
+ fs.writeFileSync(path.join(root, f), f);
62
+ }
63
+ fs.mkdirSync(path.join(root, "scripts"));
64
+ for (const name of PRODUCT_FILES) {
65
+ fs.writeFileSync(path.join(root, "scripts", name), name);
66
+ }
67
+ fs.mkdirSync(path.join(root, "scripts", "scrub-text"));
68
+ fs.writeFileSync(path.join(root, "scripts", "scrub-text", "inside"), "dir");
69
+ fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
70
+ return root;
71
+ }
72
+
73
+ describe("samePath", () => {
74
+ it("try: realpath equal vs unequal", () => {
75
+ const a = tmp("same-a-");
76
+ const b = tmp("same-b-");
77
+ expect(samePath(a, a)).toBe(true);
78
+ expect(samePath(a, b)).toBe(false);
79
+ });
80
+
81
+ it("catch: missing paths fall back to resolve", () => {
82
+ const missing = path.join(os.tmpdir(), `grunt-missing-${process.pid}-nope`);
83
+ expect(samePath(missing, missing)).toBe(true);
84
+ expect(samePath(missing, `${missing}-other`)).toBe(false);
85
+ });
86
+ });
87
+
88
+ describe("mergeGitignore", () => {
89
+ it("creates .tmp/ when missing", () => {
90
+ const dest = tmp("gi-miss-");
91
+ mergeGitignore(dest);
92
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp/\n");
93
+ });
94
+
95
+ it("noop when .tmp/ present", () => {
96
+ const dest = tmp("gi-slash-");
97
+ fs.writeFileSync(path.join(dest, ".gitignore"), "foo\n.tmp/\nbar\n");
98
+ mergeGitignore(dest);
99
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe("foo\n.tmp/\nbar\n");
100
+ });
101
+
102
+ it("noop when .tmp present", () => {
103
+ const dest = tmp("gi-bare-");
104
+ fs.writeFileSync(path.join(dest, ".gitignore"), ".tmp\n");
105
+ mergeGitignore(dest);
106
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp\n");
107
+ });
108
+
109
+ it("appends after trailing newline", () => {
110
+ const dest = tmp("gi-nl-");
111
+ fs.writeFileSync(path.join(dest, ".gitignore"), "node_modules/\n");
112
+ mergeGitignore(dest);
113
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
114
+ "node_modules/\n.tmp/\n",
115
+ );
116
+ });
117
+
118
+ it("appends with inserted newline when file lacks one", () => {
119
+ const dest = tmp("gi-nonl-");
120
+ fs.writeFileSync(path.join(dest, ".gitignore"), "node_modules/");
121
+ mergeGitignore(dest);
122
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(
123
+ "node_modules/\n.tmp/\n",
124
+ );
125
+ });
126
+
127
+ it("appends to empty existing file", () => {
128
+ const dest = tmp("gi-empty-");
129
+ fs.writeFileSync(path.join(dest, ".gitignore"), "");
130
+ mergeGitignore(dest);
131
+ expect(fs.readFileSync(path.join(dest, ".gitignore"), "utf8")).toBe(".tmp/\n");
132
+ });
133
+ });
134
+
135
+ describe("mergePackageJson", () => {
136
+ it("creates {} dest and merges scripts skipping test, injects deps, sorts keys", () => {
137
+ const pkgRoot = stubPkgRoot();
138
+ const dest = tmp("pj-new-");
139
+ mergePackageJson(dest, pkgRoot);
140
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
141
+ expect(out.scripts).toEqual({
142
+ alpha: "a",
143
+ "rulesync:generate": "gen",
144
+ zeta: "z",
145
+ });
146
+ expect(Object.keys(out.scripts)).toEqual(["alpha", "rulesync:generate", "zeta"]);
147
+ expect(out.devDependencies).toEqual({
148
+ rulesync: "latest",
149
+ "smol-toml": "^1.8.0",
150
+ });
151
+ expect(Object.keys(out.devDependencies)).toEqual(["rulesync", "smol-toml"]);
152
+ expect(out.scripts.test).toBeUndefined();
153
+ });
154
+
155
+ it("merges over existing dest scripts/deps, keeps dest test", () => {
156
+ const pkgRoot = stubPkgRoot();
157
+ const dest = tmp("pj-exist-");
158
+ fs.writeFileSync(
159
+ path.join(dest, "package.json"),
160
+ JSON.stringify({
161
+ name: "app",
162
+ scripts: { test: "jest", alpha: "old", foo: "bar" },
163
+ devDependencies: { lodash: "4" },
164
+ }),
165
+ );
166
+ mergePackageJson(dest, pkgRoot);
167
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
168
+ expect(out.scripts.test).toBe("jest");
169
+ expect(out.scripts.alpha).toBe("a");
170
+ expect(out.scripts.foo).toBe("bar");
171
+ expect(out.devDependencies.lodash).toBe("4");
172
+ expect(out.devDependencies["smol-toml"]).toBe("^1.8.0");
173
+ expect(out.devDependencies.rulesync).toBe("latest");
174
+ });
175
+
176
+ it("early return when name is @lovrozagar/grunt", () => {
177
+ const pkgRoot = stubPkgRoot();
178
+ const dest = tmp("pj-self-");
179
+ const raw = `${JSON.stringify({ name: "@lovrozagar/grunt", scripts: { test: "x" } }, null, 2)}\n`;
180
+ fs.writeFileSync(path.join(dest, "package.json"), raw);
181
+ mergePackageJson(dest, pkgRoot);
182
+ expect(fs.readFileSync(path.join(dest, "package.json"), "utf8")).toBe(raw);
183
+ });
184
+
185
+ it("src without scripts uses empty object", () => {
186
+ const pkgRoot = stubPkgRoot({
187
+ name: "no-scripts",
188
+ devDependencies: { "smol-toml": "1", rulesync: "2" },
189
+ });
190
+ const dest = tmp("pj-noscripts-");
191
+ mergePackageJson(dest, pkgRoot);
192
+ const out = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
193
+ expect(out.scripts).toEqual({});
194
+ expect(out.devDependencies).toEqual({ rulesync: "2", "smol-toml": "1" });
195
+ });
196
+ });
197
+
198
+ describe("init", () => {
199
+ it("self-skip: copies + gitignore, no exec, no package merge", () => {
200
+ const pkgRoot = stubPkgRoot();
201
+ const exec = vi.fn();
202
+ init(pkgRoot, { pkgRoot, execFileSync: exec });
203
+ expect(exec).not.toHaveBeenCalled();
204
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
205
+ expect(pkg.name).toBe("fixture-pkg");
206
+ expect(fs.existsSync(path.join(pkgRoot, ".tmp"))).toBe(true);
207
+ expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
208
+ expect(fs.readFileSync(path.join(pkgRoot, "scripts", "telemetry.mjs"), "utf8")).toBe(
209
+ "telemetry.mjs",
210
+ );
211
+ });
212
+
213
+ it("non-self: file vs dir product scripts, then npm pipeline in order", () => {
214
+ const pkgRoot = stubPkgRoot();
215
+ const dest = tmp("grunt-dest-");
216
+ const exec = vi.fn();
217
+ init(dest, { pkgRoot, execFileSync: exec });
218
+
219
+ for (const d of COPY_DIRS) {
220
+ expect(fs.readFileSync(path.join(dest, d, "marker"), "utf8")).toBe(d);
221
+ }
222
+ for (const f of COPY_FILES) {
223
+ expect(fs.readFileSync(path.join(dest, f), "utf8")).toBe(f);
224
+ }
225
+ for (const name of PRODUCT_FILES) {
226
+ expect(fs.statSync(path.join(dest, "scripts", name)).isFile()).toBe(true);
227
+ }
228
+ expect(fs.statSync(path.join(dest, "scripts", "scrub-text")).isDirectory()).toBe(true);
229
+ expect(fs.readFileSync(path.join(dest, "scripts", "scrub-text", "inside"), "utf8")).toBe(
230
+ "dir",
231
+ );
232
+
233
+ const destPkg = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf8"));
234
+ expect(destPkg.scripts.test).toBeUndefined();
235
+ expect(destPkg.devDependencies["smol-toml"]).toBe("^1.8.0");
236
+
237
+ expect(exec.mock.calls.map((c) => [c[0], c[1]])).toEqual([
238
+ ["npm", ["install"]],
239
+ ["npm", ["run", "rulesync:generate"]],
240
+ ["npm", ["run", "sync:globals:apply"]],
241
+ ["npm", ["run", "rulesync:check"]],
242
+ ]);
243
+ for (const call of exec.mock.calls) {
244
+ expect(call[2]).toEqual({ cwd: dest, stdio: "inherit" });
245
+ }
246
+ });
247
+
248
+ it("omitted execFileSync uses default; self-skip never invokes it", () => {
249
+ const pkgRoot = stubPkgRoot();
250
+ init(pkgRoot, { pkgRoot });
251
+ expect(fs.existsSync(path.join(pkgRoot, "AGENTS.md"))).toBe(true);
252
+ expect(fs.readFileSync(path.join(pkgRoot, ".gitignore"), "utf8")).toBe(".tmp/\n");
253
+ });
254
+
255
+ it("omitted pkgRoot uses built-in PKG_ROOT; exec mocked", () => {
256
+ const dest = tmp("grunt-realroot-");
257
+ const exec = vi.fn();
258
+ init(dest, { execFileSync: exec });
259
+ expect(fs.existsSync(path.join(dest, "AGENTS.md"))).toBe(true);
260
+ expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
261
+ expect(exec.mock.calls.map((c) => c[1])).toEqual([
262
+ ["install"],
263
+ ["run", "rulesync:generate"],
264
+ ["run", "sync:globals:apply"],
265
+ ["run", "rulesync:check"],
266
+ ]);
267
+ });
268
+ });
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@lovrozagar/grunt",
3
+ "version": "0.1.0",
4
+ "description": "Multi-agent orchestrator SoT — deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
5
+ "keywords": [
6
+ "agents",
7
+ "grunt",
8
+ "orchestrator",
9
+ "rulesync"
10
+ ],
11
+ "homepage": "https://github.com/lovrozagar/grunt#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/lovrozagar/grunt/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": "lovrozagar",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/lovrozagar/grunt.git"
20
+ },
21
+ "bin": {
22
+ "grunt": "./bin/grunt.js"
23
+ },
24
+ "files": [
25
+ "bin/grunt.js",
26
+ "cli",
27
+ "scripts/check-globals.mjs",
28
+ "scripts/emit-mcp-policy.mjs",
29
+ "scripts/gate-fat-tools.mjs",
30
+ "scripts/grunt-job.mjs",
31
+ "scripts/parse-need.mjs",
32
+ "scripts/persist-plan.mjs",
33
+ "scripts/purge-global-mcps.mjs",
34
+ "scripts/scrub-spawn-prompt.mjs",
35
+ "scripts/scrub-text-lib.mjs",
36
+ "scripts/sync-global-settings.mjs",
37
+ "scripts/telemetry.mjs",
38
+ "scripts/scrub-text",
39
+ ".rulesync",
40
+ ".grok",
41
+ ".codex",
42
+ ".claude",
43
+ ".agents",
44
+ "AGENTS.md",
45
+ "CLAUDE.md",
46
+ ".mcp.json",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "type": "module",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "rulesync:generate": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks && node scripts/emit-mcp-policy.mjs",
56
+ "rulesync:watch": "node scripts/emit-mcp-policy.mjs && rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents --watch",
57
+ "rulesync:check": "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents --check && rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks --check && node scripts/emit-mcp-policy.mjs --check && node scripts/check-globals.mjs",
58
+ "rulesync:doctor": "rulesync doctor",
59
+ "sync:globals": "node scripts/sync-global-settings.mjs",
60
+ "sync:globals:check": "node scripts/check-globals.mjs",
61
+ "sync:globals:apply": "node scripts/sync-global-settings.mjs --apply",
62
+ "purge:global-mcps": "node scripts/purge-global-mcps.mjs",
63
+ "purge:global-mcps:apply": "node scripts/purge-global-mcps.mjs --apply",
64
+ "test": "vitest run --coverage"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^24.13.3",
68
+ "@vitest/coverage-v8": "^3.2.7",
69
+ "rulesync": "latest",
70
+ "smol-toml": "^1.8.0",
71
+ "typescript": "^5.9.3",
72
+ "vitest": "^3.2.7"
73
+ },
74
+ "engines": {
75
+ "node": ">=22"
76
+ }
77
+ }