@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.
- package/.agents/agents/grunt.md +36 -0
- package/.agents/agents/implementer.md +40 -0
- package/.agents/agents/orchestrator.md +50 -0
- package/.agents/agents/thinker.md +27 -0
- package/.agents/hooks.json +26 -0
- package/.agents/mcp_config.json +3 -0
- package/.claude/agents/grunt.md +44 -0
- package/.claude/agents/implementer.md +46 -0
- package/.claude/agents/orchestrator.md +55 -0
- package/.claude/agents/thinker.md +34 -0
- package/.claude/settings.json +36 -0
- package/.codex/agents/grunt.toml +30 -0
- package/.codex/agents/implementer.toml +34 -0
- package/.codex/agents/orchestrator.toml +45 -0
- package/.codex/agents/thinker.toml +23 -0
- package/.codex/config.toml +2 -0
- package/.codex/hooks.json +26 -0
- package/.grok/agents/grunt.md +31 -0
- package/.grok/agents/implementer.md +35 -0
- package/.grok/agents/orchestrator.md +47 -0
- package/.grok/agents/thinker.md +25 -0
- package/.grok/config.toml +6 -0
- package/.grok/global-settings.toml +35 -0
- package/.grok/hooks/orchestrate-parent.js +476 -0
- package/.grok/hooks/orchestrate-parent.json +60 -0
- package/.grok/hooks/rtk-grok.js +137 -0
- package/.grok/hooks/rtk.json +16 -0
- package/.grok/hooks/rulesync.json +3 -0
- package/.grok/parent.md +1 -0
- package/.grok/roles/grunt.toml +5 -0
- package/.grok/roles/implementer.toml +5 -0
- package/.grok/roles/thinker.toml +5 -0
- package/.grok/skills/explain/SKILL.md +15 -0
- package/.grok/skills/implement-plan/SKILL.md +91 -0
- package/.grok/skills/parent/SKILL.md +12 -0
- package/.grok/skills/shared/plan-format.md +158 -0
- package/.grok/skills/write-plan/SKILL.md +84 -0
- package/.mcp.json +3 -0
- package/.rulesync/global-settings/antigravity.json +1 -0
- package/.rulesync/global-settings/claude.json +1 -0
- package/.rulesync/global-settings/codex.toml +1 -0
- package/.rulesync/global-settings/manifest.json +37 -0
- package/.rulesync/hooks.jsonc +22 -0
- package/.rulesync/mcp-policy.jsonc +15 -0
- package/.rulesync/reference/cascade.md +85 -0
- package/.rulesync/reference/hooks.md +19 -0
- package/.rulesync/reference/map.md +57 -0
- package/.rulesync/reference/rtk.md +23 -0
- package/.rulesync/rules/CLAUDE.md +13 -0
- package/.rulesync/rules/overview.md +23 -0
- package/.rulesync/skills/explain/SKILL.md +15 -0
- package/.rulesync/skills/parent/SKILL.md +12 -0
- package/.rulesync/skills/terse/SKILL.md +8 -0
- package/.rulesync/subagents/grunt.md +51 -0
- package/.rulesync/subagents/implementer.md +54 -0
- package/.rulesync/subagents/orchestrator.md +69 -0
- package/.rulesync/subagents/thinker.md +44 -0
- package/AGENTS.md +12 -0
- package/CLAUDE.md +7 -0
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/bin/grunt.js +4 -0
- package/cli/grunt.mjs +54 -0
- package/cli/grunt.test.ts +117 -0
- package/cli/init.mjs +113 -0
- package/cli/init.test.ts +268 -0
- package/package.json +77 -0
- package/scripts/check-globals.mjs +98 -0
- package/scripts/emit-mcp-policy.mjs +434 -0
- package/scripts/gate-fat-tools.mjs +485 -0
- package/scripts/grunt-job.mjs +482 -0
- package/scripts/parse-need.mjs +75 -0
- package/scripts/persist-plan.mjs +309 -0
- package/scripts/purge-global-mcps.mjs +291 -0
- package/scripts/scrub-spawn-prompt.mjs +219 -0
- package/scripts/scrub-text +58 -0
- package/scripts/scrub-text-lib.mjs +210 -0
- package/scripts/sync-global-settings.mjs +239 -0
- 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
|
+
}
|
package/cli/init.test.ts
ADDED
|
@@ -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
|
+
}
|