@lovrozagar/grunt 0.5.0 → 0.6.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 +2 -23
- package/.agents/agents/orchestrator.md +5 -45
- package/.agents/skills/ask/SKILL.md +11 -0
- package/.agents/skills/auto/SKILL.md +13 -0
- package/.agents/skills/browser/SKILL.md +4 -7
- package/.agents/skills/clasp/SKILL.md +11 -0
- package/.agents/skills/commit/SKILL.md +3 -1
- package/.agents/skills/commit-push/SKILL.md +2 -0
- package/.agents/skills/commit-push-deploy/SKILL.md +2 -0
- package/.agents/skills/commit-push-release/SKILL.md +2 -0
- package/.agents/skills/explain/SKILL.md +7 -13
- package/.agents/skills/google-workspace/SKILL.md +13 -0
- package/.agents/skills/handoff/SKILL.md +7 -11
- package/.agents/skills/implement-plan/SKILL.md +5 -108
- package/.agents/skills/listen/SKILL.md +15 -0
- package/.agents/skills/pickup/SKILL.md +6 -12
- package/.agents/skills/speak/SKILL.md +13 -0
- package/.agents/skills/tmp/SKILL.md +54 -0
- package/.agents/skills/write-plan/SKILL.md +5 -74
- package/.claude/agents/grunt.md +2 -25
- package/.claude/agents/orchestrator.md +12 -45
- package/.claude/skills/ask/SKILL.md +11 -0
- package/.claude/skills/auto/SKILL.md +13 -0
- package/.claude/skills/browser/SKILL.md +4 -7
- package/.claude/skills/clasp/SKILL.md +11 -0
- package/.claude/skills/commit/SKILL.md +3 -1
- package/.claude/skills/commit-push/SKILL.md +2 -0
- package/.claude/skills/commit-push-deploy/SKILL.md +2 -0
- package/.claude/skills/commit-push-release/SKILL.md +2 -0
- package/.claude/skills/explain/SKILL.md +7 -13
- package/.claude/skills/google-workspace/SKILL.md +13 -0
- package/.claude/skills/handoff/SKILL.md +7 -11
- package/.claude/skills/implement-plan/SKILL.md +5 -108
- package/.claude/skills/listen/SKILL.md +15 -0
- package/.claude/skills/pickup/SKILL.md +6 -12
- package/.claude/skills/speak/SKILL.md +13 -0
- package/.claude/skills/tmp/SKILL.md +54 -0
- package/.claude/skills/write-plan/SKILL.md +5 -74
- package/.codex/agents/grunt.toml +2 -25
- package/.codex/agents/orchestrator.toml +4 -45
- package/.grok/agents/grunt.md +2 -23
- package/.grok/agents/orchestrator.md +4 -43
- package/.grok/global-settings.toml +1 -3
- package/.grok/hooks/orchestrate-parent.js +278 -228
- package/.grok/skills/ask/SKILL.md +11 -0
- package/.grok/skills/auto/SKILL.md +13 -0
- package/.grok/skills/browser/SKILL.md +4 -7
- package/.grok/skills/clasp/SKILL.md +11 -0
- package/.grok/skills/commit/SKILL.md +3 -1
- package/.grok/skills/commit-push/SKILL.md +2 -0
- package/.grok/skills/commit-push-deploy/SKILL.md +2 -0
- package/.grok/skills/commit-push-release/SKILL.md +2 -0
- package/.grok/skills/explain/SKILL.md +7 -13
- package/.grok/skills/google-workspace/SKILL.md +13 -0
- package/.grok/skills/handoff/SKILL.md +7 -11
- package/.grok/skills/implement-plan/SKILL.md +5 -108
- package/.grok/skills/listen/SKILL.md +15 -0
- package/.grok/skills/pickup/SKILL.md +6 -12
- package/.grok/skills/speak/SKILL.md +13 -0
- package/.grok/skills/tmp/SKILL.md +54 -0
- package/.grok/skills/write-plan/SKILL.md +5 -74
- package/.rulesync/hooks.jsonc +1 -1
- package/.rulesync/mcp-policy.jsonc +0 -2
- package/.rulesync/reference/INDEX.md +30 -17
- package/.rulesync/reference/browser.md +13 -6
- package/.rulesync/reference/cascade.md +6 -84
- package/.rulesync/reference/clasp.md +21 -0
- package/.rulesync/reference/google-workspace.md +79 -0
- package/.rulesync/reference/hooks.md +7 -9
- package/.rulesync/reference/implementation-format.md +113 -0
- package/.rulesync/reference/law.md +5 -3
- package/.rulesync/reference/listen.md +80 -0
- package/.rulesync/reference/map.md +39 -31
- package/.rulesync/reference/output.md +3 -41
- package/.rulesync/reference/plan-format.md +14 -12
- package/.rulesync/reference/refs-map.md +12 -5
- package/.rulesync/reference/rtk.md +3 -2
- package/.rulesync/reference/scope.md +37 -0
- package/.rulesync/reference/setup.md +27 -0
- package/.rulesync/reference/skills-map.md +13 -9
- package/.rulesync/reference/speak.md +59 -0
- package/.rulesync/rules/CLAUDE.md +3 -44
- package/.rulesync/rules/overview.md +9 -42
- package/.rulesync/skills/ask/SKILL.md +11 -0
- package/.rulesync/skills/auto/SKILL.md +11 -0
- package/.rulesync/skills/browser/SKILL.md +4 -8
- package/.rulesync/skills/clasp/SKILL.md +9 -0
- package/.rulesync/skills/commit/SKILL.md +3 -1
- package/.rulesync/skills/commit-push/SKILL.md +2 -0
- package/.rulesync/skills/commit-push-deploy/SKILL.md +2 -0
- package/.rulesync/skills/commit-push-release/SKILL.md +2 -0
- package/.rulesync/skills/explain/SKILL.md +7 -14
- package/.rulesync/skills/google-workspace/SKILL.md +11 -0
- package/.rulesync/skills/handoff/SKILL.md +7 -11
- package/.rulesync/skills/implement-plan/SKILL.md +5 -116
- package/.rulesync/skills/listen/SKILL.md +13 -0
- package/.rulesync/skills/pickup/SKILL.md +6 -12
- package/.rulesync/skills/speak/SKILL.md +13 -0
- package/.rulesync/skills/tmp/SKILL.md +54 -0
- package/.rulesync/skills/write-plan/SKILL.md +5 -80
- package/.rulesync/subagents/grunt.md +2 -23
- package/.rulesync/subagents/orchestrator.md +14 -54
- package/AGENTS.md +9 -42
- package/CHANGELOG.md +89 -0
- package/CLAUDE.md +1 -44
- package/README.md +97 -88
- package/cli/grunt.mjs +29 -13
- package/cli/grunt.test.ts +62 -20
- package/cli/init.mjs +218 -31
- package/cli/init.test.ts +669 -46
- package/cli/prompt.mjs +8 -0
- package/cli/prompt.test.ts +14 -0
- package/package.json +13 -1
- package/scripts/browser.mjs +98 -5
- package/scripts/doctor.mjs +82 -1
- package/scripts/emit-gemini.mjs +1 -1
- package/scripts/emit-mcp-policy.mjs +1 -1
- package/scripts/gate-fat-tools.mjs +223 -173
- package/scripts/google-workspace.mjs +713 -0
- package/scripts/grunt-job.mjs +453 -69
- package/scripts/listen.mjs +829 -0
- package/scripts/parse-need.mjs +21 -2
- package/scripts/persist-implementation.mjs +278 -0
- package/scripts/persist-plan.mjs +17 -2
- package/scripts/persist-tmp.mjs +123 -0
- package/scripts/scrub-spawn-prompt.mjs +4 -51
- package/scripts/setup.mjs +746 -0
- package/scripts/speak.mjs +291 -0
- package/.agents/agents/implementer.md +0 -32
- package/.agents/agents/thinker.md +0 -28
- package/.agents/skills/cascade/SKILL.md +0 -26
- package/.agents/skills/parent/SKILL.md +0 -6
- package/.agents/skills/solo/SKILL.md +0 -23
- package/.claude/agents/implementer.md +0 -37
- package/.claude/agents/thinker.md +0 -35
- package/.claude/skills/cascade/SKILL.md +0 -26
- package/.claude/skills/parent/SKILL.md +0 -6
- package/.claude/skills/solo/SKILL.md +0 -23
- package/.codex/agents/implementer.toml +0 -26
- package/.codex/agents/thinker.toml +0 -25
- package/.grok/agents/implementer.md +0 -27
- package/.grok/agents/thinker.md +0 -26
- package/.grok/parent.md +0 -1
- package/.grok/roles/implementer.toml +0 -5
- package/.grok/roles/thinker.toml +0 -5
- package/.grok/skills/cascade/SKILL.md +0 -26
- package/.grok/skills/parent/SKILL.md +0 -6
- package/.grok/skills/solo/SKILL.md +0 -23
- package/.rulesync/skills/cascade/SKILL.md +0 -26
- package/.rulesync/skills/parent/SKILL.md +0 -6
- package/.rulesync/skills/solo/SKILL.md +0 -23
- package/.rulesync/subagents/implementer.md +0 -49
- package/.rulesync/subagents/thinker.md +0 -48
package/scripts/grunt-job.mjs
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/** Deterministic grunt for job: search|exec|test. Exit 2 + FALLBACK on
|
|
2
|
+
/** Deterministic grunt for job: search|exec|test|slice|fetch. Exit 2 + FALLBACK on denylist/HTML-as-exec/unbounded. */
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import crypto from "node:crypto";
|
|
4
5
|
import fs from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
|
|
8
9
|
export const FALLBACK = "FALLBACK";
|
|
9
10
|
export const SHELL_META = /[|&;`$(){}<>\n\r]/;
|
|
11
|
+
export const MAX_FACTS = 6;
|
|
12
|
+
export const MAX_STDOUT = 32 * 1024;
|
|
13
|
+
export const STASH_CAP = 2 * 1024 * 1024;
|
|
14
|
+
export const MAX_HITS = 10_000;
|
|
15
|
+
export const STASH_REL = ".tmp/grunt/stash";
|
|
16
|
+
|
|
10
17
|
const DENY_SEGS = new Set(["node_modules", "dist", ".next", "build", "coverage"]);
|
|
11
18
|
const DENY_LOCK = /\b(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)\b/;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
19
|
+
const FAIL_RE = /\b(FAIL(?:ED)?|ERROR|Error:|Exception|panic|E\d{3,})\b/;
|
|
20
|
+
const STACK_RE = /^\s+at\s/;
|
|
21
|
+
const PASS_RE = /^\s*(✓|√|PASS|ok |passed)\b/i;
|
|
22
|
+
const KNOWN_FLAGS = new Set([
|
|
23
|
+
"--job",
|
|
24
|
+
"--query",
|
|
25
|
+
"--path",
|
|
26
|
+
"--glob",
|
|
27
|
+
"--cwd",
|
|
28
|
+
"--stash",
|
|
29
|
+
"--from",
|
|
30
|
+
"--to",
|
|
31
|
+
]);
|
|
32
|
+
const JOBS = new Set(["search", "exec", "test", "slice", "fetch"]);
|
|
14
33
|
|
|
15
34
|
function takeFlag(argv, i, prefix) {
|
|
16
35
|
const a = argv[i];
|
|
@@ -24,10 +43,18 @@ export function parseArgv(argv) {
|
|
|
24
43
|
let query = "";
|
|
25
44
|
let searchPath = "";
|
|
26
45
|
let cwd = "";
|
|
46
|
+
let stash = "";
|
|
47
|
+
let from = "";
|
|
48
|
+
let to = "";
|
|
27
49
|
const glob = [];
|
|
28
50
|
let unknown = false;
|
|
29
51
|
for (let i = 0; i < argv.length; i++) {
|
|
30
52
|
const a = argv[i];
|
|
53
|
+
const prefix = a.startsWith("--") ? a.split("=")[0] : "";
|
|
54
|
+
if (prefix && !KNOWN_FLAGS.has(prefix)) {
|
|
55
|
+
unknown = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
31
58
|
const jobF = takeFlag(argv, i, "--job");
|
|
32
59
|
if (jobF) {
|
|
33
60
|
job = jobF.value;
|
|
@@ -58,6 +85,24 @@ export function parseArgv(argv) {
|
|
|
58
85
|
i = globF.next;
|
|
59
86
|
continue;
|
|
60
87
|
}
|
|
88
|
+
const stashF = takeFlag(argv, i, "--stash");
|
|
89
|
+
if (stashF) {
|
|
90
|
+
stash = stashF.value;
|
|
91
|
+
i = stashF.next;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const fromF = takeFlag(argv, i, "--from");
|
|
95
|
+
if (fromF) {
|
|
96
|
+
from = fromF.value;
|
|
97
|
+
i = fromF.next;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const toF = takeFlag(argv, i, "--to");
|
|
101
|
+
if (toF) {
|
|
102
|
+
to = toF.value;
|
|
103
|
+
i = toF.next;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
61
106
|
unknown = true;
|
|
62
107
|
}
|
|
63
108
|
return {
|
|
@@ -66,6 +111,9 @@ export function parseArgv(argv) {
|
|
|
66
111
|
path: String(searchPath),
|
|
67
112
|
glob,
|
|
68
113
|
cwd: String(cwd),
|
|
114
|
+
stash: String(stash).trim(),
|
|
115
|
+
from: String(from).trim(),
|
|
116
|
+
to: String(to).trim(),
|
|
69
117
|
unknown,
|
|
70
118
|
};
|
|
71
119
|
}
|
|
@@ -83,8 +131,15 @@ export function resolveJobCwd(cwd, workspaceRoot) {
|
|
|
83
131
|
return resolveUnder(workspaceRoot || process.cwd(), cwd);
|
|
84
132
|
}
|
|
85
133
|
|
|
86
|
-
|
|
87
|
-
|
|
134
|
+
function escapeRe(s) {
|
|
135
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function shouldFallback(job, query, extra = {}) {
|
|
139
|
+
const j = String(job || "").trim().toLowerCase();
|
|
140
|
+
if (!JOBS.has(j)) return true;
|
|
141
|
+
if (j === "slice") return !String(extra.stash || "").trim();
|
|
142
|
+
if (j === "fetch") return !/^https?:\/\//i.test(String(query || "").trim());
|
|
88
143
|
const q = String(query || "");
|
|
89
144
|
if (!q.trim()) return true;
|
|
90
145
|
if (/\b(curl|wget)\b/i.test(q)) return true;
|
|
@@ -98,33 +153,192 @@ export function shouldFallback(job, query) {
|
|
|
98
153
|
return false;
|
|
99
154
|
}
|
|
100
155
|
|
|
101
|
-
function
|
|
102
|
-
|
|
156
|
+
export function isHtml(stdout, stderr) {
|
|
157
|
+
const s = String(stdout || "") + String(stderr || "");
|
|
158
|
+
return /<\s*html\b/i.test(s) || /<\s*!DOCTYPE html/i.test(s);
|
|
103
159
|
}
|
|
104
160
|
|
|
105
161
|
export function looksHtmlOrUnbounded(stdout, stderr) {
|
|
106
162
|
const s = String(stdout || "") + String(stderr || "");
|
|
107
163
|
if (s.length > MAX_STDOUT) return true;
|
|
108
|
-
|
|
109
|
-
return false;
|
|
164
|
+
return isHtml(stdout, stderr);
|
|
110
165
|
}
|
|
111
166
|
|
|
112
|
-
export function
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
167
|
+
export function squeezLines(lines, query) {
|
|
168
|
+
const arr = (lines || []).map((l, i) => ({ l: String(l), i }));
|
|
169
|
+
if (arr.length <= MAX_FACTS) return arr.map((x) => x.l);
|
|
170
|
+
const q = String(query || "").trim();
|
|
171
|
+
let queryRe = null;
|
|
172
|
+
if (q) {
|
|
173
|
+
try {
|
|
174
|
+
queryRe = new RegExp(q);
|
|
175
|
+
} catch {
|
|
176
|
+
queryRe = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function score(item) {
|
|
180
|
+
const s = item.l;
|
|
181
|
+
if (FAIL_RE.test(s) || STACK_RE.test(s)) return 100;
|
|
182
|
+
if (queryRe) {
|
|
183
|
+
const hit = queryRe.test(s);
|
|
184
|
+
queryRe.lastIndex = 0;
|
|
185
|
+
if (hit) return 50;
|
|
186
|
+
} else if (q && s.includes(q)) return 50;
|
|
187
|
+
if (item.i === 0 || item.i === arr.length - 1) return 20;
|
|
188
|
+
if (PASS_RE.test(s)) return 0;
|
|
189
|
+
return 10;
|
|
190
|
+
}
|
|
191
|
+
const ranked = arr.map((x) => ({ ...x, sc: score(x) }));
|
|
192
|
+
const kept = [];
|
|
193
|
+
const used = new Set();
|
|
194
|
+
function take(pred) {
|
|
195
|
+
for (const x of ranked) {
|
|
196
|
+
if (kept.length >= MAX_FACTS) break;
|
|
197
|
+
if (used.has(x.i)) continue;
|
|
198
|
+
if (pred(x)) {
|
|
199
|
+
kept.push(x);
|
|
200
|
+
used.add(x.i);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
take((x) => x.sc >= 100);
|
|
205
|
+
take((x) => x.sc >= 50);
|
|
206
|
+
if (!used.has(0) && kept.length < MAX_FACTS) {
|
|
207
|
+
kept.push(ranked[0]);
|
|
208
|
+
used.add(0);
|
|
209
|
+
}
|
|
210
|
+
const last = arr.length - 1;
|
|
211
|
+
if (!used.has(last) && kept.length < MAX_FACTS) {
|
|
212
|
+
kept.push(ranked[last]);
|
|
213
|
+
used.add(last);
|
|
214
|
+
}
|
|
215
|
+
take((x) => x.sc > 0);
|
|
216
|
+
take(() => true);
|
|
217
|
+
kept.sort((a, b) => a.i - b.i);
|
|
218
|
+
return kept.map((x) => x.l);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function formatFacts({
|
|
222
|
+
kind,
|
|
223
|
+
n = 0,
|
|
224
|
+
facts,
|
|
225
|
+
errors,
|
|
226
|
+
failed = false,
|
|
227
|
+
stash = "",
|
|
228
|
+
truncated = false,
|
|
229
|
+
} = {}) {
|
|
230
|
+
const lines = [];
|
|
231
|
+
const shown = (facts || []).slice(0, MAX_FACTS);
|
|
232
|
+
if (failed) {
|
|
233
|
+
const head =
|
|
234
|
+
kind === "search" ? "Search failed." : "Command failed.";
|
|
235
|
+
lines.push(stash ? `${head.slice(0, -1)}. stash=${stash}` : head);
|
|
115
236
|
for (const e of (errors || []).slice(0, 3)) lines.push(`- ${e}`);
|
|
237
|
+
return lines.slice(0, 8).join("\n") + "\n";
|
|
238
|
+
}
|
|
239
|
+
if (!n) {
|
|
240
|
+
lines.push(kind === "search" ? "No matches." : "No output.");
|
|
241
|
+
return lines.join("\n") + "\n";
|
|
242
|
+
}
|
|
243
|
+
const unit =
|
|
244
|
+
kind === "search" ? (n === 1 ? "match" : "matches") : n === 1 ? "line" : "lines";
|
|
245
|
+
const nLabel = truncated ? `${n}+` : String(n);
|
|
246
|
+
if (stash) {
|
|
247
|
+
lines.push(`${nLabel} ${unit}, ${shown.length} shown. stash=${stash}`);
|
|
248
|
+
} else if (kind === "search") {
|
|
249
|
+
lines.push(n === 1 ? "1 match." : `${n} matches.`);
|
|
116
250
|
} else {
|
|
117
|
-
|
|
251
|
+
lines.push(n === 1 ? "1 line." : `${n} lines.`);
|
|
118
252
|
}
|
|
253
|
+
for (const f of shown) lines.push(`- ${f}`);
|
|
119
254
|
return lines.slice(0, 8).join("\n") + "\n";
|
|
120
255
|
}
|
|
121
256
|
|
|
257
|
+
function stashId() {
|
|
258
|
+
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
259
|
+
const b = crypto.randomBytes(4);
|
|
260
|
+
let s = "";
|
|
261
|
+
for (const x of b) s += alphabet[x % 36];
|
|
262
|
+
return s;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function writeStash(cwd, kind, raw) {
|
|
266
|
+
const root = cwd || process.cwd();
|
|
267
|
+
const dir = path.join(root, ".tmp", "grunt", "stash");
|
|
268
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
269
|
+
const name = `${kind}-${stashId()}.txt`;
|
|
270
|
+
let body = String(raw || "");
|
|
271
|
+
const buf = Buffer.from(body, "utf8");
|
|
272
|
+
if (buf.length > STASH_CAP) body = buf.subarray(0, STASH_CAP).toString("utf8");
|
|
273
|
+
if (!body.endsWith("\n")) body += "\n";
|
|
274
|
+
fs.writeFileSync(path.join(dir, name), body);
|
|
275
|
+
return `${STASH_REL}/${name}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function relStash(abs, cwd) {
|
|
279
|
+
const root = cwd || process.cwd();
|
|
280
|
+
return path.relative(root, abs).replace(/\\/g, "/");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function resolveStashFile(cwd, stash) {
|
|
284
|
+
const id = String(stash || "").trim();
|
|
285
|
+
if (!id || id.includes("..") || path.isAbsolute(id) || /[\\/]/.test(id)) {
|
|
286
|
+
if (id && !id.includes("..") && !path.isAbsolute(id)) {
|
|
287
|
+
const under = String(id).replace(/\\/g, "/");
|
|
288
|
+
if (under.startsWith(".tmp/grunt/stash/") && !under.includes("..")) {
|
|
289
|
+
const abs = resolveUnder(cwd || process.cwd(), under);
|
|
290
|
+
if (abs && fs.existsSync(abs) && fs.statSync(abs).isFile()) return abs;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!id || id.includes("..") || path.isAbsolute(id)) return null;
|
|
294
|
+
}
|
|
295
|
+
const dir = path.join(cwd || process.cwd(), ".tmp", "grunt", "stash");
|
|
296
|
+
if (!fs.existsSync(dir)) return null;
|
|
297
|
+
const names = fs.readdirSync(dir);
|
|
298
|
+
const exact = names.find((n) => n === id || n === `${id}.txt`);
|
|
299
|
+
if (exact) return path.join(dir, exact);
|
|
300
|
+
const hit = names.find((n) => n.includes(id) && n.endsWith(".txt"));
|
|
301
|
+
return hit ? path.join(dir, hit) : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function verdictFromLines({
|
|
305
|
+
kind,
|
|
306
|
+
lines,
|
|
307
|
+
raw,
|
|
308
|
+
query,
|
|
309
|
+
cwd,
|
|
310
|
+
failed = false,
|
|
311
|
+
errors,
|
|
312
|
+
truncated = false,
|
|
313
|
+
}) {
|
|
314
|
+
const n = lines.length;
|
|
315
|
+
const shown = squeezLines(lines, query);
|
|
316
|
+
let stash = "";
|
|
317
|
+
const rawText = raw != null ? String(raw) : lines.join("\n");
|
|
318
|
+
if (n > MAX_FACTS || Buffer.byteLength(rawText, "utf8") > MAX_STDOUT) {
|
|
319
|
+
stash = writeStash(cwd, kind, rawText);
|
|
320
|
+
}
|
|
321
|
+
if (failed) {
|
|
322
|
+
const err = (errors && errors.length ? errors : shown).slice(0, 3);
|
|
323
|
+
return {
|
|
324
|
+
fallback: false,
|
|
325
|
+
text: formatFacts({ kind, failed: true, errors: err, stash, n, facts: shown }),
|
|
326
|
+
code: 0,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
fallback: false,
|
|
331
|
+
text: formatFacts({ kind, n, facts: shown, stash, truncated }),
|
|
332
|
+
code: 0,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
122
336
|
function runCmd(file, args, cwd) {
|
|
123
337
|
return spawnSync(file, args, {
|
|
124
338
|
encoding: "utf8",
|
|
125
339
|
cwd: cwd || process.cwd(),
|
|
126
340
|
timeout: 20_000,
|
|
127
|
-
maxBuffer:
|
|
341
|
+
maxBuffer: STASH_CAP + 4096,
|
|
128
342
|
env: process.env,
|
|
129
343
|
});
|
|
130
344
|
}
|
|
@@ -137,7 +351,6 @@ const SEARCH_SKIP_DIRS = new Set([
|
|
|
137
351
|
"coverage",
|
|
138
352
|
".git",
|
|
139
353
|
]);
|
|
140
|
-
const SEARCH_MAX_HITS = 20;
|
|
141
354
|
const SEARCH_MAX_FILE = 200 * 1024;
|
|
142
355
|
|
|
143
356
|
function parseSearchHits(stdout) {
|
|
@@ -150,22 +363,7 @@ function parseSearchHits(stdout) {
|
|
|
150
363
|
if (!m) return line.slice(0, 160);
|
|
151
364
|
return `${m[1]}:${m[2]} — ${m[3].trim().slice(0, 120)}`;
|
|
152
365
|
});
|
|
153
|
-
return { n: hits.length, facts };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function verdictFromHits({ n, facts }) {
|
|
157
|
-
if (!n) {
|
|
158
|
-
return {
|
|
159
|
-
fallback: false,
|
|
160
|
-
text: formatVerdict({ status: "empty", n: 0, facts: [] }),
|
|
161
|
-
code: 0,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
fallback: false,
|
|
166
|
-
text: formatVerdict({ status: "ok", n, facts }),
|
|
167
|
-
code: 0,
|
|
168
|
-
};
|
|
366
|
+
return { n: hits.length, facts, raw: hits.join("\n") };
|
|
169
367
|
}
|
|
170
368
|
|
|
171
369
|
export function globToRegExp(glob) {
|
|
@@ -211,11 +409,12 @@ function nodeSearch(query, { cwd, start, glob } = {}) {
|
|
|
211
409
|
}
|
|
212
410
|
const facts = [];
|
|
213
411
|
let n = 0;
|
|
412
|
+
let bytes = 0;
|
|
214
413
|
const root = cwd || process.cwd();
|
|
215
414
|
const origin = start || root;
|
|
216
415
|
|
|
217
416
|
function searchFile(abs) {
|
|
218
|
-
if (n >=
|
|
417
|
+
if (n >= MAX_HITS || bytes >= STASH_CAP) return;
|
|
219
418
|
let st;
|
|
220
419
|
try {
|
|
221
420
|
st = fs.statSync(abs);
|
|
@@ -239,15 +438,15 @@ function nodeSearch(query, { cwd, start, glob } = {}) {
|
|
|
239
438
|
if (re) re.lastIndex = 0;
|
|
240
439
|
if (!hit) continue;
|
|
241
440
|
n += 1;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (n >=
|
|
441
|
+
const fact = `${rel}:${i + 1} — ${line.trim().slice(0, 120)}`;
|
|
442
|
+
facts.push(fact);
|
|
443
|
+
bytes += Buffer.byteLength(fact, "utf8") + 1;
|
|
444
|
+
if (n >= MAX_HITS || bytes >= STASH_CAP) return;
|
|
246
445
|
}
|
|
247
446
|
}
|
|
248
447
|
|
|
249
448
|
function walk(dir) {
|
|
250
|
-
if (n >=
|
|
449
|
+
if (n >= MAX_HITS || bytes >= STASH_CAP) return;
|
|
251
450
|
let ents;
|
|
252
451
|
try {
|
|
253
452
|
ents = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -255,7 +454,7 @@ function nodeSearch(query, { cwd, start, glob } = {}) {
|
|
|
255
454
|
return;
|
|
256
455
|
}
|
|
257
456
|
for (const ent of ents) {
|
|
258
|
-
if (n >=
|
|
457
|
+
if (n >= MAX_HITS || bytes >= STASH_CAP) return;
|
|
259
458
|
if (SEARCH_SKIP_DIRS.has(ent.name)) continue;
|
|
260
459
|
const abs = path.join(dir, ent.name);
|
|
261
460
|
if (ent.isDirectory()) {
|
|
@@ -271,11 +470,11 @@ function nodeSearch(query, { cwd, start, glob } = {}) {
|
|
|
271
470
|
try {
|
|
272
471
|
st = fs.statSync(origin);
|
|
273
472
|
} catch {
|
|
274
|
-
return { n: 0, facts: [] };
|
|
473
|
+
return { n: 0, facts: [], truncated: false };
|
|
275
474
|
}
|
|
276
475
|
if (st.isFile()) searchFile(origin);
|
|
277
476
|
else walk(origin);
|
|
278
|
-
return { n, facts };
|
|
477
|
+
return { n, facts, truncated: n >= MAX_HITS || bytes >= STASH_CAP };
|
|
279
478
|
}
|
|
280
479
|
|
|
281
480
|
export function runSearch(query, { cwd, path: searchPath, glob } = {}) {
|
|
@@ -289,7 +488,7 @@ export function runSearch(query, { cwd, path: searchPath, glob } = {}) {
|
|
|
289
488
|
const rgArgs = [
|
|
290
489
|
"-n",
|
|
291
490
|
"--max-count",
|
|
292
|
-
|
|
491
|
+
String(MAX_HITS),
|
|
293
492
|
"--max-filesize",
|
|
294
493
|
"200K",
|
|
295
494
|
"-g",
|
|
@@ -313,14 +512,27 @@ export function runSearch(query, { cwd, path: searchPath, glob } = {}) {
|
|
|
313
512
|
if (!(r.error && r.error.code === "ENOENT")) {
|
|
314
513
|
const stdout = String(r.stdout || "");
|
|
315
514
|
const stderr = String(r.stderr || "");
|
|
316
|
-
if (
|
|
515
|
+
if (isHtml(stdout, stderr)) {
|
|
317
516
|
return { fallback: true, text: FALLBACK + "\n", code: 2 };
|
|
318
517
|
}
|
|
319
518
|
if (r.status === 1) {
|
|
320
|
-
return
|
|
519
|
+
return {
|
|
520
|
+
fallback: false,
|
|
521
|
+
text: formatFacts({ kind: "search", n: 0, facts: [] }),
|
|
522
|
+
code: 0,
|
|
523
|
+
};
|
|
321
524
|
}
|
|
322
525
|
if (r.status === 0) {
|
|
323
|
-
|
|
526
|
+
const parsed = parseSearchHits(stdout);
|
|
527
|
+
const truncated = parsed.n >= MAX_HITS;
|
|
528
|
+
return verdictFromLines({
|
|
529
|
+
kind: "search",
|
|
530
|
+
lines: parsed.facts,
|
|
531
|
+
raw: parsed.raw,
|
|
532
|
+
query,
|
|
533
|
+
cwd: root,
|
|
534
|
+
truncated,
|
|
535
|
+
});
|
|
324
536
|
}
|
|
325
537
|
if (r.status != null) {
|
|
326
538
|
const errors = stderr
|
|
@@ -329,9 +541,9 @@ export function runSearch(query, { cwd, path: searchPath, glob } = {}) {
|
|
|
329
541
|
.filter(Boolean);
|
|
330
542
|
return {
|
|
331
543
|
fallback: false,
|
|
332
|
-
text:
|
|
333
|
-
|
|
334
|
-
|
|
544
|
+
text: formatFacts({
|
|
545
|
+
kind: "search",
|
|
546
|
+
failed: true,
|
|
335
547
|
errors: errors.length ? errors : [`rg exit ${r.status}`],
|
|
336
548
|
}),
|
|
337
549
|
code: 0,
|
|
@@ -339,7 +551,15 @@ export function runSearch(query, { cwd, path: searchPath, glob } = {}) {
|
|
|
339
551
|
}
|
|
340
552
|
}
|
|
341
553
|
|
|
342
|
-
|
|
554
|
+
const node = nodeSearch(query, { cwd: root, start, glob: globs });
|
|
555
|
+
return verdictFromLines({
|
|
556
|
+
kind: "search",
|
|
557
|
+
lines: node.facts,
|
|
558
|
+
raw: node.facts.join("\n"),
|
|
559
|
+
query,
|
|
560
|
+
cwd: root,
|
|
561
|
+
truncated: node.truncated,
|
|
562
|
+
});
|
|
343
563
|
}
|
|
344
564
|
|
|
345
565
|
function alreadyRtk(query) {
|
|
@@ -368,9 +588,9 @@ export function runExec(query, { cwd } = {}) {
|
|
|
368
588
|
if (r.error && r.error.code === "ENOENT") {
|
|
369
589
|
return {
|
|
370
590
|
fallback: false,
|
|
371
|
-
text:
|
|
372
|
-
|
|
373
|
-
|
|
591
|
+
text: formatFacts({
|
|
592
|
+
kind: "exec",
|
|
593
|
+
failed: true,
|
|
374
594
|
errors: [String(r.error.code || "ENOENT")],
|
|
375
595
|
}),
|
|
376
596
|
code: 0,
|
|
@@ -378,7 +598,7 @@ export function runExec(query, { cwd } = {}) {
|
|
|
378
598
|
}
|
|
379
599
|
const stdout = String(r.stdout || "");
|
|
380
600
|
const stderr = String(r.stderr || "");
|
|
381
|
-
if (
|
|
601
|
+
if (isHtml(stdout, stderr)) {
|
|
382
602
|
return { fallback: true, text: FALLBACK + "\n", code: 2 };
|
|
383
603
|
}
|
|
384
604
|
if (r.status !== 0) {
|
|
@@ -393,15 +613,17 @@ export function runExec(query, { cwd } = {}) {
|
|
|
393
613
|
.filter(Boolean);
|
|
394
614
|
errors.push(...fromOut);
|
|
395
615
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
616
|
+
const raw = [stdout, stderr].filter(Boolean).join("\n");
|
|
617
|
+
const lines = (errors.length ? errors : [`exit ${r.status}`]);
|
|
618
|
+
return verdictFromLines({
|
|
619
|
+
kind: "exec",
|
|
620
|
+
lines,
|
|
621
|
+
raw,
|
|
622
|
+
query: q,
|
|
623
|
+
cwd,
|
|
624
|
+
failed: true,
|
|
625
|
+
errors: lines,
|
|
626
|
+
});
|
|
405
627
|
}
|
|
406
628
|
const lines = stdout
|
|
407
629
|
.split(/\n/)
|
|
@@ -410,31 +632,190 @@ export function runExec(query, { cwd } = {}) {
|
|
|
410
632
|
if (!lines.length && !stderr.trim()) {
|
|
411
633
|
return {
|
|
412
634
|
fallback: false,
|
|
413
|
-
text:
|
|
635
|
+
text: formatFacts({ kind: "exec", n: 0, facts: [] }),
|
|
636
|
+
code: 0,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const body = lines.length ? lines : [stderr.trim()];
|
|
640
|
+
return verdictFromLines({
|
|
641
|
+
kind: "exec",
|
|
642
|
+
lines: body,
|
|
643
|
+
raw: stdout || stderr,
|
|
644
|
+
query: q,
|
|
645
|
+
cwd,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function runSlice({ stash, path: filter, from, to, cwd } = {}) {
|
|
650
|
+
const abs = resolveStashFile(cwd, stash);
|
|
651
|
+
if (!abs) {
|
|
652
|
+
return {
|
|
653
|
+
fallback: false,
|
|
654
|
+
text: formatFacts({
|
|
655
|
+
kind: "exec",
|
|
656
|
+
failed: true,
|
|
657
|
+
errors: ["missing stash"],
|
|
658
|
+
}),
|
|
659
|
+
code: 0,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
let text;
|
|
663
|
+
try {
|
|
664
|
+
text = fs.readFileSync(abs, "utf8");
|
|
665
|
+
} catch {
|
|
666
|
+
return {
|
|
667
|
+
fallback: false,
|
|
668
|
+
text: formatFacts({
|
|
669
|
+
kind: "exec",
|
|
670
|
+
failed: true,
|
|
671
|
+
errors: ["missing stash"],
|
|
672
|
+
}),
|
|
673
|
+
code: 0,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
if (text.includes("\0")) {
|
|
677
|
+
return {
|
|
678
|
+
fallback: false,
|
|
679
|
+
text: formatFacts({
|
|
680
|
+
kind: "exec",
|
|
681
|
+
failed: true,
|
|
682
|
+
errors: ["binary stash"],
|
|
683
|
+
}),
|
|
414
684
|
code: 0,
|
|
415
685
|
};
|
|
416
686
|
}
|
|
417
|
-
const
|
|
687
|
+
const numbered = text.split(/\n/).map((l, i) => ({ n: i + 1, l }));
|
|
688
|
+
let picked = numbered;
|
|
689
|
+
const a = from !== "" && from != null ? Number(from) : null;
|
|
690
|
+
const b = to !== "" && to != null ? Number(to) : null;
|
|
691
|
+
if (Number.isFinite(a) || Number.isFinite(b)) {
|
|
692
|
+
const start = Number.isFinite(a) ? a : 1;
|
|
693
|
+
const end = Number.isFinite(b) ? b : numbered.length;
|
|
694
|
+
picked = numbered.filter((x) => x.n >= start && x.n <= end);
|
|
695
|
+
}
|
|
696
|
+
if (filter) picked = picked.filter((x) => x.l.includes(String(filter)));
|
|
697
|
+
const factLines = picked.map(
|
|
698
|
+
(x) => `stash:${x.n} — ${x.l.trim().slice(0, 120)}`,
|
|
699
|
+
);
|
|
700
|
+
const rel = relStash(abs, cwd);
|
|
701
|
+
if (picked.length <= MAX_FACTS) {
|
|
702
|
+
return {
|
|
703
|
+
fallback: false,
|
|
704
|
+
text: formatFacts({ kind: "exec", n: picked.length, facts: factLines }),
|
|
705
|
+
code: 0,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const shown = squeezLines(factLines, filter);
|
|
418
709
|
return {
|
|
419
710
|
fallback: false,
|
|
420
|
-
text:
|
|
421
|
-
|
|
422
|
-
n:
|
|
423
|
-
facts,
|
|
711
|
+
text: formatFacts({
|
|
712
|
+
kind: "exec",
|
|
713
|
+
n: picked.length,
|
|
714
|
+
facts: shown,
|
|
715
|
+
stash: rel,
|
|
424
716
|
}),
|
|
425
717
|
code: 0,
|
|
426
718
|
};
|
|
427
719
|
}
|
|
428
720
|
|
|
721
|
+
function stripHtml(html) {
|
|
722
|
+
const s = String(html || "");
|
|
723
|
+
const title = (s.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [])[1] || "";
|
|
724
|
+
const text = s
|
|
725
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
726
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
727
|
+
.replace(/<[^>]+>/g, " ")
|
|
728
|
+
.replace(/ /g, " ")
|
|
729
|
+
.replace(/&/g, "&")
|
|
730
|
+
.replace(/</g, "<")
|
|
731
|
+
.replace(/>/g, ">")
|
|
732
|
+
.replace(/\s+/g, " ")
|
|
733
|
+
.trim();
|
|
734
|
+
return { title: String(title).replace(/\s+/g, " ").trim(), text };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function runFetch(query, { cwd } = {}) {
|
|
738
|
+
const url = String(query || "").trim();
|
|
739
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
740
|
+
return { fallback: true, text: FALLBACK + "\n", code: 2 };
|
|
741
|
+
}
|
|
742
|
+
const script = `
|
|
743
|
+
const u = process.argv[1];
|
|
744
|
+
const cap = ${STASH_CAP};
|
|
745
|
+
fetch(u, { redirect: "follow" })
|
|
746
|
+
.then(async (r) => {
|
|
747
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
748
|
+
process.stdout.write(buf.subarray(0, cap));
|
|
749
|
+
})
|
|
750
|
+
.catch((e) => {
|
|
751
|
+
process.stderr.write(String(e && e.message ? e.message : e));
|
|
752
|
+
process.exit(1);
|
|
753
|
+
});
|
|
754
|
+
`;
|
|
755
|
+
const r = spawnSync(process.execPath, ["-e", script, url], {
|
|
756
|
+
encoding: "utf8",
|
|
757
|
+
cwd: cwd || process.cwd(),
|
|
758
|
+
timeout: 20_000,
|
|
759
|
+
maxBuffer: STASH_CAP + 4096,
|
|
760
|
+
env: process.env,
|
|
761
|
+
});
|
|
762
|
+
if (r.status !== 0) {
|
|
763
|
+
const err = String(r.stderr || r.stdout || "fetch failed")
|
|
764
|
+
.split(/\n/)
|
|
765
|
+
.map((l) => l.trim())
|
|
766
|
+
.filter(Boolean);
|
|
767
|
+
return {
|
|
768
|
+
fallback: false,
|
|
769
|
+
text: formatFacts({
|
|
770
|
+
kind: "exec",
|
|
771
|
+
failed: true,
|
|
772
|
+
errors: err.length ? err.slice(0, 3) : ["fetch failed"],
|
|
773
|
+
}),
|
|
774
|
+
code: 0,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const raw = String(r.stdout || "");
|
|
778
|
+
const { title, text } = stripHtml(raw);
|
|
779
|
+
const lines = [];
|
|
780
|
+
if (title) lines.push(title);
|
|
781
|
+
for (const part of text.split(/(?<=\.)\s+/)) {
|
|
782
|
+
const p = part.trim();
|
|
783
|
+
if (p) lines.push(p.slice(0, 160));
|
|
784
|
+
}
|
|
785
|
+
const body = lines.length ? lines : ["(empty)"];
|
|
786
|
+
return verdictFromLines({
|
|
787
|
+
kind: "exec",
|
|
788
|
+
lines: body,
|
|
789
|
+
raw,
|
|
790
|
+
query: url,
|
|
791
|
+
cwd,
|
|
792
|
+
truncated: Buffer.byteLength(raw, "utf8") >= STASH_CAP,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
429
796
|
function fallbackResult() {
|
|
430
797
|
return { fallback: true, text: FALLBACK + "\n", code: 2 };
|
|
431
798
|
}
|
|
432
799
|
|
|
433
|
-
export function runJob({
|
|
800
|
+
export function runJob({
|
|
801
|
+
job,
|
|
802
|
+
query,
|
|
803
|
+
cwd,
|
|
804
|
+
path: searchPath,
|
|
805
|
+
glob,
|
|
806
|
+
stash,
|
|
807
|
+
from,
|
|
808
|
+
to,
|
|
809
|
+
unknown,
|
|
810
|
+
} = {}) {
|
|
434
811
|
if (unknown) return fallbackResult();
|
|
435
|
-
if (shouldFallback(job, query)) return fallbackResult();
|
|
812
|
+
if (shouldFallback(job, query, { stash })) return fallbackResult();
|
|
436
813
|
const root = cwd || process.cwd();
|
|
437
814
|
if (job === "search") return runSearch(query, { cwd: root, path: searchPath, glob });
|
|
815
|
+
if (job === "slice") {
|
|
816
|
+
return runSlice({ stash, path: searchPath, from, to, cwd: root });
|
|
817
|
+
}
|
|
818
|
+
if (job === "fetch") return runFetch(query, { cwd: root });
|
|
438
819
|
return runExec(query, { cwd: root });
|
|
439
820
|
}
|
|
440
821
|
|
|
@@ -457,6 +838,9 @@ function main() {
|
|
|
457
838
|
cwd,
|
|
458
839
|
path: parsed.path,
|
|
459
840
|
glob: parsed.glob,
|
|
841
|
+
stash: parsed.stash,
|
|
842
|
+
from: parsed.from,
|
|
843
|
+
to: parsed.to,
|
|
460
844
|
});
|
|
461
845
|
process.stdout.write(result.text);
|
|
462
846
|
return result.code;
|