@lovrozagar/grunt 0.9.4 → 0.10.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/skills/jev/SKILL.md +18 -0
- package/.claude/skills/jev/SKILL.md +18 -0
- package/.grok/hooks/orchestrate-parent.js +9 -1
- package/.grok/skills/jev/SKILL.md +18 -0
- package/.rulesync/reference/INDEX.md +4 -2
- package/.rulesync/reference/hooks.md +1 -1
- package/.rulesync/reference/jev.md +56 -0
- package/.rulesync/reference/law.md +2 -2
- package/.rulesync/reference/refs-map.md +1 -0
- package/.rulesync/reference/setup.md +3 -1
- package/.rulesync/reference/skills-map.md +1 -0
- package/.rulesync/skills/jev/SKILL.md +15 -0
- package/CHANGELOG.md +15 -0
- package/README.md +19 -7
- package/cli/grunt.mjs +60 -8
- package/cli/grunt.test.ts +122 -1
- package/cli/init.mjs +118 -10
- package/cli/init.test.ts +302 -5
- package/cli/prompt.mjs +1 -0
- package/cli/prompt.test.ts +29 -0
- package/package.json +4 -1
- package/scripts/board.mjs +390 -0
- package/scripts/doctor.mjs +53 -15
- package/scripts/feature-packs.mjs +123 -0
- package/scripts/guarded-md.mjs +28 -0
- package/scripts/jev.mjs +355 -0
- package/scripts/listen.mjs +2 -1
- package/scripts/pipeline.mjs +84 -2
- package/scripts/prompt.mjs +4 -0
- package/scripts/session-map.mjs +4 -2
- package/scripts/setup.mjs +85 -47
- package/scripts/speak.mjs +2 -0
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(() => {
|
|
@@ -1714,6 +1723,158 @@ describe("mergePackageJson", () => {
|
|
|
1714
1723
|
expect(pkg.scripts[k]).toBe(v);
|
|
1715
1724
|
}
|
|
1716
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
|
+
});
|
|
1717
1878
|
});
|
|
1718
1879
|
|
|
1719
1880
|
describe("init", () => {
|
|
@@ -1764,7 +1925,8 @@ describe("init", () => {
|
|
|
1764
1925
|
expect(fs.existsSync(path.join(dest, ".mcp.json"))).toBe(false);
|
|
1765
1926
|
|
|
1766
1927
|
for (const name of PRODUCT_FILES) {
|
|
1767
|
-
|
|
1928
|
+
const optional = ["speak.mjs", "listen.mjs", "google-workspace.mjs"].includes(name);
|
|
1929
|
+
expect(fs.existsSync(path.join(dest, "scripts", name)), name).toBe(!optional);
|
|
1768
1930
|
}
|
|
1769
1931
|
expect(fs.statSync(path.join(dest, "scripts", "scrub-text")).isDirectory()).toBe(true);
|
|
1770
1932
|
expect(fs.readFileSync(path.join(dest, "scripts", "scrub-text", "inside"), "utf8")).toBe(
|
|
@@ -1875,6 +2037,7 @@ describe("init", () => {
|
|
|
1875
2037
|
"google-workspace",
|
|
1876
2038
|
"handoff",
|
|
1877
2039
|
"implement-plan",
|
|
2040
|
+
"jev",
|
|
1878
2041
|
"listen",
|
|
1879
2042
|
"pickup",
|
|
1880
2043
|
"speak",
|
|
@@ -1958,9 +2121,13 @@ describe("init", () => {
|
|
|
1958
2121
|
expect(claude.split("<!-- grunt:end -->")).toHaveLength(2);
|
|
1959
2122
|
expect(fs.existsSync(path.join(dest, "scripts", "scrub-text"))).toBe(true);
|
|
1960
2123
|
expect(fs.existsSync(path.join(dest, "scripts", "grunt-config.mjs"))).toBe(false);
|
|
1961
|
-
expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(
|
|
1962
|
-
expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(
|
|
1963
|
-
expect(fs.existsSync(path.join(dest, "scripts", "google-workspace.mjs"))).toBe(
|
|
2124
|
+
expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(false);
|
|
2125
|
+
expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(false);
|
|
2126
|
+
expect(fs.existsSync(path.join(dest, "scripts", "google-workspace.mjs"))).toBe(false);
|
|
2127
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "clasp"))).toBe(false);
|
|
2128
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "browser"))).toBe(true);
|
|
2129
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "reference", "listen.md"))).toBe(false);
|
|
2130
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.features.jsonc"))).toBe(false);
|
|
1964
2131
|
expect(fs.existsSync(path.join(dest, ".claude", "settings.json"))).toBe(true);
|
|
1965
2132
|
expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.config.jsonc"))).toBe(false);
|
|
1966
2133
|
expect(exec.mock.calls.map((c) => c[1])).toEqual([
|
|
@@ -1971,6 +2138,84 @@ describe("init", () => {
|
|
|
1971
2138
|
]);
|
|
1972
2139
|
});
|
|
1973
2140
|
|
|
2141
|
+
function writeSkill(root: string, name: string) {
|
|
2142
|
+
for (const dir of [".rulesync/skills", ".grok/skills", ".claude/skills", ".agents/skills"]) {
|
|
2143
|
+
fs.mkdirSync(path.join(root, dir, name), { recursive: true });
|
|
2144
|
+
fs.writeFileSync(path.join(root, dir, name, "SKILL.md"), `${name}\n`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
it("copies only selected packs and drops an unselected ref instead of merging it back", () => {
|
|
2149
|
+
const pkgRoot = stubPkgRoot();
|
|
2150
|
+
writeSkill(pkgRoot, "ask");
|
|
2151
|
+
writeSkill(pkgRoot, "listen");
|
|
2152
|
+
writeSkill(pkgRoot, "speak");
|
|
2153
|
+
writeSkill(pkgRoot, "clasp");
|
|
2154
|
+
fs.mkdirSync(path.join(pkgRoot, ".rulesync", "reference"), { recursive: true });
|
|
2155
|
+
fs.writeFileSync(path.join(pkgRoot, ".rulesync", "reference", "listen.md"), "# Listen\n\npack\n");
|
|
2156
|
+
fs.writeFileSync(path.join(pkgRoot, ".rulesync", "reference", "law.md"), "# Law\n\n## Skills naming\n\nstock\n");
|
|
2157
|
+
const dest = tmp("packs-default-");
|
|
2158
|
+
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
2159
|
+
fs.writeFileSync(
|
|
2160
|
+
path.join(dest, ".rulesync", "reference", "listen.md"),
|
|
2161
|
+
"# Listen\n\npack\n\n## Mine\n\nkeep\n",
|
|
2162
|
+
);
|
|
2163
|
+
fs.writeFileSync(
|
|
2164
|
+
path.join(dest, ".rulesync", "reference", "law.md"),
|
|
2165
|
+
"# Law\n\n## Skills naming\n\nold\n\n## Mine\n\nkeep\n",
|
|
2166
|
+
);
|
|
2167
|
+
fs.mkdirSync(path.join(dest, ".grok", "skills", "clasp"), { recursive: true });
|
|
2168
|
+
fs.writeFileSync(path.join(dest, ".grok", "skills", "clasp", "SKILL.md"), "old clasp\n");
|
|
2169
|
+
const exec = vi.fn();
|
|
2170
|
+
init(dest, { pkgRoot, execFileSync: exec, packageManager: "npm" });
|
|
2171
|
+
expect(fs.readFileSync(path.join(dest, ".rulesync", "skills", "ask", "SKILL.md"), "utf8")).toBe("ask\n");
|
|
2172
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "skills", "listen"))).toBe(false);
|
|
2173
|
+
expect(fs.existsSync(path.join(dest, ".grok", "skills", "speak"))).toBe(false);
|
|
2174
|
+
expect(fs.existsSync(path.join(dest, ".claude", "skills", "clasp"))).toBe(false);
|
|
2175
|
+
expect(fs.existsSync(path.join(dest, ".agents", "skills", "listen"))).toBe(false);
|
|
2176
|
+
expect(fs.existsSync(path.join(dest, ".grok", "skills", "clasp"))).toBe(false);
|
|
2177
|
+
expect(fs.existsSync(path.join(dest, "scripts", "listen.mjs"))).toBe(false);
|
|
2178
|
+
expect(fs.existsSync(path.join(dest, "scripts", "speak.mjs"))).toBe(false);
|
|
2179
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "reference", "listen.md"))).toBe(false);
|
|
2180
|
+
expect(fs.readFileSync(path.join(dest, ".rulesync", "reference", "law.md"), "utf8")).toContain("## Mine");
|
|
2181
|
+
expect(fs.existsSync(path.join(dest, ".rulesync", "grunt.features.jsonc"))).toBe(false);
|
|
2182
|
+
|
|
2183
|
+
const picked = tmp("packs-listen-");
|
|
2184
|
+
init(picked, { pkgRoot, execFileSync: exec, packageManager: "npm", packs: ["listen"] });
|
|
2185
|
+
expect(fs.readFileSync(path.join(picked, ".rulesync", "skills", "listen", "SKILL.md"), "utf8")).toBe("listen\n");
|
|
2186
|
+
expect(fs.readFileSync(path.join(picked, "scripts", "listen.mjs"), "utf8")).toBe("listen.mjs");
|
|
2187
|
+
expect(fs.existsSync(path.join(picked, ".rulesync", "reference", "listen.md"))).toBe(true);
|
|
2188
|
+
expect(fs.existsSync(path.join(picked, ".rulesync", "skills", "speak"))).toBe(false);
|
|
2189
|
+
expect(fs.existsSync(path.join(picked, "scripts", "speak.mjs"))).toBe(false);
|
|
2190
|
+
expect(JSON.parse(fs.readFileSync(path.join(picked, ".rulesync", "grunt.features.jsonc"), "utf8"))).toEqual({
|
|
2191
|
+
packs: ["listen"],
|
|
2192
|
+
});
|
|
2193
|
+
|
|
2194
|
+
const saved = tmp("packs-saved-");
|
|
2195
|
+
fs.mkdirSync(path.join(saved, ".rulesync"), { recursive: true });
|
|
2196
|
+
fs.writeFileSync(
|
|
2197
|
+
path.join(saved, ".rulesync", "grunt.features.jsonc"),
|
|
2198
|
+
'{"packs":["speak"]}\n',
|
|
2199
|
+
);
|
|
2200
|
+
init(saved, { pkgRoot, execFileSync: exec, packageManager: "npm" });
|
|
2201
|
+
expect(fs.existsSync(path.join(saved, ".rulesync", "skills", "speak"))).toBe(true);
|
|
2202
|
+
expect(fs.existsSync(path.join(saved, "scripts", "listen.mjs"))).toBe(false);
|
|
2203
|
+
expect(fs.readFileSync(path.join(saved, ".rulesync", "grunt.features.jsonc"), "utf8")).toBe(
|
|
2204
|
+
'{"packs":["speak"]}\n',
|
|
2205
|
+
);
|
|
2206
|
+
|
|
2207
|
+
const pkgSelf = stubPkgRoot();
|
|
2208
|
+
writeSkill(pkgSelf, "clasp");
|
|
2209
|
+
fs.writeFileSync(path.join(pkgSelf, "scripts", "speak.mjs"), "keep-speak\n");
|
|
2210
|
+
init(pkgSelf, { pkgRoot: pkgSelf, execFileSync: exec, packs: [] });
|
|
2211
|
+
expect(fs.readFileSync(path.join(pkgSelf, ".rulesync", "skills", "clasp", "SKILL.md"), "utf8")).toBe("clasp\n");
|
|
2212
|
+
expect(fs.readFileSync(path.join(pkgSelf, "scripts", "speak.mjs"), "utf8")).toBe("keep-speak\n");
|
|
2213
|
+
expect(fs.existsSync(path.join(pkgSelf, ".rulesync", "grunt.features.jsonc"))).toBe(false);
|
|
2214
|
+
expect(() => init(tmp("packs-bad-"), { pkgRoot, execFileSync: exec, packageManager: "npm", packs: ["nope"] })).toThrow(
|
|
2215
|
+
/unknown pack/,
|
|
2216
|
+
);
|
|
2217
|
+
});
|
|
2218
|
+
|
|
1974
2219
|
it("--skip-globals skips sync:globals:apply", () => {
|
|
1975
2220
|
const pkgRoot = stubPkgRoot();
|
|
1976
2221
|
const dest = tmp("grunt-skip-globals-");
|
|
@@ -2220,6 +2465,43 @@ describe("init", () => {
|
|
|
2220
2465
|
).toThrow("emit-maps failed");
|
|
2221
2466
|
});
|
|
2222
2467
|
|
|
2468
|
+
it("keeps extra ## sections in dest law.md and INDEX", () => {
|
|
2469
|
+
const dest = tmp("grunt-law-extra-");
|
|
2470
|
+
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
2471
|
+
const pkgLaw = fs.readFileSync(path.join(PKG_ROOT, ".rulesync", "reference", "law.md"), "utf8");
|
|
2472
|
+
fs.writeFileSync(
|
|
2473
|
+
path.join(dest, ".rulesync", "reference", "law.md"),
|
|
2474
|
+
`${pkgLaw.trimEnd()}\n\n## Autorun\n\npush → go-live/\n`,
|
|
2475
|
+
);
|
|
2476
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
|
|
2477
|
+
const law = fs.readFileSync(path.join(dest, ".rulesync", "reference", "law.md"), "utf8");
|
|
2478
|
+
expect(law).toMatch(/## Skills naming/);
|
|
2479
|
+
expect(law).toMatch(/## Autorun/);
|
|
2480
|
+
expect(law).toMatch(/push → go-live\//);
|
|
2481
|
+
const index = fs.readFileSync(path.join(dest, ".rulesync", "reference", "INDEX.md"), "utf8");
|
|
2482
|
+
expect(index).toMatch(/## Autorun/);
|
|
2483
|
+
expect(index).toMatch(/push → go-live\//);
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
it("seeds grunt.pipeline.jsonc from dest pipeline omissions during init", () => {
|
|
2487
|
+
const dest = tmp("grunt-pipe-init-");
|
|
2488
|
+
fs.mkdirSync(path.join(dest, "scripts"), { recursive: true });
|
|
2489
|
+
const skipped = PIPELINE_COMMANDS.check.find((c) => c.includes("subagents"));
|
|
2490
|
+
const src = `export const COMMANDS = {
|
|
2491
|
+
check: [
|
|
2492
|
+
${PIPELINE_COMMANDS.check.filter((c) => c !== skipped).map((c) => JSON.stringify(c)).join(",\n ")}
|
|
2493
|
+
],
|
|
2494
|
+
};
|
|
2495
|
+
`;
|
|
2496
|
+
fs.writeFileSync(path.join(dest, "scripts", "pipeline.mjs"), src);
|
|
2497
|
+
init(dest, { execFileSync: vi.fn(), skipGlobals: true, packageManager: "npm" });
|
|
2498
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(dest, PIPELINE_CONFIG_REL), "utf8"));
|
|
2499
|
+
expect(cfg.check.skip).toEqual([skipped]);
|
|
2500
|
+
expect(fs.readFileSync(path.join(dest, "scripts", "pipeline.mjs"), "utf8")).toContain(
|
|
2501
|
+
"subagents --check",
|
|
2502
|
+
);
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2223
2505
|
it("does not stamp package INDEX over dest; re-emits union", () => {
|
|
2224
2506
|
const dest = tmp("grunt-index-keep-");
|
|
2225
2507
|
fs.mkdirSync(path.join(dest, ".rulesync", "reference"), { recursive: true });
|
|
@@ -2413,16 +2695,31 @@ describe("product scripts are consumer-safe", () => {
|
|
|
2413
2695
|
expect(fs.existsSync(path.join(dest, "cli"))).toBe(false);
|
|
2414
2696
|
expect(fs.existsSync(path.join(dest, "scripts", "guarded-roots.mjs"))).toBe(true);
|
|
2415
2697
|
|
|
2698
|
+
const optionalScripts = new Set(["speak.mjs", "listen.mjs", "google-workspace.mjs"]);
|
|
2416
2699
|
for (const name of PRODUCT_FILES) {
|
|
2417
2700
|
const file = path.join(dest, "scripts", name);
|
|
2701
|
+
if (optionalScripts.has(name)) {
|
|
2702
|
+
expect(fs.existsSync(file), name).toBe(false);
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2418
2705
|
expect(fs.existsSync(file), name).toBe(true);
|
|
2419
2706
|
const text = fs.readFileSync(file, "utf8");
|
|
2420
2707
|
for (const spec of relativeSpecs(text, RELATIVE_IMPORT_RE)) {
|
|
2421
2708
|
const resolved = path.resolve(path.dirname(file), spec);
|
|
2422
|
-
|
|
2709
|
+
const optional = optionalScripts.has(path.basename(resolved));
|
|
2710
|
+
expect(fs.existsSync(resolved), `${name} -> ${spec}`).toBe(!optional);
|
|
2423
2711
|
}
|
|
2424
2712
|
}
|
|
2425
2713
|
|
|
2714
|
+
const setupRun = spawnDest(dest, ["./scripts/setup.mjs"], {
|
|
2715
|
+
...process.env,
|
|
2716
|
+
CI: "1",
|
|
2717
|
+
PATH: ["/usr/bin", "/bin"].join(path.delimiter),
|
|
2718
|
+
});
|
|
2719
|
+
expect(`${setupRun.stdout}${setupRun.stderr}`).not.toMatch(/ERR_MODULE_NOT_FOUND/);
|
|
2720
|
+
expect(setupRun.status).toBe(1);
|
|
2721
|
+
expect(`${setupRun.stdout}${setupRun.stderr}`).toContain("usage: setup");
|
|
2722
|
+
|
|
2426
2723
|
const loaded = spawnDest(dest, ["./scripts/guarded-roots.mjs"]);
|
|
2427
2724
|
expect(loaded.status).toBe(1);
|
|
2428
2725
|
expect(loaded.stderr).toBe("usage: guarded-roots.mjs generate|check|watch\n");
|
package/cli/prompt.mjs
CHANGED
package/cli/prompt.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
const clackSelect = vi.hoisted(() => vi.fn());
|
|
4
|
+
const clackMultiselect = vi.hoisted(() => vi.fn());
|
|
4
5
|
const clackConfirm = vi.hoisted(() => vi.fn());
|
|
5
6
|
const clackText = vi.hoisted(() => vi.fn());
|
|
6
7
|
const clackPassword = vi.hoisted(() => vi.fn());
|
|
@@ -10,6 +11,7 @@ const clackCancel = vi.hoisted(() => vi.fn());
|
|
|
10
11
|
|
|
11
12
|
vi.mock("@clack/prompts", () => ({
|
|
12
13
|
select: clackSelect,
|
|
14
|
+
multiselect: clackMultiselect,
|
|
13
15
|
confirm: clackConfirm,
|
|
14
16
|
text: clackText,
|
|
15
17
|
password: clackPassword,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
confirm,
|
|
24
26
|
isInteractive,
|
|
25
27
|
password,
|
|
28
|
+
multiselect,
|
|
26
29
|
select,
|
|
27
30
|
spinner,
|
|
28
31
|
text,
|
|
@@ -164,12 +167,38 @@ describe("bailIfCancel", () => {
|
|
|
164
167
|
describe("clack wrappers", () => {
|
|
165
168
|
afterEach(() => {
|
|
166
169
|
clackSelect.mockReset();
|
|
170
|
+
clackMultiselect.mockReset();
|
|
167
171
|
clackConfirm.mockReset();
|
|
168
172
|
clackSpinner.mockReset();
|
|
169
173
|
clackIsCancel.mockReset();
|
|
170
174
|
clackCancel.mockReset();
|
|
171
175
|
});
|
|
172
176
|
|
|
177
|
+
it("multiselect returns the chosen packs", async () => {
|
|
178
|
+
clackMultiselect.mockResolvedValue(["listen"]);
|
|
179
|
+
clackIsCancel.mockReturnValue(false);
|
|
180
|
+
await expect(
|
|
181
|
+
multiselect({ message: "Optional packs", options: [], required: false }),
|
|
182
|
+
).resolves.toEqual(["listen"]);
|
|
183
|
+
expect(clackMultiselect).toHaveBeenCalledWith({
|
|
184
|
+
message: "Optional packs",
|
|
185
|
+
options: [],
|
|
186
|
+
required: false,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("multiselect cancel exits 0", async () => {
|
|
191
|
+
clackMultiselect.mockResolvedValue(Symbol("cancel"));
|
|
192
|
+
clackIsCancel.mockReturnValue(true);
|
|
193
|
+
const exit = vi.spyOn(process, "exit").mockImplementation((() => {
|
|
194
|
+
throw new Error("EXIT");
|
|
195
|
+
}) as typeof process.exit);
|
|
196
|
+
await expect(multiselect({})).rejects.toThrow("EXIT");
|
|
197
|
+
expect(clackCancel).toHaveBeenCalledWith("Aborted");
|
|
198
|
+
expect(exit).toHaveBeenCalledWith(0);
|
|
199
|
+
exit.mockRestore();
|
|
200
|
+
});
|
|
201
|
+
|
|
173
202
|
it("select bails then returns", async () => {
|
|
174
203
|
clackSelect.mockResolvedValue("init");
|
|
175
204
|
clackIsCancel.mockReturnValue(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/grunt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Multi-agent orchestrator SoT — deep-merge rulesync trees, product scripts, and generate pipeline into a consumer repo",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -48,9 +48,12 @@
|
|
|
48
48
|
"scripts/scrub-text-lib.mjs",
|
|
49
49
|
"scripts/session-map.mjs",
|
|
50
50
|
"scripts/sync-global-settings.mjs",
|
|
51
|
+
"scripts/board.mjs",
|
|
51
52
|
"scripts/browser.mjs",
|
|
53
|
+
"scripts/feature-packs.mjs",
|
|
52
54
|
"scripts/speak.mjs",
|
|
53
55
|
"scripts/listen.mjs",
|
|
56
|
+
"scripts/jev.mjs",
|
|
54
57
|
"scripts/google-workspace.mjs",
|
|
55
58
|
"scripts/interactive.mjs",
|
|
56
59
|
"scripts/prompt.mjs",
|