@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
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env node
2
+ /** Deterministic grunt for job: search|exec|test. Exit 2 + FALLBACK on web/HTML/unbounded/denylist. */
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { logTelemetry } from "./telemetry.mjs";
8
+
9
+ export const FALLBACK = "FALLBACK";
10
+ export const SHELL_META = /[|&;`$(){}<>\n\r]/;
11
+ const DENY_SEGS = new Set(["node_modules", "dist", ".next", "build", "coverage"]);
12
+ const DENY_LOCK = /\b(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)\b/;
13
+ const MAX_FACTS = 6;
14
+ const MAX_STDOUT = 32 * 1024;
15
+
16
+ function takeFlag(argv, i, prefix) {
17
+ const a = argv[i];
18
+ if (a === prefix) return { value: String(argv[i + 1] || ""), next: i + 1 };
19
+ if (a.startsWith(prefix + "=")) return { value: a.slice(prefix.length + 1), next: i };
20
+ return null;
21
+ }
22
+
23
+ export function parseArgv(argv) {
24
+ let job = "";
25
+ let query = "";
26
+ let searchPath = "";
27
+ let cwd = "";
28
+ const glob = [];
29
+ let unknown = false;
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const a = argv[i];
32
+ const jobF = takeFlag(argv, i, "--job");
33
+ if (jobF) {
34
+ job = jobF.value;
35
+ i = jobF.next;
36
+ continue;
37
+ }
38
+ const queryF = takeFlag(argv, i, "--query");
39
+ if (queryF) {
40
+ query = queryF.value;
41
+ i = queryF.next;
42
+ continue;
43
+ }
44
+ const pathF = takeFlag(argv, i, "--path");
45
+ if (pathF) {
46
+ searchPath = pathF.value;
47
+ i = pathF.next;
48
+ continue;
49
+ }
50
+ const cwdF = takeFlag(argv, i, "--cwd");
51
+ if (cwdF) {
52
+ cwd = cwdF.value;
53
+ i = cwdF.next;
54
+ continue;
55
+ }
56
+ const globF = takeFlag(argv, i, "--glob");
57
+ if (globF) {
58
+ if (globF.value) glob.push(globF.value);
59
+ i = globF.next;
60
+ continue;
61
+ }
62
+ unknown = true;
63
+ }
64
+ return {
65
+ job: String(job).trim().toLowerCase(),
66
+ query: String(query),
67
+ path: String(searchPath),
68
+ glob,
69
+ cwd: String(cwd),
70
+ unknown,
71
+ };
72
+ }
73
+
74
+ export function resolveUnder(root, rel) {
75
+ const base = path.resolve(root || process.cwd());
76
+ if (rel == null || rel === "") return base;
77
+ const abs = path.isAbsolute(rel) ? path.resolve(rel) : path.resolve(base, rel);
78
+ const prefix = base.endsWith(path.sep) ? base : base + path.sep;
79
+ if (abs !== base && !abs.startsWith(prefix)) return null;
80
+ return abs;
81
+ }
82
+
83
+ export function resolveJobCwd(cwd, workspaceRoot) {
84
+ return resolveUnder(workspaceRoot || process.cwd(), cwd);
85
+ }
86
+
87
+ export function shouldFallback(job, query) {
88
+ if (job !== "search" && job !== "exec" && job !== "test") return true;
89
+ const q = String(query || "");
90
+ if (!q.trim()) return true;
91
+ if (/\b(curl|wget)\b/i.test(q)) return true;
92
+ if (/<\s*html\b/i.test(q) || /<\s*!DOCTYPE html/i.test(q)) return true;
93
+ if (DENY_LOCK.test(q)) return true;
94
+ if (/(^|[\s/])\.git(\/|$)/.test(q.replace(/\\/g, "/"))) return true;
95
+ for (const seg of DENY_SEGS) {
96
+ const re = new RegExp(`(^|[\\/\\s'"\`])${escapeRe(seg)}([\\/\\s'"\`]|$)`);
97
+ if (re.test(q)) return true;
98
+ }
99
+ return false;
100
+ }
101
+
102
+ function escapeRe(s) {
103
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
104
+ }
105
+
106
+ export function looksHtmlOrUnbounded(stdout, stderr) {
107
+ const s = String(stdout || "") + String(stderr || "");
108
+ if (s.length > MAX_STDOUT) return true;
109
+ if (/<\s*html\b/i.test(s) || /<\s*!DOCTYPE html/i.test(s)) return true;
110
+ return false;
111
+ }
112
+
113
+ export function formatVerdict({ status, n, facts, errors }) {
114
+ const lines = [`verdict: ${status}`, `n: ${n}`];
115
+ if (status === "fail") {
116
+ for (const e of (errors || []).slice(0, 3)) lines.push(`- ${e}`);
117
+ } else {
118
+ for (const f of (facts || []).slice(0, MAX_FACTS)) lines.push(`- ${f}`);
119
+ }
120
+ return lines.slice(0, 8).join("\n") + "\n";
121
+ }
122
+
123
+ function runCmd(file, args, cwd) {
124
+ return spawnSync(file, args, {
125
+ encoding: "utf8",
126
+ cwd: cwd || process.cwd(),
127
+ timeout: 20_000,
128
+ maxBuffer: MAX_STDOUT + 4096,
129
+ env: process.env,
130
+ });
131
+ }
132
+
133
+ const SEARCH_SKIP_DIRS = new Set([
134
+ "node_modules",
135
+ "dist",
136
+ ".next",
137
+ "build",
138
+ "coverage",
139
+ ".git",
140
+ ]);
141
+ const SEARCH_MAX_HITS = 20;
142
+ const SEARCH_MAX_FILE = 200 * 1024;
143
+
144
+ function parseSearchHits(stdout) {
145
+ const hits = String(stdout || "")
146
+ .split(/\n/)
147
+ .map((l) => l.trim())
148
+ .filter(Boolean);
149
+ const facts = hits.map((line) => {
150
+ const m = line.match(/^([^:]+):(\d+):(.*)$/);
151
+ if (!m) return line.slice(0, 160);
152
+ return `${m[1]}:${m[2]} — ${m[3].trim().slice(0, 120)}`;
153
+ });
154
+ return { n: hits.length, facts };
155
+ }
156
+
157
+ function verdictFromHits({ n, facts }) {
158
+ if (!n) {
159
+ return {
160
+ fallback: false,
161
+ text: formatVerdict({ status: "empty", n: 0, facts: [] }),
162
+ code: 0,
163
+ };
164
+ }
165
+ return {
166
+ fallback: false,
167
+ text: formatVerdict({ status: "ok", n, facts }),
168
+ code: 0,
169
+ };
170
+ }
171
+
172
+ export function globToRegExp(glob) {
173
+ const g = String(glob || "").replace(/\\/g, "/");
174
+ let s = "";
175
+ for (let i = 0; i < g.length; i++) {
176
+ const c = g[i];
177
+ if (c === "*" && g[i + 1] === "*") {
178
+ if (g[i + 2] === "/") {
179
+ s += "(?:.*/)?";
180
+ i += 2;
181
+ } else {
182
+ s += ".*";
183
+ i += 1;
184
+ }
185
+ } else if (c === "*") s += "[^/]*";
186
+ else if (c === "?") s += "[^/]";
187
+ else s += escapeRe(c);
188
+ }
189
+ return new RegExp("^" + s + "$");
190
+ }
191
+
192
+ export function globMatch(rel, pattern) {
193
+ const n = String(rel || "").replace(/\\/g, "/");
194
+ let p = String(pattern || "").replace(/\\/g, "/");
195
+ if (!p) return false;
196
+ if (!p.includes("/")) p = "**/" + p;
197
+ return globToRegExp(p).test(n) || globToRegExp(String(pattern).replace(/\\/g, "/")).test(n);
198
+ }
199
+
200
+ function fileMatchesGlobs(rel, globs) {
201
+ if (!globs || !globs.length) return true;
202
+ const base = path.posix.basename(rel);
203
+ return globs.some((g) => globMatch(rel, g) || globMatch(base, g));
204
+ }
205
+
206
+ function nodeSearch(query, { cwd, start, glob } = {}) {
207
+ let re;
208
+ try {
209
+ re = new RegExp(query);
210
+ } catch {
211
+ re = null;
212
+ }
213
+ const facts = [];
214
+ let n = 0;
215
+ const root = cwd || process.cwd();
216
+ const origin = start || root;
217
+
218
+ function searchFile(abs) {
219
+ if (n >= SEARCH_MAX_HITS) return;
220
+ let st;
221
+ try {
222
+ st = fs.statSync(abs);
223
+ } catch {
224
+ return;
225
+ }
226
+ if (!st.isFile() || st.size > SEARCH_MAX_FILE) return;
227
+ const rel = path.relative(root, abs).replace(/\\/g, "/");
228
+ if (!fileMatchesGlobs(rel, glob)) return;
229
+ let text;
230
+ try {
231
+ text = fs.readFileSync(abs, "utf8");
232
+ } catch {
233
+ return;
234
+ }
235
+ if (text.includes("\0")) return;
236
+ const lines = text.split(/\n/);
237
+ for (let i = 0; i < lines.length; i++) {
238
+ const line = lines[i];
239
+ const hit = re ? re.test(line) : line.includes(query);
240
+ if (re) re.lastIndex = 0;
241
+ if (!hit) continue;
242
+ n += 1;
243
+ if (facts.length < MAX_FACTS) {
244
+ facts.push(`${rel}:${i + 1} — ${line.trim().slice(0, 120)}`);
245
+ }
246
+ if (n >= SEARCH_MAX_HITS) return;
247
+ }
248
+ }
249
+
250
+ function walk(dir) {
251
+ if (n >= SEARCH_MAX_HITS) return;
252
+ let ents;
253
+ try {
254
+ ents = fs.readdirSync(dir, { withFileTypes: true });
255
+ } catch {
256
+ return;
257
+ }
258
+ for (const ent of ents) {
259
+ if (n >= SEARCH_MAX_HITS) return;
260
+ if (SEARCH_SKIP_DIRS.has(ent.name)) continue;
261
+ const abs = path.join(dir, ent.name);
262
+ if (ent.isDirectory()) {
263
+ walk(abs);
264
+ continue;
265
+ }
266
+ if (!ent.isFile()) continue;
267
+ searchFile(abs);
268
+ }
269
+ }
270
+
271
+ let st;
272
+ try {
273
+ st = fs.statSync(origin);
274
+ } catch {
275
+ return { n: 0, facts: [] };
276
+ }
277
+ if (st.isFile()) searchFile(origin);
278
+ else walk(origin);
279
+ return { n, facts };
280
+ }
281
+
282
+ export function runSearch(query, { cwd, path: searchPath, glob } = {}) {
283
+ const root = cwd || process.cwd();
284
+ const start = searchPath ? resolveUnder(root, searchPath) : root;
285
+ if (!start) {
286
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
287
+ }
288
+ const globs = Array.isArray(glob) ? glob.filter(Boolean) : glob ? [String(glob)] : [];
289
+ const relStart = path.relative(root, start) || ".";
290
+ const rgArgs = [
291
+ "-n",
292
+ "--max-count",
293
+ "20",
294
+ "--max-filesize",
295
+ "200K",
296
+ "-g",
297
+ "!node_modules",
298
+ "-g",
299
+ "!dist",
300
+ "-g",
301
+ "!coverage",
302
+ "-g",
303
+ "!.next",
304
+ "-g",
305
+ "!build",
306
+ "-g",
307
+ "!.git",
308
+ ];
309
+ for (const g of globs) {
310
+ rgArgs.push("-g", g);
311
+ }
312
+ rgArgs.push("-e", query, relStart);
313
+ const r = runCmd("rg", rgArgs, root);
314
+ if (!(r.error && r.error.code === "ENOENT")) {
315
+ const stdout = String(r.stdout || "");
316
+ const stderr = String(r.stderr || "");
317
+ if (looksHtmlOrUnbounded(stdout, stderr)) {
318
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
319
+ }
320
+ if (r.status === 1) {
321
+ return verdictFromHits({ n: 0, facts: [] });
322
+ }
323
+ if (r.status === 0) {
324
+ return verdictFromHits(parseSearchHits(stdout));
325
+ }
326
+ if (r.status != null) {
327
+ const errors = stderr
328
+ .split(/\n/)
329
+ .map((l) => l.trim())
330
+ .filter(Boolean);
331
+ return {
332
+ fallback: false,
333
+ text: formatVerdict({
334
+ status: "fail",
335
+ n: 0,
336
+ errors: errors.length ? errors : [`rg exit ${r.status}`],
337
+ }),
338
+ code: 0,
339
+ };
340
+ }
341
+ }
342
+
343
+ return verdictFromHits(nodeSearch(query, { cwd: root, start, glob: globs }));
344
+ }
345
+
346
+ function alreadyRtk(query) {
347
+ const first = String(query || "").trim().split(/\s+/, 1)[0] || "";
348
+ return first === "rtk" || first.endsWith("/rtk");
349
+ }
350
+
351
+ export function runExec(query, { cwd } = {}) {
352
+ const q = String(query).trim();
353
+ if (SHELL_META.test(q)) {
354
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
355
+ }
356
+ const parts = q.split(/\s+/).filter(Boolean);
357
+ if (!parts.length) {
358
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
359
+ }
360
+ let r;
361
+ if (alreadyRtk(q)) {
362
+ r = runCmd(parts[0], parts.slice(1), cwd);
363
+ } else {
364
+ r = runCmd("rtk", parts, cwd);
365
+ if (r.error && r.error.code === "ENOENT") {
366
+ r = runCmd(parts[0], parts.slice(1), cwd);
367
+ }
368
+ }
369
+ if (r.error && r.error.code === "ENOENT") {
370
+ return {
371
+ fallback: false,
372
+ text: formatVerdict({
373
+ status: "fail",
374
+ n: 0,
375
+ errors: [String(r.error.code || "ENOENT")],
376
+ }),
377
+ code: 0,
378
+ };
379
+ }
380
+ const stdout = String(r.stdout || "");
381
+ const stderr = String(r.stderr || "");
382
+ if (looksHtmlOrUnbounded(stdout, stderr)) {
383
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
384
+ }
385
+ if (r.status !== 0) {
386
+ const errors = stderr
387
+ .split(/\n/)
388
+ .map((l) => l.trim())
389
+ .filter(Boolean);
390
+ if (!errors.length) {
391
+ const fromOut = stdout
392
+ .split(/\n/)
393
+ .map((l) => l.trim())
394
+ .filter(Boolean);
395
+ errors.push(...fromOut);
396
+ }
397
+ return {
398
+ fallback: false,
399
+ text: formatVerdict({
400
+ status: "fail",
401
+ n: 0,
402
+ errors: errors.length ? errors : [`exit ${r.status}`],
403
+ }),
404
+ code: 0,
405
+ };
406
+ }
407
+ const lines = stdout
408
+ .split(/\n/)
409
+ .map((l) => l.trim())
410
+ .filter(Boolean);
411
+ if (!lines.length && !stderr.trim()) {
412
+ return {
413
+ fallback: false,
414
+ text: formatVerdict({ status: "empty", n: 0, facts: [] }),
415
+ code: 0,
416
+ };
417
+ }
418
+ const facts = (lines.length ? lines : [stderr.trim()]).slice(0, MAX_FACTS);
419
+ return {
420
+ fallback: false,
421
+ text: formatVerdict({
422
+ status: "ok",
423
+ n: lines.length || facts.length,
424
+ facts,
425
+ }),
426
+ code: 0,
427
+ };
428
+ }
429
+
430
+ function fallbackResult() {
431
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
432
+ }
433
+
434
+ export function runJob({ job, query, cwd, path: searchPath, glob, unknown } = {}) {
435
+ if (unknown) return fallbackResult();
436
+ if (shouldFallback(job, query)) return fallbackResult();
437
+ const root = cwd || process.cwd();
438
+ if (job === "search") return runSearch(query, { cwd: root, path: searchPath, glob });
439
+ return runExec(query, { cwd: root });
440
+ }
441
+
442
+ function main() {
443
+ try {
444
+ const parsed = parseArgv(process.argv.slice(2));
445
+ if (parsed.unknown) {
446
+ logTelemetry("grunt-job", { fallback: true, query: parsed.query }, process.cwd());
447
+ process.stdout.write(FALLBACK + "\n");
448
+ return 2;
449
+ }
450
+ const ws = process.cwd();
451
+ const cwd = parsed.cwd ? resolveJobCwd(parsed.cwd, ws) : ws;
452
+ if (!cwd) {
453
+ logTelemetry("grunt-job", { fallback: true, query: parsed.query }, ws);
454
+ process.stdout.write(FALLBACK + "\n");
455
+ return 2;
456
+ }
457
+ const result = runJob({
458
+ job: parsed.job,
459
+ query: parsed.query,
460
+ cwd,
461
+ path: parsed.path,
462
+ glob: parsed.glob,
463
+ });
464
+ logTelemetry(
465
+ "grunt-job",
466
+ { job: parsed.job, fallback: Boolean(result.fallback), query: parsed.query },
467
+ ws,
468
+ );
469
+ process.stdout.write(result.text);
470
+ return result.code;
471
+ } catch {
472
+ logTelemetry("grunt-job", { fallback: true }, process.cwd());
473
+ process.stdout.write(FALLBACK + "\n");
474
+ return 2;
475
+ }
476
+ }
477
+
478
+ const thisFile = fileURLToPath(import.meta.url);
479
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
480
+ if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
481
+ process.exit(main());
482
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /** Extract/validate a JSON `need:` array. Fail → stderr + exit 1. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ export const NEED_JOBS = new Set(["search", "exec", "web", "test"]);
8
+
9
+ export function parseNeed(text) {
10
+ const s = String(text ?? "");
11
+ const m = s.match(/^\s*need:\s*(\[[\s\S]*\])\s*$/m);
12
+ if (!m) {
13
+ return { ok: false, error: "no json need array" };
14
+ }
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(m[1]);
18
+ } catch {
19
+ return { ok: false, error: "invalid json" };
20
+ }
21
+ if (!Array.isArray(parsed) || parsed.length < 1) {
22
+ return { ok: false, error: "need array empty" };
23
+ }
24
+ const jobs = [];
25
+ for (const item of parsed) {
26
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
27
+ return { ok: false, error: "invalid job item" };
28
+ }
29
+ const job = String(item.job ?? "").trim();
30
+ const query = String(item.query ?? "").trim();
31
+ if (!NEED_JOBS.has(job) || !query) {
32
+ return { ok: false, error: "invalid job or query" };
33
+ }
34
+ const row = { job, query };
35
+ if (item.path != null && String(item.path).trim()) {
36
+ row.path = String(item.path).trim();
37
+ }
38
+ if (item.cwd != null && String(item.cwd).trim()) {
39
+ row.cwd = String(item.cwd).trim();
40
+ }
41
+ if (item.glob != null && item.glob !== "") {
42
+ row.glob = Array.isArray(item.glob)
43
+ ? item.glob.map((g) => String(g))
44
+ : [String(item.glob)];
45
+ }
46
+ jobs.push(row);
47
+ }
48
+ return { ok: true, jobs };
49
+ }
50
+
51
+ function main() {
52
+ try {
53
+ const arg = process.argv[2];
54
+ const text =
55
+ arg && arg !== "-"
56
+ ? fs.readFileSync(arg, "utf8")
57
+ : fs.readFileSync(0, "utf8");
58
+ const result = parseNeed(text);
59
+ if (!result.ok) {
60
+ process.stderr.write(result.error + "\n");
61
+ return 1;
62
+ }
63
+ process.stdout.write(JSON.stringify(result.jobs) + "\n");
64
+ return 0;
65
+ } catch {
66
+ process.stderr.write("parse-need failed\n");
67
+ return 1;
68
+ }
69
+ }
70
+
71
+ const thisFile = fileURLToPath(import.meta.url);
72
+ const invoked = process.argv[1] ? path.resolve(process.argv[1]) : "";
73
+ if (invoked === thisFile || import.meta.url === pathToFileURL(invoked).href) {
74
+ process.exit(main());
75
+ }