@lovrozagar/grunt 0.4.2 → 0.5.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 (48) hide show
  1. package/.agents/agents/orchestrator.md +8 -7
  2. package/.agents/agents/thinker.md +6 -5
  3. package/.agents/skills/commit-and-push/SKILL.md +3 -3
  4. package/.agents/skills/explain/SKILL.md +1 -0
  5. package/.agents/skills/implement-plan/SKILL.md +116 -0
  6. package/.agents/skills/write-plan/SKILL.md +82 -0
  7. package/.claude/agents/orchestrator.md +8 -7
  8. package/.claude/agents/thinker.md +6 -5
  9. package/.claude/skills/commit-and-push/SKILL.md +3 -3
  10. package/.claude/skills/explain/SKILL.md +1 -0
  11. package/.claude/skills/implement-plan/SKILL.md +116 -0
  12. package/.claude/skills/write-plan/SKILL.md +82 -0
  13. package/.codex/agents/orchestrator.toml +8 -7
  14. package/.codex/agents/thinker.toml +6 -5
  15. package/.grok/agents/orchestrator.md +8 -7
  16. package/.grok/agents/thinker.md +6 -5
  17. package/.grok/skills/commit-and-push/SKILL.md +3 -3
  18. package/.grok/skills/explain/SKILL.md +1 -0
  19. package/.grok/skills/implement-plan/SKILL.md +45 -17
  20. package/.grok/skills/write-plan/SKILL.md +11 -11
  21. package/.rulesync/reference/INDEX.md +43 -0
  22. package/.rulesync/reference/cascade.md +11 -9
  23. package/.rulesync/reference/law.md +9 -0
  24. package/.rulesync/reference/map.md +10 -5
  25. package/.rulesync/reference/output.md +13 -8
  26. package/{.grok/skills/shared → .rulesync/reference}/plan-format.md +2 -1
  27. package/.rulesync/reference/refs-map.md +14 -0
  28. package/.rulesync/reference/skills-map.md +20 -0
  29. package/.rulesync/rules/CLAUDE.md +12 -10
  30. package/.rulesync/rules/overview.md +12 -10
  31. package/.rulesync/skills/commit-and-push/SKILL.md +3 -3
  32. package/.rulesync/skills/explain/SKILL.md +1 -0
  33. package/.rulesync/skills/implement-plan/SKILL.md +124 -0
  34. package/.rulesync/skills/write-plan/SKILL.md +88 -0
  35. package/.rulesync/subagents/orchestrator.md +8 -7
  36. package/.rulesync/subagents/thinker.md +6 -5
  37. package/AGENTS.md +12 -10
  38. package/CHANGELOG.md +23 -0
  39. package/CLAUDE.md +12 -10
  40. package/README.md +14 -10
  41. package/cli/init.mjs +20 -1
  42. package/cli/init.test.ts +81 -4
  43. package/package.json +4 -4
  44. package/scripts/doctor.mjs +64 -2
  45. package/scripts/emit-maps.mjs +422 -0
  46. package/scripts/guarded-roots.mjs +4 -7
  47. package/scripts/pipeline.mjs +67 -0
  48. package/scripts/skill-conflicts.mjs +71 -0
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+ /** Emit INDEX (aggregate) + skills-map/refs-map slices from one skills+refs scan; law.md is a hand slice. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { splitFrontmatter } from "./emit-agent-shell-tools.mjs";
7
+
8
+ export const INDEX_REL = ".rulesync/reference/INDEX.md";
9
+ export const SKILLS_MAP_REL = ".rulesync/reference/skills-map.md";
10
+ export const REFS_MAP_REL = ".rulesync/reference/refs-map.md";
11
+ export const LAW_REL = ".rulesync/reference/law.md";
12
+ const SKILLS_REL = ".rulesync/skills";
13
+ const REFS_REL = ".rulesync/reference";
14
+ const PACKAGED_SKILLS_REL = path.join("node_modules", "@lovrozagar", "grunt", ".rulesync", "skills");
15
+ const BANNER = "<!-- Generated by emit-maps.mjs. Do not hand-edit. -->\n";
16
+ const DESC_MAX = 120;
17
+ const HUGE_BYTES = 512 * 1024;
18
+ const SKIP_REF_NAMES = new Set(["skills-map.md", "refs-map.md", "INDEX.md"]);
19
+
20
+ export function parseArgv(argv) {
21
+ const args = Array.isArray(argv) ? argv : [];
22
+ let check = false;
23
+ for (const a of args) {
24
+ if (a === "--check") {
25
+ check = true;
26
+ continue;
27
+ }
28
+ return { ok: false, error: `unknown flag: ${a}` };
29
+ }
30
+ return { ok: true, check };
31
+ }
32
+
33
+ function unquoteYaml(raw) {
34
+ const s = String(raw).trim();
35
+ if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
36
+ try {
37
+ return JSON.parse(s);
38
+ } catch {
39
+ return s.slice(1, -1);
40
+ }
41
+ }
42
+ if (s.length >= 2 && s[0] === "'" && s[s.length - 1] === "'") {
43
+ return s.slice(1, -1).replace(/''/g, "'");
44
+ }
45
+ return s;
46
+ }
47
+
48
+ function parseFlowSeq(s) {
49
+ const t = String(s).trim();
50
+ if (!t.startsWith("[") || !t.endsWith("]")) return null;
51
+ const inner = t.slice(1, -1).trim();
52
+ if (!inner) return [];
53
+ return inner.split(",").map((x) => unquoteYaml(x.trim())).filter((x) => x !== "");
54
+ }
55
+
56
+ function indentOf(line) {
57
+ const m = String(line).match(/^[ \t]*/);
58
+ return m ? m[0].length : 0;
59
+ }
60
+
61
+ function parseYamlDoc(fm) {
62
+ const lines = String(fm).split(/\r?\n/);
63
+ let i = 0;
64
+ function parseBlock(baseIndent) {
65
+ const obj = {};
66
+ while (i < lines.length) {
67
+ const line = lines[i];
68
+ if (!line.trim() || /^\s*#/.test(line)) {
69
+ i += 1;
70
+ continue;
71
+ }
72
+ const ind = indentOf(line);
73
+ if (ind < baseIndent) break;
74
+ if (baseIndent > 0 && ind > baseIndent) break;
75
+ const trimmed = line.trim();
76
+ const km = trimmed.match(/^([^:#\s][^:]*):\s*(.*?)\s*$/);
77
+ if (!km) {
78
+ i += 1;
79
+ continue;
80
+ }
81
+ const key = km[1].trim();
82
+ const val = km[2];
83
+ i += 1;
84
+ if (val === ">" || val === "|") {
85
+ const parts = [];
86
+ while (i < lines.length) {
87
+ const n = lines[i];
88
+ if (!n.trim()) {
89
+ parts.push("");
90
+ i += 1;
91
+ continue;
92
+ }
93
+ if (indentOf(n) <= ind) break;
94
+ parts.push(n.trim());
95
+ i += 1;
96
+ }
97
+ obj[key] = parts.filter(Boolean).join(" ");
98
+ continue;
99
+ }
100
+ if (val === "") {
101
+ if (i < lines.length && indentOf(lines[i]) > ind) {
102
+ const next = lines[i].trim();
103
+ if (next.startsWith("- ")) {
104
+ const arr = [];
105
+ while (i < lines.length && indentOf(lines[i]) > ind && lines[i].trim().startsWith("- ")) {
106
+ arr.push(unquoteYaml(lines[i].trim().replace(/^- /, "")));
107
+ i += 1;
108
+ }
109
+ obj[key] = arr;
110
+ } else {
111
+ obj[key] = parseBlock(indentOf(lines[i]));
112
+ }
113
+ } else {
114
+ obj[key] = {};
115
+ }
116
+ continue;
117
+ }
118
+ const seq = parseFlowSeq(val);
119
+ obj[key] = seq != null ? seq : unquoteYaml(val);
120
+ }
121
+ return obj;
122
+ }
123
+ return parseBlock(0);
124
+ }
125
+
126
+ function oneLine(s, max = DESC_MAX) {
127
+ const t = String(s ?? "").replace(/\s+/g, " ").trim();
128
+ if (t.length <= max) return t;
129
+ return `${t.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
130
+ }
131
+
132
+ function cell(s) {
133
+ return oneLine(s).replace(/\|/g, "\\|");
134
+ }
135
+
136
+ function readText(abs) {
137
+ try {
138
+ return fs.readFileSync(abs, "utf8");
139
+ } catch (err) {
140
+ if (err && err.code === "ENOENT") return null;
141
+ throw err;
142
+ }
143
+ }
144
+
145
+ function lstat(abs) {
146
+ try {
147
+ return fs.lstatSync(abs);
148
+ } catch (err) {
149
+ if (err && err.code === "ENOENT") return null;
150
+ throw err;
151
+ }
152
+ }
153
+
154
+ function posixRel(...parts) {
155
+ return parts.join("/").replace(/\\/g, "/");
156
+ }
157
+
158
+ function mapRefs(parsed) {
159
+ if (Array.isArray(parsed["map.refs"])) return parsed["map.refs"].map(String);
160
+ if (typeof parsed["map.refs"] === "string" && parsed["map.refs"]) return [parsed["map.refs"]];
161
+ const m = parsed.map;
162
+ if (m && Array.isArray(m.refs)) return m.refs.map(String);
163
+ if (m && typeof m.refs === "string" && m.refs) return [m.refs];
164
+ return [];
165
+ }
166
+
167
+ function scalarTask(v) {
168
+ return typeof v === "string" && v !== "" ? v : "";
169
+ }
170
+
171
+ function mapTask(parsed) {
172
+ const dotted = scalarTask(parsed["map.task"]);
173
+ if (dotted) return dotted;
174
+ const m = parsed.map;
175
+ if (m && typeof m === "object" && !Array.isArray(m)) return scalarTask(m.task);
176
+ return "";
177
+ }
178
+
179
+ function tagsOf(parsed) {
180
+ if (Array.isArray(parsed.tags)) return parsed.tags.map(String);
181
+ if (typeof parsed.tags === "string" && parsed.tags) {
182
+ const seq = parseFlowSeq(parsed.tags);
183
+ if (seq) return seq.map(String);
184
+ return [parsed.tags];
185
+ }
186
+ return [];
187
+ }
188
+
189
+ function firstH1(body) {
190
+ const m = String(body).match(/^#\s+(.+?)\s*$/m);
191
+ return m ? m[1].replace(/\s+#+\s*$/, "").trim() : "";
192
+ }
193
+
194
+ function firstSummary(body) {
195
+ const lines = String(body).split(/\r?\n/);
196
+ let pastH1 = false;
197
+ for (const line of lines) {
198
+ const t = line.trim();
199
+ if (!pastH1) {
200
+ if (/^#\s+/.test(t)) pastH1 = true;
201
+ continue;
202
+ }
203
+ if (!t) continue;
204
+ if (/^#/.test(t) || /^```/.test(t) || /^<!--/.test(t) || /^---/.test(t)) continue;
205
+ return oneLine(t);
206
+ }
207
+ return "";
208
+ }
209
+
210
+ function parseMarkdown(raw) {
211
+ const src = String(raw ?? "");
212
+ const split = splitFrontmatter(src);
213
+ if (!split.ok) return { parsed: {}, body: src };
214
+ return { parsed: parseYamlDoc(split.fm), body: split.body };
215
+ }
216
+
217
+ function listSkillDirs(ws) {
218
+ const root = path.join(ws, SKILLS_REL);
219
+ let names;
220
+ try {
221
+ names = fs.readdirSync(root, { withFileTypes: true });
222
+ } catch (err) {
223
+ if (err && err.code === "ENOENT") return [];
224
+ throw err;
225
+ }
226
+ const out = [];
227
+ for (const ent of names) {
228
+ if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
229
+ const skillAbs = path.join(root, ent.name, "SKILL.md");
230
+ const st = lstat(skillAbs);
231
+ if (st && st.isFile()) out.push({ name: ent.name, abs: skillAbs });
232
+ }
233
+ out.sort((a, b) => a.name.localeCompare(b.name));
234
+ return out;
235
+ }
236
+
237
+ function walkRefMd(dir, relBase, acc) {
238
+ let entries;
239
+ try {
240
+ entries = fs.readdirSync(dir, { withFileTypes: true });
241
+ } catch (err) {
242
+ if (err && err.code === "ENOENT") return acc;
243
+ throw err;
244
+ }
245
+ for (const ent of entries) {
246
+ if (ent.name.startsWith(".")) continue;
247
+ const abs = path.join(dir, ent.name);
248
+ const rel = relBase ? posixRel(relBase, ent.name) : posixRel(REFS_REL, ent.name);
249
+ if (ent.isDirectory()) {
250
+ walkRefMd(abs, rel, acc);
251
+ continue;
252
+ }
253
+ if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
254
+ if (SKIP_REF_NAMES.has(ent.name)) continue;
255
+ acc.push({ abs, rel });
256
+ }
257
+ return acc;
258
+ }
259
+
260
+ function packagedSkillExists(ws, name) {
261
+ const abs = path.join(ws, PACKAGED_SKILLS_REL, name, "SKILL.md");
262
+ const st = lstat(abs);
263
+ return Boolean(st && st.isFile());
264
+ }
265
+
266
+ function skillFields(ws, skill) {
267
+ const raw = readText(skill.abs);
268
+ if (raw == null) return null;
269
+ const { parsed, body } = parseMarkdown(raw);
270
+ const name = parsed.name != null && parsed.name !== "" ? String(parsed.name) : skill.name;
271
+ const descSrc =
272
+ parsed.description != null && parsed.description !== "" ? parsed.description : firstSummary(body);
273
+ const commandPath = parsed.commandPath != null ? String(parsed.commandPath) : "";
274
+ const refs = mapRefs(parsed).join(", ");
275
+ const task = mapTask(parsed);
276
+ const origin = packagedSkillExists(ws, skill.name) ? "grunt" : "local";
277
+ return { name, origin, descSrc, commandPath, refs, task };
278
+ }
279
+
280
+ function skillRowFromFields(f) {
281
+ return `| ${cell(f.name)} | ${cell(f.origin)} | ${cell(f.descSrc)} | ${cell(f.task)} | ${cell(f.commandPath)} | ${cell(f.refs)} |`;
282
+ }
283
+
284
+ function refRow(file) {
285
+ const st = lstat(file.abs);
286
+ if (!st || !st.isFile()) return null;
287
+ const fallbackTitle = path.basename(file.rel, ".md");
288
+ if (st.size > HUGE_BYTES) {
289
+ return `| ${cell(file.rel)} | ${cell(fallbackTitle)} | | |`;
290
+ }
291
+ const raw = readText(file.abs);
292
+ if (raw == null || raw.includes("\0")) {
293
+ return `| ${cell(file.rel)} | ${cell(fallbackTitle)} | | |`;
294
+ }
295
+ const { parsed, body } = parseMarkdown(raw);
296
+ const title = firstH1(body) || fallbackTitle;
297
+ const tags = tagsOf(parsed).join(", ");
298
+ const summary = firstSummary(body);
299
+ return `| ${cell(file.rel)} | ${cell(title)} | ${cell(tags)} | ${cell(summary)} |`;
300
+ }
301
+
302
+ function skillsTable(rows) {
303
+ return [
304
+ "| name | origin | description | task | commandPath | refs |",
305
+ "| --- | --- | --- | --- | --- | --- |",
306
+ ...rows,
307
+ ].join("\n");
308
+ }
309
+
310
+ function refsTable(rows) {
311
+ return [
312
+ "| path | title | tags | summary |",
313
+ "| --- | --- | --- | --- |",
314
+ ...rows,
315
+ ].join("\n");
316
+ }
317
+
318
+ function renderSkillsMap(rows) {
319
+ return `${BANNER}\n# Skills map\n\n${skillsTable(rows)}\n`;
320
+ }
321
+
322
+ function renderRefsMap(rows) {
323
+ return `${BANNER}\n# Refs map\n\n${refsTable(rows)}\n`;
324
+ }
325
+
326
+ function readLawBody(ws) {
327
+ const abs = path.join(ws, LAW_REL);
328
+ const st = lstat(abs);
329
+ if (!st || !st.isFile()) {
330
+ return { ok: false, error: `missing: ${LAW_REL}` };
331
+ }
332
+ const raw = readText(abs);
333
+ if (raw == null) {
334
+ return { ok: false, error: `missing: ${LAW_REL}` };
335
+ }
336
+ const { body } = parseMarkdown(raw);
337
+ return { ok: true, body: String(body ?? "").replace(/^\uFEFF/, "").replace(/\s+$/, "") };
338
+ }
339
+
340
+ function renderIndex(lawBody, skillRows, refRows) {
341
+ return `${BANNER}\n${lawBody}\n\n# Skills\n\n${skillsTable(skillRows)}\n\n# Refs\n\n${refsTable(refRows)}\n`;
342
+ }
343
+
344
+ function desiredFiles(ws) {
345
+ const law = readLawBody(ws);
346
+ if (!law.ok) return law;
347
+ const fields = listSkillDirs(ws).map((s) => skillFields(ws, s)).filter(Boolean);
348
+ const skills = fields.map((f) => skillRowFromFields(f));
349
+ const refs = walkRefMd(path.join(ws, REFS_REL), "", [])
350
+ .sort((a, b) => a.rel.localeCompare(b.rel))
351
+ .map(refRow)
352
+ .filter(Boolean);
353
+ return {
354
+ ok: true,
355
+ files: [
356
+ { rel: INDEX_REL, text: renderIndex(law.body, skills, refs) },
357
+ { rel: SKILLS_MAP_REL, text: renderSkillsMap(skills) },
358
+ { rel: REFS_MAP_REL, text: renderRefsMap(refs) },
359
+ ],
360
+ };
361
+ }
362
+
363
+ export function emitMaps({ workspaceRoot, check = false } = {}) {
364
+ const ws = workspaceRoot || process.cwd();
365
+ const rulesync = path.join(ws, ".rulesync");
366
+ const st = lstat(rulesync);
367
+ if (!st || !st.isDirectory()) {
368
+ return { ok: true, check, skipped: true };
369
+ }
370
+
371
+ const desired = desiredFiles(ws);
372
+ if (!desired.ok) {
373
+ return { ok: false, check, error: desired.error };
374
+ }
375
+ const files = desired.files;
376
+ if (check) {
377
+ const drift = [];
378
+ for (const f of files) {
379
+ const disk = readText(path.join(ws, f.rel));
380
+ if (disk !== f.text) drift.push(f.rel);
381
+ }
382
+ if (drift.length) {
383
+ return { ok: false, check: true, error: `drift: ${drift.join(", ")}`, drift };
384
+ }
385
+ return { ok: true, check: true, drift: [] };
386
+ }
387
+
388
+ for (const f of files) {
389
+ const abs = path.join(ws, f.rel);
390
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
391
+ fs.writeFileSync(abs, f.text);
392
+ }
393
+ return { ok: true, check: false };
394
+ }
395
+
396
+ function main() {
397
+ try {
398
+ const parsed = parseArgv(process.argv.slice(2));
399
+ if (!parsed.ok) {
400
+ process.stderr.write((parsed.error || "invalid argv") + "\n");
401
+ return 1;
402
+ }
403
+ const result = emitMaps({
404
+ workspaceRoot: process.cwd(),
405
+ check: parsed.check,
406
+ });
407
+ if (!result.ok) {
408
+ process.stderr.write((result.error || "emit failed") + "\n");
409
+ return 1;
410
+ }
411
+ return 0;
412
+ } catch (err) {
413
+ process.stderr.write(String(err && err.message ? err.message : err) + "\n");
414
+ return 1;
415
+ }
416
+ }
417
+
418
+ const thisFile = fileURLToPath(import.meta.url);
419
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
420
+ if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
421
+ process.exit(main());
422
+ }
@@ -11,10 +11,7 @@ import {
11
11
  snapshotGuardedRoots,
12
12
  withGuardedCheckInteriors,
13
13
  } from "../cli/init.mjs";
