harnery 0.7.0 → 0.8.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/README.md +1 -1
- package/dist/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/callers.d.ts.map +1 -1
- package/dist/commands/callers.js +69 -17
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +46 -2
- package/dist/commands/grep.d.ts +44 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +246 -45
- package/dist/core/agents/cli.js +48 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +10 -3
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -0
- package/dist/lib/tools/ripgrep.d.ts +61 -0
- package/dist/lib/tools/ripgrep.d.ts.map +1 -0
- package/dist/lib/tools/ripgrep.js +217 -0
- package/package.json +1 -1
- package/src/commander.ts +9 -0
- package/src/commands/callers.ts +90 -26
- package/src/commands/doctor.ts +50 -2
- package/src/commands/grep.ts +277 -46
- package/src/core/agents/cli.ts +55 -0
- package/src/core/agents/state/heartbeat-projector.ts +11 -3
- package/src/core/config.ts +24 -0
- package/src/lib/tools/ripgrep.ts +244 -0
package/dist/commands/grep.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolveSearchEngine } from "../lib/tools/ripgrep.js";
|
|
2
3
|
/**
|
|
3
|
-
* `grep`: monorepo-aware code search.
|
|
4
|
-
*
|
|
4
|
+
* `grep`: monorepo-aware code search. Prefers ripgrep (`rg`) when it is on
|
|
5
|
+
* PATH and falls back to GNU `grep -rn` transparently — both engines are
|
|
6
|
+
* driven with equivalent flags and their output is parsed into the same
|
|
7
|
+
* envelope, so results are identical (pinned by tests/unit/grep-engine.test.ts).
|
|
8
|
+
* Smart default excludes (skip dist/.next/node_modules/.git/...), repo
|
|
5
9
|
* scoping (`--repo <name>` or `--all-repos`), and language presets.
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
11
|
+
* Engine selection: `HARNERY_GREP_ENGINE=rg|grep` forces one; otherwise `rg`
|
|
12
|
+
* is resolved (managed install, PATH, or opt-in auto-provision; see resolveEngine) and used when available. Repos are searched in
|
|
13
|
+
* parallel, and in `--all-repos` mode the parent scan prunes submodule
|
|
14
|
+
* directories so each match is attributed to exactly one repo (previously the
|
|
15
|
+
* parent scan descended into submodules, double-scanning and double-reporting
|
|
16
|
+
* every submodule match).
|
|
17
|
+
*
|
|
18
|
+
* Default behavior matches extended-regex semantics (`grep -E` / ripgrep's
|
|
19
|
+
* default). Use `-F` / `--literal` to pin to literal-string mode. Output is
|
|
20
|
+
* line-oriented `file:line:content` in TTY mode, `{rows, total, truncated}`
|
|
21
|
+
* in --json mode. Matches are sorted (file, then line) for stable output
|
|
22
|
+
* across runs and engines; with `-C <n>` context, engine order is kept so
|
|
23
|
+
* context groups stay adjacent.
|
|
10
24
|
*/
|
|
11
25
|
const DEFAULT_EXCLUDE_DIRS = [
|
|
12
26
|
".git",
|
|
@@ -47,7 +61,8 @@ const LANG_GLOBS = {
|
|
|
47
61
|
export function registerGrepCommand(program, emit, context) {
|
|
48
62
|
program
|
|
49
63
|
.command("grep <pattern> [paths...]")
|
|
50
|
-
.description("Monorepo-aware code search
|
|
64
|
+
.description("Monorepo-aware code search (ripgrep when available, GNU grep fallback). " +
|
|
65
|
+
"Skips dist/.next/node_modules/.git/... by default. " +
|
|
51
66
|
"Use --repo, --all-repos, --lang for scoping. Regex by default; -F for literal.")
|
|
52
67
|
.option("--repo <name>", "Scope to one submodule (`.` = parent repo root)")
|
|
53
68
|
.option("--all-repos", "Search parent + every submodule")
|
|
@@ -56,6 +71,8 @@ export function registerGrepCommand(program, emit, context) {
|
|
|
56
71
|
.option("-w, --whole-word", "Match whole words only")
|
|
57
72
|
.option("-F, --literal", "Treat <pattern> as a literal string (no regex)")
|
|
58
73
|
.option("-l, --files-only", "Only print file names containing a match")
|
|
74
|
+
.option("--files", "Filename search: treat <pattern> as a filename glob and list matching files " +
|
|
75
|
+
"(rg --files when available, POSIX find fallback)")
|
|
59
76
|
.option("-c, --count", "Print match count per file (suppresses content)")
|
|
60
77
|
.option("-C, --context <n>", "Print N lines of context around each match", "0")
|
|
61
78
|
.option("--max-count <n>", "Stop after N matches per file")
|
|
@@ -83,33 +100,72 @@ export function registerGrepCommand(program, emit, context) {
|
|
|
83
100
|
function collect(value, prev) {
|
|
84
101
|
return [...prev, value];
|
|
85
102
|
}
|
|
86
|
-
|
|
103
|
+
/** Exported for tests (not part of the package exports map). */
|
|
104
|
+
export async function runGrep(pattern, paths, opts, context) {
|
|
87
105
|
if (!pattern)
|
|
88
106
|
throw new Error("pattern required");
|
|
107
|
+
if (opts.files) {
|
|
108
|
+
// Filename mode lists files by name glob; content-search flags make no
|
|
109
|
+
// sense here — reject loudly rather than silently ignoring them.
|
|
110
|
+
const incompatible = [
|
|
111
|
+
[opts.lang, "--lang"],
|
|
112
|
+
[opts.count, "-c/--count"],
|
|
113
|
+
[opts.wholeWord, "-w/--whole-word"],
|
|
114
|
+
[opts.literal, "-F/--literal"],
|
|
115
|
+
[opts.maxCount, "--max-count"],
|
|
116
|
+
[opts.include?.length, "--include"],
|
|
117
|
+
[Number.parseInt(opts.context ?? "0", 10) > 0 ? true : undefined, "-C/--context"],
|
|
118
|
+
];
|
|
119
|
+
for (const [set, flag] of incompatible) {
|
|
120
|
+
if (set)
|
|
121
|
+
throw new Error(`${flag} does not apply to --files (filename glob) mode`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
89
124
|
const started = Date.now();
|
|
125
|
+
const { engine, rgBin } = await resolveSearchEngine("grep");
|
|
90
126
|
const repos = resolveRepos(opts, context);
|
|
91
127
|
const limit = opts.limit ? Number.parseInt(opts.limit, 10) : Number.POSITIVE_INFINITY;
|
|
128
|
+
const contextN = Number.parseInt(opts.context ?? "0", 10);
|
|
129
|
+
const sortable = !(Number.isFinite(contextN) && contextN > 0);
|
|
130
|
+
// Host-injected default excludes (generated mirrors, vendored trees, ...)
|
|
131
|
+
// ride the same --no-default-excludes gate as the built-in list.
|
|
132
|
+
const hostExcludeDirs = opts.noDefaultExcludes ? [] : (context?.grepExcludeDirs ?? []);
|
|
133
|
+
// All repos are searched concurrently; each is capped at the global limit
|
|
134
|
+
// (a single repo can never contribute more), then the global budget is
|
|
135
|
+
// applied in repo order below so `--limit` semantics stay deterministic.
|
|
136
|
+
const perRepo = await Promise.all(repos.map((repo) => {
|
|
137
|
+
// In --all-repos mode the parent scan prunes submodule dirs — each
|
|
138
|
+
// submodule gets its own scoped scan, so descending from the parent
|
|
139
|
+
// would double-scan and double-report every submodule match. This
|
|
140
|
+
// pruning is correctness (one repo owns each match), so it applies
|
|
141
|
+
// even when --no-default-excludes is set.
|
|
142
|
+
const dedupeDirs = opts.allRepos && repo.name === "parent" ? (context?.submodules ?? []) : [];
|
|
143
|
+
const extraDirs = [...hostExcludeDirs, ...dedupeDirs];
|
|
144
|
+
return runGrepInRepo(pattern, paths, opts, repo.cwd, repo.name, limit, engine, rgBin, extraDirs);
|
|
145
|
+
}));
|
|
92
146
|
const allRepoResults = [];
|
|
93
147
|
let totalMatches = 0;
|
|
94
148
|
const filesSeen = new Set();
|
|
95
149
|
let truncated = false;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
allRepoResults.push({ name: repo.name, cwd: repo.cwd, matches: [], truncated: true });
|
|
105
|
-
break;
|
|
150
|
+
let budget = limit;
|
|
151
|
+
for (let i = 0; i < repos.length; i++) {
|
|
152
|
+
const repo = repos[i];
|
|
153
|
+
if (!repo)
|
|
154
|
+
continue;
|
|
155
|
+
const collected = perRepo[i] ?? [];
|
|
156
|
+
if (sortable) {
|
|
157
|
+
collected.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
|
|
106
158
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
159
|
+
const engineCapped = Number.isFinite(limit) && collected.length >= limit;
|
|
160
|
+
const take = Number.isFinite(budget)
|
|
161
|
+
? Math.min(collected.length, Math.max(0, budget))
|
|
162
|
+
: collected.length;
|
|
163
|
+
const matches = collected.slice(0, take);
|
|
164
|
+
const repoTruncated = engineCapped || collected.length > take;
|
|
165
|
+
if (repoTruncated)
|
|
111
166
|
truncated = true;
|
|
112
|
-
|
|
167
|
+
if (Number.isFinite(budget))
|
|
168
|
+
budget -= take;
|
|
113
169
|
totalMatches += matches.length;
|
|
114
170
|
for (const m of matches)
|
|
115
171
|
filesSeen.add(`${repo.name}/${m.file}`);
|
|
@@ -117,7 +173,8 @@ async function runGrep(pattern, paths, opts, context) {
|
|
|
117
173
|
}
|
|
118
174
|
return {
|
|
119
175
|
pattern,
|
|
120
|
-
mode: opts.literal ? "literal" : "regex",
|
|
176
|
+
mode: opts.files ? "files" : opts.literal ? "literal" : "regex",
|
|
177
|
+
engine,
|
|
121
178
|
repos: allRepoResults,
|
|
122
179
|
total_matches: totalMatches,
|
|
123
180
|
total_files: filesSeen.size,
|
|
@@ -155,8 +212,32 @@ function resolveRepos(opts, context) {
|
|
|
155
212
|
}
|
|
156
213
|
return [{ name: "cwd", cwd: process.cwd() }];
|
|
157
214
|
}
|
|
158
|
-
async function runGrepInRepo(pattern, paths, opts, cwd, repoName, limit) {
|
|
159
|
-
|
|
215
|
+
async function runGrepInRepo(pattern, paths, opts, cwd, repoName, limit, engine, rgBin, extraExcludeDirs) {
|
|
216
|
+
// Filename mode: `rg --files` when available; POSIX `find` otherwise (GNU
|
|
217
|
+
// grep has no list-by-name mode). Content mode: rg / grep as selected.
|
|
218
|
+
const [bin, args] = opts.files
|
|
219
|
+
? engine === "rg"
|
|
220
|
+
? [rgBin, buildRgFilesArgs(pattern, paths, opts, extraExcludeDirs)]
|
|
221
|
+
: ["find", buildFindArgs(pattern, paths, opts, extraExcludeDirs)]
|
|
222
|
+
: engine === "rg"
|
|
223
|
+
? [rgBin, buildRgArgs(pattern, paths, opts, extraExcludeDirs)]
|
|
224
|
+
: ["grep", buildGrepArgs(pattern, paths, opts, extraExcludeDirs)];
|
|
225
|
+
let matches = await execSearch(bin, args, cwd, limit);
|
|
226
|
+
// -c mode: GNU grep prints a `path:0` row for every searched file; ripgrep
|
|
227
|
+
// omits zero-count rows. Filter zeros on both engines so output matches.
|
|
228
|
+
if (opts.count)
|
|
229
|
+
matches = matches.filter((m) => m.line > 0);
|
|
230
|
+
return matches.map((m) => ({ ...m, repo: repoName }));
|
|
231
|
+
}
|
|
232
|
+
function resolveLangGlobs(opts) {
|
|
233
|
+
const langGlobs = opts.lang ? LANG_GLOBS[opts.lang] : undefined;
|
|
234
|
+
if (opts.lang && !langGlobs) {
|
|
235
|
+
throw new Error(`unknown --lang "${opts.lang}". Valid: ${Object.keys(LANG_GLOBS).join(", ")}`);
|
|
236
|
+
}
|
|
237
|
+
return langGlobs;
|
|
238
|
+
}
|
|
239
|
+
function buildGrepArgs(pattern, paths, opts, extraExcludeDirs) {
|
|
240
|
+
const args = ["-rn", "-H", "--color=never", "-I"]; // -I: skip binary files
|
|
160
241
|
if (opts.ignoreCase)
|
|
161
242
|
args.push("-i");
|
|
162
243
|
if (opts.wholeWord)
|
|
@@ -183,12 +264,11 @@ async function runGrepInRepo(pattern, paths, opts, cwd, repoName, limit) {
|
|
|
183
264
|
for (const f of DEFAULT_EXCLUDE_FILES)
|
|
184
265
|
args.push(`--exclude=${f}`);
|
|
185
266
|
}
|
|
267
|
+
for (const d of extraExcludeDirs)
|
|
268
|
+
args.push(`--exclude-dir=${d}`);
|
|
186
269
|
for (const e of opts.exclude ?? [])
|
|
187
270
|
args.push(`--exclude=${e}`);
|
|
188
|
-
const langGlobs = opts
|
|
189
|
-
if (opts.lang && !langGlobs) {
|
|
190
|
-
throw new Error(`unknown --lang "${opts.lang}". Valid: ${Object.keys(LANG_GLOBS).join(", ")}`);
|
|
191
|
-
}
|
|
271
|
+
const langGlobs = resolveLangGlobs(opts);
|
|
192
272
|
if (langGlobs)
|
|
193
273
|
for (const g of langGlobs)
|
|
194
274
|
args.push(`--include=${g}`);
|
|
@@ -199,12 +279,125 @@ async function runGrepInRepo(pattern, paths, opts, cwd, repoName, limit) {
|
|
|
199
279
|
args.push(...paths);
|
|
200
280
|
else
|
|
201
281
|
args.push(".");
|
|
202
|
-
|
|
203
|
-
return matches.map((m) => ({ ...m, repo: repoName }));
|
|
282
|
+
return args;
|
|
204
283
|
}
|
|
205
|
-
function
|
|
284
|
+
function buildRgArgs(pattern, paths, opts, extraExcludeDirs) {
|
|
285
|
+
// --hidden --no-ignore: match GNU grep's semantics (search dotdirs, ignore
|
|
286
|
+
// .gitignore) so the only filters are the explicit exclude lists.
|
|
287
|
+
// --no-config: a user's ripgrep config file must not skew results.
|
|
288
|
+
// --with-filename: rg drops the file prefix for a single explicit file arg,
|
|
289
|
+
// which would break the shared parser.
|
|
290
|
+
const args = [
|
|
291
|
+
"-n",
|
|
292
|
+
"--no-heading",
|
|
293
|
+
"--with-filename",
|
|
294
|
+
"--color=never",
|
|
295
|
+
"--no-config",
|
|
296
|
+
"--hidden",
|
|
297
|
+
"--no-ignore",
|
|
298
|
+
];
|
|
299
|
+
if (opts.ignoreCase)
|
|
300
|
+
args.push("-i");
|
|
301
|
+
if (opts.wholeWord)
|
|
302
|
+
args.push("-w");
|
|
303
|
+
if (opts.literal)
|
|
304
|
+
args.push("-F");
|
|
305
|
+
if (opts.filesOnly)
|
|
306
|
+
args.push("-l");
|
|
307
|
+
if (opts.count)
|
|
308
|
+
args.push("-c");
|
|
309
|
+
const contextN = Number.parseInt(opts.context ?? "0", 10);
|
|
310
|
+
if (Number.isFinite(contextN) && contextN > 0)
|
|
311
|
+
args.push(`-C${contextN}`);
|
|
312
|
+
if (opts.maxCount) {
|
|
313
|
+
const n = Number.parseInt(opts.maxCount, 10);
|
|
314
|
+
if (Number.isFinite(n) && n > 0)
|
|
315
|
+
args.push(`-m${n}`);
|
|
316
|
+
}
|
|
317
|
+
// Gitignore-style globs: a bare name matches (and prunes) at any depth,
|
|
318
|
+
// mirroring grep's --exclude-dir / --exclude basename semantics. ORDER
|
|
319
|
+
// MATTERS: rg globs are last-match-wins, so positives (includes/lang) go
|
|
320
|
+
// first and negatives (excludes) last — otherwise `--include '*.md'` would
|
|
321
|
+
// re-include a .md file inside an excluded node_modules/. grep's
|
|
322
|
+
// --exclude-dir always beats --include, so this keeps the engines aligned.
|
|
323
|
+
const langGlobs = resolveLangGlobs(opts);
|
|
324
|
+
if (langGlobs)
|
|
325
|
+
for (const g of langGlobs)
|
|
326
|
+
args.push(`--glob=${g}`);
|
|
327
|
+
for (const g of opts.include ?? [])
|
|
328
|
+
args.push(`--glob=${g}`);
|
|
329
|
+
if (!opts.noDefaultExcludes) {
|
|
330
|
+
for (const d of DEFAULT_EXCLUDE_DIRS)
|
|
331
|
+
args.push(`--glob=!${d}`);
|
|
332
|
+
for (const f of DEFAULT_EXCLUDE_FILES)
|
|
333
|
+
args.push(`--glob=!${f}`);
|
|
334
|
+
}
|
|
335
|
+
for (const d of extraExcludeDirs)
|
|
336
|
+
args.push(`--glob=!${d}`);
|
|
337
|
+
for (const e of opts.exclude ?? [])
|
|
338
|
+
args.push(`--glob=!${e}`);
|
|
339
|
+
args.push("--", pattern);
|
|
340
|
+
if (paths.length > 0)
|
|
341
|
+
args.push(...paths);
|
|
342
|
+
else
|
|
343
|
+
args.push(".");
|
|
344
|
+
return args;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Filename mode via ripgrep: `--files` lists files; a single positive glob
|
|
348
|
+
* acts as a whitelist, negative globs prune. `--iglob` gives -i semantics.
|
|
349
|
+
*/
|
|
350
|
+
function buildRgFilesArgs(pattern, paths, opts, extraExcludeDirs) {
|
|
351
|
+
const args = ["--files", "--hidden", "--no-ignore", "--no-config", "--color=never"];
|
|
352
|
+
// Positive pattern FIRST, negatives last (rg globs are last-match-wins;
|
|
353
|
+
// excludes must beat the pattern — see the ordering note in buildRgArgs).
|
|
354
|
+
args.push(`${opts.ignoreCase ? "--iglob" : "--glob"}=${pattern}`);
|
|
355
|
+
if (!opts.noDefaultExcludes) {
|
|
356
|
+
for (const d of DEFAULT_EXCLUDE_DIRS)
|
|
357
|
+
args.push(`--glob=!${d}`);
|
|
358
|
+
for (const f of DEFAULT_EXCLUDE_FILES)
|
|
359
|
+
args.push(`--glob=!${f}`);
|
|
360
|
+
}
|
|
361
|
+
for (const d of extraExcludeDirs)
|
|
362
|
+
args.push(`--glob=!${d}`);
|
|
363
|
+
for (const e of opts.exclude ?? [])
|
|
364
|
+
args.push(`--glob=!${e}`);
|
|
365
|
+
if (paths.length > 0)
|
|
366
|
+
args.push(...paths);
|
|
367
|
+
else
|
|
368
|
+
args.push(".");
|
|
369
|
+
return args;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Filename mode via POSIX find (the no-ripgrep fallback):
|
|
373
|
+
* find <paths> ( -name d1 -o -name d2 ... ) -prune -o -type f -name <glob> [! -name <ex>]... -print
|
|
374
|
+
* Sticks to POSIX operators (`(`, `-o`, `!`) so BSD/macOS find behaves the same.
|
|
375
|
+
*/
|
|
376
|
+
function buildFindArgs(pattern, paths, opts, extraExcludeDirs) {
|
|
377
|
+
const args = paths.length > 0 ? [...paths] : ["."];
|
|
378
|
+
const pruneDirs = [...(opts.noDefaultExcludes ? [] : DEFAULT_EXCLUDE_DIRS), ...extraExcludeDirs];
|
|
379
|
+
if (pruneDirs.length > 0) {
|
|
380
|
+
args.push("(");
|
|
381
|
+
pruneDirs.forEach((d, i) => {
|
|
382
|
+
if (i > 0)
|
|
383
|
+
args.push("-o");
|
|
384
|
+
args.push("-name", d);
|
|
385
|
+
});
|
|
386
|
+
args.push(")", "-prune", "-o");
|
|
387
|
+
}
|
|
388
|
+
args.push("-type", "f", opts.ignoreCase ? "-iname" : "-name", pattern);
|
|
389
|
+
const fileExcludes = [
|
|
390
|
+
...(opts.noDefaultExcludes ? [] : DEFAULT_EXCLUDE_FILES),
|
|
391
|
+
...(opts.exclude ?? []),
|
|
392
|
+
];
|
|
393
|
+
for (const e of fileExcludes)
|
|
394
|
+
args.push("!", "-name", e);
|
|
395
|
+
args.push("-print");
|
|
396
|
+
return args;
|
|
397
|
+
}
|
|
398
|
+
function execSearch(bin, args, cwd, limit) {
|
|
206
399
|
return new Promise((resolveP, reject) => {
|
|
207
|
-
const proc = spawn(
|
|
400
|
+
const proc = spawn(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
208
401
|
const matches = [];
|
|
209
402
|
let buffer = "";
|
|
210
403
|
let stderr = "";
|
|
@@ -242,31 +435,34 @@ function execGrep(args, cwd, limit) {
|
|
|
242
435
|
if (parsed)
|
|
243
436
|
matches.push(parsed);
|
|
244
437
|
}
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
438
|
+
// Exit 1 is "no matches" on both engines: normal, not an error. Exit 2
|
|
439
|
+
// with matches collected means a partial failure (an unreadable file
|
|
440
|
+
// mid-walk); surface the results rather than throwing them away.
|
|
441
|
+
if (code !== null && code !== 0 && code !== 1 && !truncated && matches.length === 0) {
|
|
442
|
+
reject(new Error(`${bin} exited ${code}: ${stderr.trim() || "(no stderr)"}`));
|
|
248
443
|
return;
|
|
249
444
|
}
|
|
250
445
|
resolveP(matches);
|
|
251
446
|
});
|
|
252
447
|
});
|
|
253
448
|
}
|
|
254
|
-
/** Parse `path:line:content` from grep -n output. Returns null for ambiguous (no line number) lines. */
|
|
449
|
+
/** Parse `path:line:content` from grep/rg -n output. Returns null for ambiguous (no line number) lines. */
|
|
255
450
|
function parseGrepLine(line) {
|
|
256
451
|
// First colon ends path; second colon ends line number (if numeric).
|
|
257
452
|
const firstColon = line.indexOf(":");
|
|
258
453
|
if (firstColon < 0) {
|
|
259
454
|
// -l mode: just a path. Encode as line=0, text="".
|
|
260
|
-
return { file: line, line: 0, text: "" };
|
|
455
|
+
return { file: normalizeFile(line), line: 0, text: "" };
|
|
261
456
|
}
|
|
262
457
|
const after = line.slice(firstColon + 1);
|
|
263
458
|
const secondColon = after.indexOf(":");
|
|
264
459
|
if (secondColon < 0) {
|
|
265
460
|
// -c mode: `path:count`.
|
|
266
461
|
const count = Number.parseInt(after, 10);
|
|
267
|
-
if (Number.isFinite(count))
|
|
268
|
-
return { file: line.slice(0, firstColon), line: count, text: "" };
|
|
269
|
-
|
|
462
|
+
if (Number.isFinite(count)) {
|
|
463
|
+
return { file: normalizeFile(line.slice(0, firstColon)), line: count, text: "" };
|
|
464
|
+
}
|
|
465
|
+
return { file: normalizeFile(line.slice(0, firstColon)), line: 0, text: after };
|
|
270
466
|
}
|
|
271
467
|
const lineNum = Number.parseInt(after.slice(0, secondColon), 10);
|
|
272
468
|
if (!Number.isFinite(lineNum)) {
|
|
@@ -274,11 +470,15 @@ function parseGrepLine(line) {
|
|
|
274
470
|
return null;
|
|
275
471
|
}
|
|
276
472
|
return {
|
|
277
|
-
file: line.slice(0, firstColon),
|
|
473
|
+
file: normalizeFile(line.slice(0, firstColon)),
|
|
278
474
|
line: lineNum,
|
|
279
475
|
text: after.slice(secondColon + 1),
|
|
280
476
|
};
|
|
281
477
|
}
|
|
478
|
+
/** Both engines may emit a leading `./` for the default path; strip it so output is engine-identical. */
|
|
479
|
+
function normalizeFile(file) {
|
|
480
|
+
return file.startsWith("./") ? file.slice(2) : file;
|
|
481
|
+
}
|
|
282
482
|
function renderResult(r, opts) {
|
|
283
483
|
const lines = [];
|
|
284
484
|
const showRepoHeader = r.repos.length > 1;
|
|
@@ -290,7 +490,7 @@ function renderResult(r, opts) {
|
|
|
290
490
|
lines.push(`── ${repo.name} (${repo.matches.length} match${repo.matches.length === 1 ? "" : "es"}) ──`);
|
|
291
491
|
}
|
|
292
492
|
for (const m of repo.matches) {
|
|
293
|
-
if (opts.filesOnly) {
|
|
493
|
+
if (opts.filesOnly || opts.files) {
|
|
294
494
|
lines.push(m.file);
|
|
295
495
|
}
|
|
296
496
|
else if (opts.count) {
|
|
@@ -305,13 +505,14 @@ function renderResult(r, opts) {
|
|
|
305
505
|
}
|
|
306
506
|
}
|
|
307
507
|
if (r.total_matches === 0) {
|
|
308
|
-
|
|
508
|
+
const modeTag = r.mode === "literal" ? " (literal)" : r.mode === "files" ? " (files)" : "";
|
|
509
|
+
lines.push(`(no matches for /${r.pattern}/${modeTag})`);
|
|
309
510
|
}
|
|
310
511
|
else if (r.repos.length > 1 || r.truncated) {
|
|
311
512
|
lines.push("");
|
|
312
513
|
const summary = `${r.total_matches} match${r.total_matches === 1 ? "" : "es"} across ${r.total_files} file${r.total_files === 1 ? "" : "s"}`;
|
|
313
514
|
const tail = r.truncated ? " (truncated; use --limit)" : "";
|
|
314
|
-
lines.push(`${summary}${tail} (${r.elapsed_ms}ms)`);
|
|
515
|
+
lines.push(`${summary}${tail} (${r.elapsed_ms}ms, ${r.engine})`);
|
|
315
516
|
}
|
|
316
517
|
return lines.join("\n");
|
|
317
518
|
}
|
package/dist/core/agents/cli.js
CHANGED
|
@@ -150,6 +150,32 @@ async function handleProject(root, rest) {
|
|
|
150
150
|
}
|
|
151
151
|
return 0;
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Append a canonical `claim.release` event for a path dropped from an owner's
|
|
155
|
+
* files_touched. The path is canonicalized to repo-relative (matching the
|
|
156
|
+
* projector's normalization) so the subtraction matches on replay regardless
|
|
157
|
+
* of the form the caller passed. Soft-fails: a failed emit must never break
|
|
158
|
+
* the release/kill flow — the file mutation already happened.
|
|
159
|
+
*/
|
|
160
|
+
async function emitClaimRelease(root, owner, hb, path, reason) {
|
|
161
|
+
try {
|
|
162
|
+
const { emit } = await import("./events/emit.js");
|
|
163
|
+
const canonical = path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
|
|
164
|
+
const platform = hb.platform;
|
|
165
|
+
const harness = platform === "cursor" ? "cursor" : platform === "codex" ? "codex" : "claude-code";
|
|
166
|
+
emit(root, {
|
|
167
|
+
event_type: "claim.release",
|
|
168
|
+
instance_id: owner,
|
|
169
|
+
session_id: hb.session_id ?? owner,
|
|
170
|
+
harness,
|
|
171
|
+
source: "agent-coord",
|
|
172
|
+
data: { path: canonical, reason },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
/* soft-fail: never break the caller */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
153
179
|
async function handleStateAction(root, action, rest) {
|
|
154
180
|
const writer = await import("./state/heartbeat-writer.js");
|
|
155
181
|
const [owner, ...args] = rest;
|
|
@@ -189,14 +215,36 @@ async function handleStateAction(root, action, rest) {
|
|
|
189
215
|
process.stderr.write("agent-coord release-claim: missing <path>\n");
|
|
190
216
|
return 2;
|
|
191
217
|
}
|
|
218
|
+
const before = writer.readHeartbeat(root, owner);
|
|
192
219
|
const hb = writer.releaseClaim(root, owner, path);
|
|
193
220
|
if (!hb)
|
|
194
221
|
return 1;
|
|
222
|
+
// Durability: the projector rebuilds files_touched by replaying the
|
|
223
|
+
// permanent Edit/Write events, so a file-only release is silently
|
|
224
|
+
// reverted by the next full replay. Emitting claim.release puts the
|
|
225
|
+
// subtraction into the stream so every future replay honors it. Only
|
|
226
|
+
// emit when the release actually removed a held path (idempotent
|
|
227
|
+
// re-releases stay quiet).
|
|
228
|
+
const heldBefore = before?.files_touched?.length ?? 0;
|
|
229
|
+
const heldAfter = hb.files_touched?.length ?? 0;
|
|
230
|
+
if (heldBefore > heldAfter) {
|
|
231
|
+
await emitClaimRelease(root, owner, before ?? hb, path, "explicit");
|
|
232
|
+
}
|
|
195
233
|
process.stdout.write(`${JSON.stringify({ instance_id: owner, files_touched: hb.files_touched })}\n`);
|
|
196
234
|
return 0;
|
|
197
235
|
}
|
|
198
236
|
case "kill-heartbeat": {
|
|
237
|
+
// Read held claims BEFORE the unlink so they can be released durably —
|
|
238
|
+
// killing only the file leaves the claims resurrectable from the
|
|
239
|
+
// permanent Edit/Write events on the next full replay (observed: a
|
|
240
|
+
// 6-day-dead agent's claims returning after its heartbeat was killed).
|
|
241
|
+
const before = writer.readHeartbeat(root, owner);
|
|
199
242
|
const ok = writer.killHeartbeat(root, owner);
|
|
243
|
+
if (ok && before) {
|
|
244
|
+
for (const held of before.files_touched ?? []) {
|
|
245
|
+
await emitClaimRelease(root, owner, before, held, "heal");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
200
248
|
process.stdout.write(`${JSON.stringify({ instance_id: owner, removed: ok })}\n`);
|
|
201
249
|
return ok ? 0 : 1;
|
|
202
250
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heartbeat-projector.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,OAAO,EAAE;QACP,cAAc,EAAE,CAAC,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,cAAc,EAAE,GAChC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAE,CAoD9D;
|
|
1
|
+
{"version":3,"file":"heartbeat-projector.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/heartbeat-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,OAAO,EAAE;QACP,cAAc,EAAE,CAAC,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,cAAc,EAAE,GAChC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAE,CAoD9D;AAsWD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3E"}
|
|
@@ -38,7 +38,7 @@ export function projectHeartbeats(coordRoot, events) {
|
|
|
38
38
|
continue;
|
|
39
39
|
perOwner[ev.instance_id] = existing ?? seed(ev, coordRoot);
|
|
40
40
|
}
|
|
41
|
-
apply(perOwner[ev.instance_id], ev);
|
|
41
|
+
apply(perOwner[ev.instance_id], ev, coordRoot);
|
|
42
42
|
}
|
|
43
43
|
const written = [];
|
|
44
44
|
for (const [instance_id, hb] of Object.entries(perOwner)) {
|
|
@@ -101,7 +101,7 @@ function seed(ev, coordRoot) {
|
|
|
101
101
|
}
|
|
102
102
|
return hb;
|
|
103
103
|
}
|
|
104
|
-
function apply(hb, ev) {
|
|
104
|
+
function apply(hb, ev, coordRoot) {
|
|
105
105
|
hb.last_heartbeat = ev.ts;
|
|
106
106
|
hb.last_event_id = ev.event_id;
|
|
107
107
|
hb.events_applied += 1;
|
|
@@ -238,7 +238,14 @@ function apply(hb, ev) {
|
|
|
238
238
|
case "claim.release": {
|
|
239
239
|
const path = pickStr(d, "path");
|
|
240
240
|
if (path && hb.files_touched) {
|
|
241
|
-
|
|
241
|
+
// files_touched holds a mix of absolute-under-coordRoot and canonical
|
|
242
|
+
// repo-relative entries (Edit events report absolute; release-claim
|
|
243
|
+
// canonicalizes to relative). Normalize both sides so a release
|
|
244
|
+
// subtracts regardless of form — an exact-string compare silently
|
|
245
|
+
// no-ops on the mismatch and the claim resurrects on the next replay.
|
|
246
|
+
const norm = (p) => p.startsWith(`${coordRoot}/`) ? p.slice(coordRoot.length + 1) : p;
|
|
247
|
+
const target = norm(path);
|
|
248
|
+
hb.files_touched = hb.files_touched.filter((p) => norm(p) !== target);
|
|
242
249
|
}
|
|
243
250
|
break;
|
|
244
251
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -43,4 +43,13 @@ export declare function pinnedBinName(projectRoot: string): string | null;
|
|
|
43
43
|
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
44
44
|
*/
|
|
45
45
|
export declare function resolveHooksSetupHint(coordRoot?: string | null): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the host project consented to automatic ripgrep provisioning:
|
|
48
|
+
* `.harnery/config.jsonc` `{ "tools": { "ripgrep": { "autoInstall": true } } }`.
|
|
49
|
+
* A repo commits that once and every clone self-heals on first `grep`; without
|
|
50
|
+
* it, a missing rg only produces a rate-limited hint (`doctor --fix` installs
|
|
51
|
+
* explicitly). `HARNERY_TOOLS_AUTOINSTALL=1|0` overrides per process.
|
|
52
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
53
|
+
*/
|
|
54
|
+
export declare function ripgrepAutoInstall(coordRoot?: string | null): boolean;
|
|
46
55
|
//# sourceMappingURL=config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAuBvC,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAqCvD;AAyBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAShE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK9E;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAOrE"}
|
package/dist/core/config.js
CHANGED
|
@@ -132,3 +132,22 @@ export function resolveHooksSetupHint(coordRoot) {
|
|
|
132
132
|
const hint = readConfig(root).hooksSetupHint;
|
|
133
133
|
return typeof hint === "string" && hint.trim() ? hint.trim() : null;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether the host project consented to automatic ripgrep provisioning:
|
|
137
|
+
* `.harnery/config.jsonc` `{ "tools": { "ripgrep": { "autoInstall": true } } }`.
|
|
138
|
+
* A repo commits that once and every clone self-heals on first `grep`; without
|
|
139
|
+
* it, a missing rg only produces a rate-limited hint (`doctor --fix` installs
|
|
140
|
+
* explicitly). `HARNERY_TOOLS_AUTOINSTALL=1|0` overrides per process.
|
|
141
|
+
* `coordRoot` is resolved via `findCoordRoot()` when not passed.
|
|
142
|
+
*/
|
|
143
|
+
export function ripgrepAutoInstall(coordRoot) {
|
|
144
|
+
const env = coordEnv("TOOLS_AUTOINSTALL");
|
|
145
|
+
if (env === "1")
|
|
146
|
+
return true;
|
|
147
|
+
if (env === "0")
|
|
148
|
+
return false;
|
|
149
|
+
const root = coordRoot ?? findCoordRoot();
|
|
150
|
+
if (!root)
|
|
151
|
+
return false;
|
|
152
|
+
return readConfig(root).tools?.ripgrep?.autoInstall === true;
|
|
153
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed ripgrep provisioning for `grep --files` / content search.
|
|
3
|
+
*
|
|
4
|
+
* harnery can install a pinned, checksum-verified ripgrep into its own tools
|
|
5
|
+
* directory (`$XDG_DATA_HOME/harnery/tools`, default `~/.local/share/harnery/
|
|
6
|
+
* tools`) so `harn grep` gets the fast engine even on machines where nobody
|
|
7
|
+
* ever installed rg. The download is version-pinned and every artifact's
|
|
8
|
+
* sha256 is baked in below (cross-checked against the official release
|
|
9
|
+
* checksums), so the install is reproducible, never "whatever is latest."
|
|
10
|
+
*
|
|
11
|
+
* Consent model: the installer only runs automatically when the host project
|
|
12
|
+
* opts in via `.harnery/config.jsonc` `{ "tools": { "ripgrep": { "autoInstall":
|
|
13
|
+
* true } } }` — a repo commits that once and every clone self-heals. Without
|
|
14
|
+
* the opt-in, a missing rg produces a rate-limited stderr hint naming
|
|
15
|
+
* `harn doctor --fix` (explicit install). Environments that forbid downloads
|
|
16
|
+
* lose nothing: every failure path falls back to GNU grep.
|
|
17
|
+
*/
|
|
18
|
+
export declare const RG_VERSION = "14.1.1";
|
|
19
|
+
/** True when a pinned artifact exists for this OS/arch (Windows: not yet). */
|
|
20
|
+
export declare function rgInstallSupported(): boolean;
|
|
21
|
+
/** Machine-level harnery tools dir (NOT the per-project .harnery/ state dir). */
|
|
22
|
+
export declare function toolsDir(): string;
|
|
23
|
+
/** Path the managed rg binary lives at once installed. */
|
|
24
|
+
export declare function managedRgPath(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Locate a usable ripgrep. Precedence:
|
|
27
|
+
* 1. HARNERY_RG_PATH (explicit override)
|
|
28
|
+
* 2. the managed tools-dir install (works even when PATH doesn't carry it)
|
|
29
|
+
* 3. `rg` on PATH
|
|
30
|
+
* Returns the spawnable path/name, or null when none respond to --version.
|
|
31
|
+
*/
|
|
32
|
+
export declare function findRg(): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Download + verify + install the pinned ripgrep into the tools dir.
|
|
35
|
+
* Atomic: extracts to a temp dir and renames the binary into place.
|
|
36
|
+
* Throws on unsupported platform, network failure, or checksum mismatch —
|
|
37
|
+
* callers are expected to fall back to grep and say why.
|
|
38
|
+
*/
|
|
39
|
+
export declare function installRg(log?: (line: string) => void): Promise<string>;
|
|
40
|
+
export type SearchEngine = "rg" | "grep";
|
|
41
|
+
/**
|
|
42
|
+
* Shared engine resolver for the ripgrep-backed search commands (`grep`,
|
|
43
|
+
* `callers`). Returns the engine plus the spawnable rg path.
|
|
44
|
+
*
|
|
45
|
+
* `HARNERY_GREP_ENGINE=rg|grep` forces one; otherwise findRg() probes
|
|
46
|
+
* HARNERY_RG_PATH → the managed tools-dir install → PATH. On a miss, the
|
|
47
|
+
* host's `.harnery/config.jsonc` `tools.ripgrep.autoInstall` consent triggers
|
|
48
|
+
* a pinned, checksum-verified install into the harnery tools dir (any failure
|
|
49
|
+
* falls back to grep with a stderr note); without consent, a rate-limited
|
|
50
|
+
* stderr hint (keyed per `hintKey`) names the explicit install command.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveSearchEngine(hintKey?: string): Promise<{
|
|
53
|
+
engine: SearchEngine;
|
|
54
|
+
rgBin: string;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Emit `line` to stderr at most once per 24h (stamp file in the tools dir).
|
|
58
|
+
* Keeps the "rg missing" nudge from nagging on every single grep invocation.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hintOncePerDay(line: string): void;
|
|
61
|
+
//# sourceMappingURL=ripgrep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ripgrep.d.ts","sourceRoot":"","sources":["../../../src/lib/tools/ripgrep.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;GAgBG;AAEH,eAAO,MAAM,UAAU,WAAW,CAAC;AA0BnC,8EAA8E;AAC9E,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,IAAI,MAAM,CAIjC;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAWD;;;;;;GAMG;AACH,wBAAgB,MAAM,IAAI,MAAM,GAAG,IAAI,CAStC;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAoD7E;AAkBD,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,MAAM,CAAC;AAEzC;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,SAAS,GACf,OAAO,CAAC;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BlD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAYjD"}
|