@levnikolaevich/hex-line-mcp 1.3.2 → 1.3.4

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.
@@ -1,541 +0,0 @@
1
- import { readFileSync, statSync, readdirSync } from "node:fs";
2
- import { execSync } from "node:child_process";
3
- import { performance } from "node:perf_hooks";
4
- import { resolve, extname, join } from "node:path";
5
- import { fnv1a, lineTag } from "./hash.mjs";
6
- import { readFile } from "./read.mjs";
7
-
8
- // ---------------------------------------------------------------------------
9
- // Constants (shared with benchmark.mjs)
10
- // ---------------------------------------------------------------------------
11
-
12
- const CODE_EXTS = new Set([".js", ".ts", ".py", ".mjs", ".go", ".rs", ".java", ".c", ".cpp", ".rb", ".php"]);
13
- const MAX_FILES_PER_CAT = 3;
14
- const RUNS = 5;
15
-
16
- // ---------------------------------------------------------------------------
17
- // File discovery
18
- // ---------------------------------------------------------------------------
19
-
20
- function walkDir(dir, depth = 0) {
21
- if (depth > 10) return [];
22
- const results = [];
23
- let entries;
24
- try { entries = readdirSync(dir, { withFileTypes: true }); }
25
- catch { return results; }
26
- for (const e of entries) {
27
- const full = resolve(dir, e.name);
28
- if (e.isDirectory()) {
29
- if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "vendor"
30
- || e.name === "dist" || e.name === "__pycache__" || e.name === "target") continue;
31
- results.push(...walkDir(full, depth + 1));
32
- } else if (e.isFile() && CODE_EXTS.has(extname(e.name).toLowerCase())) {
33
- try {
34
- const st = statSync(full);
35
- if (st.size > 0 && st.size < 1_000_000) results.push(full);
36
- } catch { /* skip */ }
37
- }
38
- }
39
- return results;
40
- }
41
-
42
- function getFileLines(f) {
43
- try { return readFileSync(f, "utf-8").replace(/\r\n/g, "\n").split("\n"); }
44
- catch { return null; }
45
- }
46
-
47
- function categorize(files) {
48
- const cats = { small: [], medium: [], large: [], xl: [] };
49
- for (const f of files) {
50
- const lines = getFileLines(f);
51
- if (!lines) continue;
52
- const n = lines.length;
53
- if (n >= 10 && n <= 50) cats.small.push(f);
54
- else if (n > 50 && n <= 200) cats.medium.push(f);
55
- else if (n > 200 && n <= 500) cats.large.push(f);
56
- else if (n > 500) cats.xl.push(f);
57
- }
58
- for (const key of Object.keys(cats)) {
59
- const arr = cats[key];
60
- if (arr.length > MAX_FILES_PER_CAT) {
61
- const step = Math.floor(arr.length / MAX_FILES_PER_CAT);
62
- cats[key] = Array.from({ length: MAX_FILES_PER_CAT }, (_, i) => arr[i * step]);
63
- }
64
- }
65
- return cats;
66
- }
67
-
68
- // ---------------------------------------------------------------------------
69
- // Temp file: 200 lines of realistic JS
70
- // ---------------------------------------------------------------------------
71
-
72
- function generateTempCode() {
73
- const lines = [];
74
- lines.push('import { readFileSync } from "node:fs";');
75
- lines.push('import { resolve, basename } from "node:path";');
76
- lines.push("");
77
- lines.push("const DEFAULT_TIMEOUT = 5000;");
78
- lines.push("const MAX_RETRIES = 3;");
79
- lines.push("");
80
- lines.push("/**");
81
- lines.push(" * Configuration manager for application settings.");
82
- lines.push(" * Supports file-based and environment-based config sources.");
83
- lines.push(" */");
84
- lines.push("class ConfigManager {");
85
- lines.push(" constructor(configPath) {");
86
- lines.push(" this.configPath = resolve(configPath);");
87
- lines.push(" this.cache = new Map();");
88
- lines.push(" this.watchers = [];");
89
- lines.push(" this.loaded = false;");
90
- lines.push(" }");
91
- lines.push("");
92
- lines.push(" load() {");
93
- lines.push(" const raw = readFileSync(this.configPath, 'utf-8');");
94
- lines.push(" const parsed = JSON.parse(raw);");
95
- lines.push(" for (const [key, value] of Object.entries(parsed)) {");
96
- lines.push(" this.cache.set(key, value);");
97
- lines.push(" }");
98
- lines.push(" this.loaded = true;");
99
- lines.push(" this.notifyWatchers('load', parsed);");
100
- lines.push(" return this;");
101
- lines.push(" }");
102
- lines.push("");
103
- lines.push(" get(key, defaultValue = undefined) {");
104
- lines.push(" if (!this.loaded) this.load();");
105
- lines.push(" return this.cache.has(key) ? this.cache.get(key) : defaultValue;");
106
- lines.push(" }");
107
- lines.push("");
108
- lines.push(" set(key, value) {");
109
- lines.push(" this.cache.set(key, value);");
110
- lines.push(" this.notifyWatchers('set', { key, value });");
111
- lines.push(" }");
112
- lines.push("");
113
- lines.push(" watch(callback) {");
114
- lines.push(" this.watchers.push(callback);");
115
- lines.push(" return () => {");
116
- lines.push(" this.watchers = this.watchers.filter(w => w !== callback);");
117
- lines.push(" };");
118
- lines.push(" }");
119
- lines.push("");
120
- lines.push(" notifyWatchers(event, data) {");
121
- lines.push(" for (const watcher of this.watchers) {");
122
- lines.push(" try { watcher(event, data); }");
123
- lines.push(" catch (e) { console.error('Watcher error:', e.message); }");
124
- lines.push(" }");
125
- lines.push(" }");
126
- lines.push("}");
127
- lines.push("");
128
- lines.push("/**");
129
- lines.push(" * Retry wrapper with exponential backoff.");
130
- lines.push(" */");
131
- lines.push("async function withRetry(fn, options = {}) {");
132
- lines.push(" const { retries = MAX_RETRIES, delay = 100, backoff = 2 } = options;");
133
- lines.push(" let lastError;");
134
- lines.push(" for (let attempt = 0; attempt <= retries; attempt++) {");
135
- lines.push(" try {");
136
- lines.push(" return await fn(attempt);");
137
- lines.push(" } catch (err) {");
138
- lines.push(" lastError = err;");
139
- lines.push(" if (attempt < retries) {");
140
- lines.push(" const wait = delay * Math.pow(backoff, attempt);");
141
- lines.push(" await new Promise(r => setTimeout(r, wait));");
142
- lines.push(" }");
143
- lines.push(" }");
144
- lines.push(" }");
145
- lines.push(" throw lastError;");
146
- lines.push("}");
147
- lines.push("");
148
- lines.push("/**");
149
- lines.push(" * HTTP client with timeout and retry support.");
150
- lines.push(" */");
151
- lines.push("class HttpClient {");
152
- lines.push(" constructor(baseUrl, options = {}) {");
153
- lines.push(" this.baseUrl = baseUrl.replace(/\\/$/, '');");
154
- lines.push(" this.timeout = options.timeout || DEFAULT_TIMEOUT;");
155
- lines.push(" this.headers = options.headers || {};");
156
- lines.push(" this.retries = options.retries || MAX_RETRIES;");
157
- lines.push(" }");
158
- lines.push("");
159
- lines.push(" async request(method, path, body = null) {");
160
- lines.push(" const url = `${this.baseUrl}${path}`;");
161
- lines.push(" const controller = new AbortController();");
162
- lines.push(" const timer = setTimeout(() => controller.abort(), this.timeout);");
163
- lines.push("");
164
- lines.push(" try {");
165
- lines.push(" return await withRetry(async () => {");
166
- lines.push(" const opts = {");
167
- lines.push(" method,");
168
- lines.push(" headers: { ...this.headers },");
169
- lines.push(" signal: controller.signal,");
170
- lines.push(" };");
171
- lines.push(" if (body) {");
172
- lines.push(" opts.headers['Content-Type'] = 'application/json';");
173
- lines.push(" opts.body = JSON.stringify(body);");
174
- lines.push(" }");
175
- lines.push(" const response = await fetch(url, opts);");
176
- lines.push(" if (!response.ok) {");
177
- lines.push(" throw new Error(`HTTP ${response.status}: ${response.statusText}`);");
178
- lines.push(" }");
179
- lines.push(" return response.json();");
180
- lines.push(" }, { retries: this.retries });");
181
- lines.push(" } finally {");
182
- lines.push(" clearTimeout(timer);");
183
- lines.push(" }");
184
- lines.push(" }");
185
- lines.push("");
186
- lines.push(" get(path) { return this.request('GET', path); }");
187
- lines.push(" post(path, body) { return this.request('POST', path, body); }");
188
- lines.push(" put(path, body) { return this.request('PUT', path, body); }");
189
- lines.push(" delete(path) { return this.request('DELETE', path); }");
190
- lines.push("}");
191
- lines.push("");
192
- lines.push("/**");
193
- lines.push(" * Simple event emitter for pub/sub patterns.");
194
- lines.push(" */");
195
- lines.push("class EventEmitter {");
196
- lines.push(" constructor() {");
197
- lines.push(" this.listeners = new Map();");
198
- lines.push(" }");
199
- lines.push("");
200
- lines.push(" on(event, handler) {");
201
- lines.push(" if (!this.listeners.has(event)) {");
202
- lines.push(" this.listeners.set(event, []);");
203
- lines.push(" }");
204
- lines.push(" this.listeners.get(event).push(handler);");
205
- lines.push(" return this;");
206
- lines.push(" }");
207
- lines.push("");
208
- lines.push(" off(event, handler) {");
209
- lines.push(" const handlers = this.listeners.get(event);");
210
- lines.push(" if (handlers) {");
211
- lines.push(" this.listeners.set(event, handlers.filter(h => h !== handler));");
212
- lines.push(" }");
213
- lines.push(" return this;");
214
- lines.push(" }");
215
- lines.push("");
216
- lines.push(" emit(event, ...args) {");
217
- lines.push(" const handlers = this.listeners.get(event) || [];");
218
- lines.push(" for (const handler of handlers) {");
219
- lines.push(" handler(...args);");
220
- lines.push(" }");
221
- lines.push(" }");
222
- lines.push("");
223
- lines.push(" once(event, handler) {");
224
- lines.push(" const wrapper = (...args) => {");
225
- lines.push(" handler(...args);");
226
- lines.push(" this.off(event, wrapper);");
227
- lines.push(" };");
228
- lines.push(" return this.on(event, wrapper);");
229
- lines.push(" }");
230
- lines.push("}");
231
- lines.push("");
232
- lines.push("/**");
233
- lines.push(" * Validate and sanitize user input.");
234
- lines.push(" */");
235
- lines.push("function validateInput(schema, data) {");
236
- lines.push(" const errors = [];");
237
- lines.push(" for (const [field, rules] of Object.entries(schema)) {");
238
- lines.push(" const value = data[field];");
239
- lines.push(" if (rules.required && (value === undefined || value === null)) {");
240
- lines.push(" errors.push(`${field} is required`);");
241
- lines.push(" continue;");
242
- lines.push(" }");
243
- lines.push(" if (value !== undefined && rules.type && typeof value !== rules.type) {");
244
- lines.push(" errors.push(`${field} must be ${rules.type}`);");
245
- lines.push(" }");
246
- lines.push(" if (typeof value === 'string' && rules.maxLength && value.length > rules.maxLength) {");
247
- lines.push(" errors.push(`${field} exceeds max length ${rules.maxLength}`);");
248
- lines.push(" }");
249
- lines.push(" if (typeof value === 'number' && rules.min !== undefined && value < rules.min) {");
250
- lines.push(" errors.push(`${field} must be >= ${rules.min}`);");
251
- lines.push(" }");
252
- lines.push(" }");
253
- lines.push(" return errors.length > 0 ? { valid: false, errors } : { valid: true };");
254
- lines.push("}");
255
- lines.push("");
256
- lines.push("/**");
257
- lines.push(" * Format bytes to human-readable string.");
258
- lines.push(" */");
259
- lines.push("function formatBytes(bytes) {");
260
- lines.push(" if (bytes === 0) return '0 B';");
261
- lines.push(" const units = ['B', 'KB', 'MB', 'GB', 'TB'];");
262
- lines.push(" const exp = Math.floor(Math.log(bytes) / Math.log(1024));");
263
- lines.push(" const value = bytes / Math.pow(1024, exp);");
264
- lines.push(" return `${value.toFixed(exp > 0 ? 1 : 0)} ${units[exp]}`;");
265
- lines.push("}");
266
- lines.push("");
267
- lines.push("/**");
268
- lines.push(" * Deep merge two objects (source into target).");
269
- lines.push(" */");
270
- lines.push("function deepMerge(target, source) {");
271
- lines.push(" const result = { ...target };");
272
- lines.push(" for (const key of Object.keys(source)) {");
273
- lines.push(" if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {");
274
- lines.push(" result[key] = deepMerge(result[key] || {}, source[key]);");
275
- lines.push(" } else {");
276
- lines.push(" result[key] = source[key];");
277
- lines.push(" }");
278
- lines.push(" }");
279
- lines.push(" return result;");
280
- lines.push("}");
281
- lines.push("");
282
- lines.push("export { ConfigManager, HttpClient, EventEmitter, withRetry, validateInput, formatBytes, deepMerge };");
283
-
284
- // Pad to exactly 200 lines
285
- while (lines.length < 200) lines.push("");
286
- return lines.slice(0, 200);
287
- }
288
-
289
- // ---------------------------------------------------------------------------
290
- // Simulators -- "without hex-line" (built-in tool output)
291
- // ---------------------------------------------------------------------------
292
-
293
- /** Simulate built-in Read: `cat -n` full file with header */
294
- function simBuiltInReadFull(filePath, lines) {
295
- const body = lines.map((l, i) => ` ${String(i + 1).padStart(5)}\t${l}`).join("\n");
296
- return `Contents of ${filePath}:\n\n${body}`;
297
- }
298
-
299
- /** Simulate outline via full read -- agent reads entire file to understand structure */
300
- function simBuiltInOutlineFull(filePath, lines) {
301
- return simBuiltInReadFull(filePath, lines);
302
- }
303
-
304
- /** Real ripgrep call (matches built-in Grep tool behavior) */
305
- function simBuiltInGrep(pattern, path) {
306
- try {
307
- return execSync(`rg -n --no-heading "${pattern}" "${path}"`, { encoding: "utf-8", timeout: 10000 });
308
- } catch { return ""; }
309
- }
310
-
311
- /** Simulate `ls -laR` style output for a directory */
312
- function simBuiltInLsR(dirPath, depth = 0, maxDepth = 3) {
313
- if (depth > maxDepth) return "";
314
- const out = [];
315
- let entries;
316
- try { entries = readdirSync(dirPath, { withFileTypes: true }); }
317
- catch { return ""; }
318
-
319
- const SKIP = new Set(["node_modules", ".git", "dist", "build", "__pycache__", "coverage"]);
320
-
321
- out.push(`${dirPath}:`);
322
- out.push("total " + entries.length);
323
-
324
- for (const e of entries) {
325
- if (SKIP.has(e.name) && e.isDirectory()) continue;
326
- const full = join(dirPath, e.name);
327
- try {
328
- const st = statSync(full);
329
- const type = e.isDirectory() ? "d" : "-";
330
- const size = String(st.size).padStart(8);
331
- const date = st.mtime.toISOString().slice(0, 16).replace("T", " ");
332
- out.push(`${type}rw-r--r-- 1 user group ${size} ${date} ${e.name}`);
333
- } catch { /* skip */ }
334
- }
335
- out.push("");
336
-
337
- for (const e of entries) {
338
- if (!e.isDirectory()) continue;
339
- if (SKIP.has(e.name)) continue;
340
- const full = join(dirPath, e.name);
341
- const sub = simBuiltInLsR(full, depth + 1, maxDepth);
342
- if (sub) out.push(sub);
343
- }
344
-
345
- return out.join("\n");
346
- }
347
-
348
- /** Simulate `stat` output for a file */
349
- function simBuiltInStat(filePath) {
350
- const st = statSync(filePath);
351
- return [
352
- ` File: ${filePath}`,
353
- ` Size: ${st.size}\tBlocks: ${Math.ceil(st.size / 512)}\tIO Block: 4096\tregular file`,
354
- `Device: 0h/0d\tInode: 0\tLinks: 1`,
355
- `Access: (0644/-rw-r--r--)\tUid: ( 1000/ user)\tGid: ( 1000/ group)`,
356
- `Access: ${st.atime.toISOString()}`,
357
- `Modify: ${st.mtime.toISOString()}`,
358
- `Change: ${st.ctime.toISOString()}`,
359
- ` Birth: ${st.birthtime.toISOString()}`,
360
- ].join("\n");
361
- }
362
-
363
- /** Simulate built-in write response */
364
- function simBuiltInWrite(filePath, content) {
365
- const lineCount = content.split("\n").length;
366
- return `File ${filePath} has been created successfully (${lineCount} lines).`;
367
- }
368
-
369
- /** Simulate built-in edit: old_string/new_string context blocks */
370
- function simBuiltInEdit(filePath, origLines, newLines) {
371
- let changeStart = -1, changeEnd = -1;
372
- for (let i = 0; i < Math.max(origLines.length, newLines.length); i++) {
373
- if (origLines[i] !== newLines[i]) {
374
- if (changeStart === -1) changeStart = i;
375
- changeEnd = i;
376
- }
377
- }
378
- if (changeStart === -1) return "";
379
-
380
- const ctxBefore = Math.max(0, changeStart - 3);
381
- const ctxAfter = Math.min(origLines.length, changeEnd + 4);
382
- const old_string = origLines.slice(ctxBefore, ctxAfter).join("\n");
383
- const new_string = newLines.slice(ctxBefore, Math.min(newLines.length, changeEnd + 4)).join("\n");
384
- return `The file ${filePath} has been edited. Here's the result of running \`cat -n\` on a snippet:\n` +
385
- `old_string:\n${old_string}\nnew_string:\n${new_string}`;
386
- }
387
-
388
- /** Simulate built-in verify: full re-read to check if file changed */
389
- function simBuiltInVerify(filePath, lines) {
390
- return simBuiltInReadFull(filePath, lines);
391
- }
392
-
393
- // ---------------------------------------------------------------------------
394
- // Simulators -- "with hex-line" (lib function output)
395
- // ---------------------------------------------------------------------------
396
-
397
- /** Hex-line outline -- regex heuristic (no tree-sitter in benchmark) */
398
- function simHexLineOutline(lines) {
399
- const structural = /^\s*(export\s+)?(function|class|def|async\s+def|impl|fn|pub\s+fn|struct|interface|type|enum|const|let|var)\b/;
400
- const importLine = /^\s*(import|from|require|use|#include)/;
401
- const entries = [];
402
- let importStart = -1, importEnd = -1, importCount = 0;
403
-
404
- for (let i = 0; i < lines.length; i++) {
405
- if (importLine.test(lines[i])) {
406
- if (importStart === -1) importStart = i + 1;
407
- importEnd = i + 1;
408
- importCount++;
409
- continue;
410
- }
411
- if (structural.test(lines[i])) {
412
- let end = lines.length;
413
- for (let j = i + 1; j < lines.length; j++) {
414
- if (structural.test(lines[j])) { end = j; break; }
415
- }
416
- entries.push(`${i + 1}-${end}: ${lines[i].trim().slice(0, 120)}`);
417
- }
418
- }
419
-
420
- const parts = [];
421
- if (importCount > 0) parts.push(`${importStart}-${importEnd}: (${importCount} imports/declarations)`);
422
- parts.push(...entries);
423
- parts.push("", `(${entries.length} symbols, ${lines.length} source lines)`);
424
- return `File: benchmark-target\n\n${parts.join("\n")}`;
425
- }
426
-
427
- /** Hex-line outline + targeted read of first function (30 lines) */
428
- function simHexLineOutlinePlusRead(filePath, lines) {
429
- const outlineStr = simHexLineOutline(lines);
430
- const structural = /^\s*(export\s+)?(function|class|def|async\s+def|impl|fn|pub\s+fn|struct)\b/;
431
- let funcStart = 0;
432
- for (let i = 0; i < lines.length; i++) {
433
- if (structural.test(lines[i])) { funcStart = i + 1; break; }
434
- }
435
- const start = Math.max(1, funcStart);
436
- const readStr = readFile(filePath, { offset: start, limit: 30 });
437
- return outlineStr + "\n---\n" + readStr;
438
- }
439
-
440
- /** Hex-line grep -- hash-annotated format */
441
- function simHexLineGrep(filePath, lines, pattern) {
442
- const re = new RegExp(pattern, "i");
443
- const matches = [];
444
- for (let i = 0; i < lines.length; i++) {
445
- if (re.test(lines[i])) {
446
- const tag = lineTag(fnv1a(lines[i]));
447
- matches.push(`${filePath}:>>${tag}.${i + 1}\t${lines[i]}`);
448
- }
449
- }
450
- return matches.length > 0
451
- ? "```\n" + matches.join("\n") + "\n```"
452
- : "No matches found.";
453
- }
454
-
455
- /** Hex-line write response */
456
- function simHexLineWrite(filePath, content) {
457
- const lineCount = content.split("\n").length;
458
- return `Created ${filePath} (${lineCount} lines)`;
459
- }
460
-
461
- /** Hex-line edit response: compact diff hunks */
462
- function simHexLineEditDiff(origLines, newLines, ctx = 3) {
463
- const out = [];
464
- const maxLen = Math.max(origLines.length, newLines.length);
465
- let i = 0;
466
-
467
- while (i < maxLen) {
468
- if (i < origLines.length && i < newLines.length && origLines[i] === newLines[i]) {
469
- i++;
470
- continue;
471
- }
472
- // Found a difference -- show context before
473
- const ctxStart = Math.max(0, i - ctx);
474
- if (ctxStart < i) {
475
- if (ctxStart > 0) out.push("...");
476
- for (let k = ctxStart; k < i; k++) {
477
- out.push(` ${k + 1}| ${origLines[k]}`);
478
- }
479
- }
480
- // Show changed lines
481
- const changeStart = i;
482
- while (i < maxLen && (i >= origLines.length || i >= newLines.length || origLines[i] !== newLines[i])) {
483
- if (i < origLines.length) out.push(`-${i + 1}| ${origLines[i]}`);
484
- i++;
485
- }
486
- for (let k = changeStart; k < i && k < newLines.length; k++) {
487
- out.push(`+${k + 1}| ${newLines[k]}`);
488
- }
489
- // Context after
490
- const ctxEnd = Math.min(maxLen, i + ctx);
491
- for (let k = i; k < ctxEnd && k < origLines.length; k++) {
492
- out.push(` ${k + 1}| ${origLines[k]}`);
493
- }
494
- if (ctxEnd < maxLen) out.push("...");
495
- break;
496
- }
497
-
498
- const diff = out.join("\n");
499
- return diff
500
- ? `Updated benchmark-file\n\nDiff:\n\`\`\`diff\n${diff}\n\`\`\``
501
- : "Updated benchmark-file";
502
- }
503
-
504
- // ---------------------------------------------------------------------------
505
- // Runner utilities
506
- // ---------------------------------------------------------------------------
507
-
508
- function median(arr) {
509
- const sorted = [...arr].sort((a, b) => a - b);
510
- const mid = Math.floor(sorted.length / 2);
511
- return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
512
- }
513
-
514
- function runN(fn, n = RUNS) {
515
- const results = [];
516
- const times = [];
517
- for (let i = 0; i < n; i++) {
518
- const t0 = performance.now();
519
- results.push(fn());
520
- times.push(performance.now() - t0);
521
- }
522
- return { value: median(results), ms: parseFloat(median(times).toFixed(1)) };
523
- }
524
-
525
- function fmt(n) {
526
- return n.toLocaleString("en-US", { maximumFractionDigits: 0 });
527
- }
528
-
529
- function pctSavings(without, withSL) {
530
- if (without === 0) return "N/A";
531
- const pct = ((without - withSL) / without) * 100;
532
- return pct >= 0 ? `${pct.toFixed(0)}%` : `-${Math.abs(pct).toFixed(0)}%`;
533
- }
534
-
535
- export {
536
- walkDir, getFileLines, categorize, generateTempCode,
537
- simBuiltInReadFull, simBuiltInOutlineFull, simBuiltInGrep,
538
- simBuiltInLsR, simBuiltInStat, simBuiltInWrite, simBuiltInEdit, simBuiltInVerify,
539
- simHexLineOutline, simHexLineOutlinePlusRead, simHexLineGrep, simHexLineWrite, simHexLineEditDiff,
540
- median, runN, fmt, pctSavings, RUNS,
541
- };
@@ -1,65 +0,0 @@
1
- import { writeFileSync } from "node:fs";
2
- import { execFileSync } from "node:child_process";
3
- import { resolve } from "node:path";
4
- import { simpleDiff } from "./edit.mjs";
5
- import { normalizePath } from "./security.mjs";
6
- import { readText, MAX_OUTPUT_CHARS } from "./format.mjs";
7
-
8
- export function bulkReplace(rootDir, globPattern, replacements, opts = {}) {
9
- const { dryRun = false, maxFiles = 100 } = opts;
10
- const abs = resolve(normalizePath(rootDir));
11
-
12
- // Find files via ripgrep (respects .gitignore)
13
- let files;
14
- try {
15
- const rgOut = execFileSync("rg", ["--files", "-g", globPattern, abs], { encoding: "utf-8", timeout: 10000 });
16
- files = rgOut.trim().split("\n").filter(Boolean);
17
- } catch (e) {
18
- if (e.status === 1) return "No files matched the glob pattern.";
19
- throw new Error(`GREP_ERROR: ${e.message}`);
20
- }
21
-
22
- if (files.length > maxFiles) {
23
- return `TOO_MANY_FILES: Found ${files.length} files, max_files is ${maxFiles}. Use more specific glob or increase max_files.`;
24
- }
25
-
26
- const results = [];
27
- let changed = 0, skipped = 0, errors = 0;
28
- const MAX_OUTPUT = MAX_OUTPUT_CHARS;
29
- let totalChars = 0;
30
-
31
- for (const file of files) {
32
- try {
33
- const original = readText(file);
34
- let content = original;
35
-
36
- for (const { old: oldText, new: newText } of replacements) {
37
- content = content.split(oldText).join(newText);
38
- }
39
-
40
- if (content === original) { skipped++; continue; }
41
-
42
- const diff = simpleDiff(original.split("\n"), content.split("\n"));
43
-
44
- if (!dryRun) {
45
- writeFileSync(file, content, "utf-8");
46
- }
47
-
48
- const relPath = file.replace(abs, "").replace(/^[/\\]/, "");
49
- results.push(`--- ${relPath}\n${diff || "(no visible diff)"}`);
50
- changed++;
51
- totalChars += results[results.length - 1].length;
52
- if (totalChars > MAX_OUTPUT) {
53
- const remaining = files.length - files.indexOf(file) - 1;
54
- if (remaining > 0) results.push(`OUTPUT_CAPPED: ${remaining} more files not shown. Output exceeded ${MAX_OUTPUT} chars.`);
55
- break;
56
- }
57
- } catch (e) {
58
- results.push(`ERROR: ${file}: ${e.message}`);
59
- errors++;
60
- }
61
- }
62
-
63
- const header = `Bulk replace: ${changed} files changed, ${skipped} skipped, ${errors} errors (dry_run: ${dryRun})`;
64
- return results.length ? `${header}\n\n${results.join("\n\n")}` : header;
65
- }