14
-
15
- const GENERATE_RAW = "rulesync:generate:raw";
16
- const CHECK_RAW = "rulesync:check:raw";
17
- const WATCH_RAW = "rulesync:watch:raw";
14
+ import { runPipeline } from "./pipeline.mjs";
18
15
 
19
16
  export function attachGuardedRootWatchers(cwd, heal) {
20
17
  const w = fs.watch(cwd, (_event, filename) => {
@@ -36,7 +33,7 @@ export function runGuardedRoots(mode, { cwd = process.cwd(), exec = execSync, at
36
33
  if (mode === "generate") {
37
34
  const snap = snapshotGuardedRoots(cwd);
38
35
  try {
39
- exec(`npm run ${GENERATE_RAW}`, { cwd, stdio: "inherit", shell: true });
36
+ runPipeline("generate", { cwd, exec });
40
37
  } finally {
41
38
  remergeGuardedRoots(cwd, snap);
42
39
  }
@@ -44,7 +41,7 @@ export function runGuardedRoots(mode, { cwd = process.cwd(), exec = execSync, at
44
41
  }
45
42
  if (mode === "check") {
46
43
  withGuardedCheckInteriors(cwd, () => {
47
- exec(`npm run ${CHECK_RAW}`, { cwd, stdio: "inherit", shell: true });
44
+ runPipeline("check", { cwd, exec });
48
45
  });
49
46
  return;
50
47
  }
@@ -54,7 +51,7 @@ export function runGuardedRoots(mode, { cwd = process.cwd(), exec = execSync, at
54
51
  const attach = attachWatchers || ((opts) => attachGuardedRootWatchers(opts.cwd, opts.heal));
55
52
  const stop = attach({ cwd, snap, heal });
56
53
  try {
57
- exec(`npm run ${WATCH_RAW}`, { cwd, stdio: "inherit", shell: true });
54
+ runPipeline("watch", { cwd, exec });
58
55
  } finally {
59
56
  try {
60
57
  stop?.();
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /** Inner generate/check/watch chain. Called by guarded-roots; not a public npm script. */
3
+ import { execSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const COMMANDS = {
8
+ generate: [
9
+ "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills",
10
+ "rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks",
11
+ "node scripts/emit-mcp-policy.mjs",
12
+ "node scripts/emit-gemini.mjs",
13
+ "node scripts/emit-agent-shell-tools.mjs",
14
+ "node scripts/emit-maps.mjs",
15
+ "node scripts/hooks-union.mjs",
16
+ ],
17
+ check: [
18
+ "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,skills --check",
19
+ "rulesync generate -t codexcli,antigravity-cli,grokcli -f subagents --check",
20
+ "rulesync generate -t claudecode,codexcli,antigravity-cli -f hooks --check",
21
+ "node scripts/emit-mcp-policy.mjs --check",
22
+ "node scripts/emit-gemini.mjs --check",
23
+ "node scripts/emit-agent-shell-tools.mjs --check",
24
+ "node scripts/emit-maps.mjs --check",
25
+ "node scripts/check-globals.mjs",
26
+ "node scripts/hooks-union.mjs --check",
27
+ ],
28
+ watch: [
29
+ "node scripts/emit-mcp-policy.mjs",
30
+ "node scripts/emit-gemini.mjs",
31
+ "node scripts/emit-agent-shell-tools.mjs",
32
+ "rulesync generate -t claudecode,codexcli,antigravity-cli,grokcli -f rules,subagents,skills --watch",
33
+ ],
34
+ };
35
+
36
+ export function envWithLocalBin(cwd, env = process.env) {
37
+ const binDir = path.join(cwd, "node_modules", ".bin");
38
+ const merged = { ...env };
39
+ const key = Object.keys(merged).find((k) => k.toLowerCase() === "path") || "PATH";
40
+ merged[key] = `${binDir}${path.delimiter}${merged[key] || ""}`;
41
+ return merged;
42
+ }
43
+
44
+ export function runPipeline(mode, { cwd = process.cwd(), exec = execSync } = {}) {
45
+ const cmds = COMMANDS[mode];
46
+ if (!cmds) {
47
+ throw new Error("usage: pipeline.mjs generate|check|watch");
48
+ }
49
+ const env = envWithLocalBin(cwd);
50
+ for (const cmd of cmds) {
51
+ exec(cmd, { cwd, stdio: "inherit", shell: true, env });
52
+ }
53
+ }
54
+
55
+ function main() {
56
+ try {
57
+ runPipeline(process.argv[2] || "");
58
+ return 0;
59
+ } catch (err) {
60
+ process.stderr.write((err && err.message ? err.message : String(err)) + "\n");
61
+ return 1;
62
+ }
63
+ }
64
+
65
+ const thisFile = fileURLToPath(import.meta.url);
66
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
67
+ if (invoked === thisFile) process.exit(main());
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /** Workspace vs packaged grunt skill content conflicts (warn-only). */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ export const WORKSPACE_SKILLS_REL = ".rulesync/skills";
7
+ export const PACKAGED_SKILLS_REL = path.join(
8
+ "node_modules",
9
+ "@lovrozagar",
10
+ "grunt",
11
+ ".rulesync",
12
+ "skills",
13
+ );
14
+
15
+ export function listSkillDirNames(skillsRoot) {
16
+ let entries;
17
+ try {
18
+ entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
19
+ } catch (err) {
20
+ if (err && err.code === "ENOENT") return [];
21
+ throw err;
22
+ }
23
+ const out = [];
24
+ for (const ent of entries) {
25
+ if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
26
+ const skillMd = path.join(skillsRoot, ent.name, "SKILL.md");
27
+ try {
28
+ if (!fs.statSync(skillMd).isFile()) continue;
29
+ } catch {
30
+ continue;
31
+ }
32
+ out.push(ent.name);
33
+ }
34
+ out.sort((a, b) => a.localeCompare(b));
35
+ return out;
36
+ }
37
+
38
+ function readSkillMd(skillsRoot, name) {
39
+ const abs = path.join(skillsRoot, name, "SKILL.md");
40
+ try {
41
+ return fs.readFileSync(abs);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /** Names in both trees whose SKILL.md bytes differ. */
48
+ export function findSkillContentConflicts({ workspaceSkillsDir, packagedSkillsDir } = {}) {
49
+ if (!workspaceSkillsDir || !packagedSkillsDir) return [];
50
+ const packaged = listSkillDirNames(packagedSkillsDir);
51
+ if (!packaged.length) return [];
52
+ const conflicts = [];
53
+ for (const name of packaged) {
54
+ const ws = readSkillMd(workspaceSkillsDir, name);
55
+ if (ws == null) continue;
56
+ const pkg = readSkillMd(packagedSkillsDir, name);
57
+ if (pkg == null) continue;
58
+ if (Buffer.compare(ws, pkg) !== 0) {
59
+ conflicts.push({
60
+ name,
61
+ workspacePath: path.join(workspaceSkillsDir, name, "SKILL.md"),
62
+ packagedPath: path.join(packagedSkillsDir, name, "SKILL.md"),
63
+ });
64
+ }
65
+ }
66
+ return conflicts;
67
+ }
68
+
69
+ export function formatSkillConflictWarn(name) {
70
+ return `skill \`${name}\` differs from packaged grunt; re-init overwrites`;
71
+ }