diffwiki-ripgrep 0.4.0-rc.202607220502.c0ac143
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/dist/bin.js +305 -0
- package/package.json +31 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
// src/protocol.ts
|
|
7
|
+
function toMessage(err) {
|
|
8
|
+
if (err instanceof Error) return err.message;
|
|
9
|
+
if (typeof err === "string") return err;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.stringify(err);
|
|
12
|
+
} catch {
|
|
13
|
+
return String(err);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function serve(handlers, io = {}) {
|
|
17
|
+
const input = io.input ?? process.stdin;
|
|
18
|
+
const output = io.output ?? process.stdout;
|
|
19
|
+
const write = (obj) => {
|
|
20
|
+
output.write(`${JSON.stringify(obj)}
|
|
21
|
+
`);
|
|
22
|
+
};
|
|
23
|
+
const log = (level, message) => {
|
|
24
|
+
write({ log: { level, message } });
|
|
25
|
+
};
|
|
26
|
+
const dispatch = async (line) => {
|
|
27
|
+
let req;
|
|
28
|
+
try {
|
|
29
|
+
req = JSON.parse(line);
|
|
30
|
+
} catch {
|
|
31
|
+
process.stderr.write(`diffwiki-ripgrep: skipping malformed line: ${line}
|
|
32
|
+
`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const handler = handlers[req.op];
|
|
36
|
+
if (!handler) {
|
|
37
|
+
write({ id: req.id, ok: false, error: { message: `unknown op: ${req.op}` } });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const result = await handler(req.params ?? {}, log);
|
|
42
|
+
write({ id: req.id, ok: true, result });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
write({ id: req.id, ok: false, error: { message: toMessage(err) } });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
let buffer = "";
|
|
49
|
+
let chain = Promise.resolve();
|
|
50
|
+
input.setEncoding("utf8");
|
|
51
|
+
input.on("data", (chunk) => {
|
|
52
|
+
buffer += chunk;
|
|
53
|
+
let nl = buffer.indexOf("\n");
|
|
54
|
+
while (nl !== -1) {
|
|
55
|
+
const line = buffer.slice(0, nl).trim();
|
|
56
|
+
buffer = buffer.slice(nl + 1);
|
|
57
|
+
if (line.length > 0) {
|
|
58
|
+
chain = chain.then(() => dispatch(line));
|
|
59
|
+
}
|
|
60
|
+
nl = buffer.indexOf("\n");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
input.on("end", () => {
|
|
64
|
+
const tail = buffer.trim();
|
|
65
|
+
if (tail.length > 0) chain = chain.then(() => dispatch(tail));
|
|
66
|
+
chain.then(resolve);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/ripgrep.ts
|
|
72
|
+
import { spawn } from "child_process";
|
|
73
|
+
import { createReadStream } from "fs";
|
|
74
|
+
import { basename, extname } from "path";
|
|
75
|
+
import { createInterface } from "readline";
|
|
76
|
+
var spawnRg = (args) => new Promise((resolve, reject) => {
|
|
77
|
+
const chunks = [];
|
|
78
|
+
const proc = spawn("rg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
79
|
+
proc.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
80
|
+
proc.on("error", reject);
|
|
81
|
+
proc.on("close", (code) => {
|
|
82
|
+
if (code === 0 || code === 1) {
|
|
83
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
84
|
+
} else {
|
|
85
|
+
const stderr = "";
|
|
86
|
+
reject(new Error(`rg exited with code ${code}${stderr ? ": " + stderr : ""}`));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
var readFileLines = (absPath) => new Promise((resolve, reject) => {
|
|
91
|
+
const lines = [];
|
|
92
|
+
const stream = createReadStream(absPath, { encoding: "utf8" });
|
|
93
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
94
|
+
rl.on("line", (line) => {
|
|
95
|
+
lines.push(line);
|
|
96
|
+
if (lines.length >= 20) {
|
|
97
|
+
rl.close();
|
|
98
|
+
stream.destroy();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
rl.on("close", () => resolve(lines));
|
|
102
|
+
rl.on("error", reject);
|
|
103
|
+
stream.on("error", reject);
|
|
104
|
+
});
|
|
105
|
+
async function rgOnPath(spawnFn = spawnRg) {
|
|
106
|
+
try {
|
|
107
|
+
await spawnFn(["--version"]);
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function runRg(spawnFn, pattern, roots, opts) {
|
|
114
|
+
const limit = opts.limit > 0 ? opts.limit : 20;
|
|
115
|
+
const args = ["--json", "--line-number", "--max-count", String(limit), "--glob", "*.{md,mdx}"];
|
|
116
|
+
if (opts.fixedStrings) {
|
|
117
|
+
args.push("--fixed-strings", "-i");
|
|
118
|
+
}
|
|
119
|
+
args.push("--", pattern, ...roots);
|
|
120
|
+
return spawnFn(args);
|
|
121
|
+
}
|
|
122
|
+
async function cheapTitle(absPath, readFn = readFileLines) {
|
|
123
|
+
let lines;
|
|
124
|
+
try {
|
|
125
|
+
lines = await readFn(absPath);
|
|
126
|
+
} catch {
|
|
127
|
+
return basename(absPath, extname(absPath));
|
|
128
|
+
}
|
|
129
|
+
if (lines.length > 0 && lines[0].trim() === "---") {
|
|
130
|
+
for (let i = 1; i < lines.length; i++) {
|
|
131
|
+
const line = lines[i];
|
|
132
|
+
if (line.trim() === "---" || line.trim() === "...") break;
|
|
133
|
+
const m = line.match(/^title:\s*(.+)/i);
|
|
134
|
+
if (m) return m[1].trim().replace(/^['"]|['"]$/g, "");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
const m = line.match(/^#{1,6}\s+(.+)/);
|
|
139
|
+
if (m) return m[1].trim();
|
|
140
|
+
}
|
|
141
|
+
return basename(absPath, extname(absPath));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/bin.ts
|
|
145
|
+
var VERSION = "0.3.0";
|
|
146
|
+
function asCollections(params) {
|
|
147
|
+
const raw = params.collections;
|
|
148
|
+
if (!Array.isArray(raw)) return [];
|
|
149
|
+
return raw.filter(
|
|
150
|
+
(c) => !!c && typeof c.name === "string" && typeof c.path === "string"
|
|
151
|
+
).map((c) => ({ name: c.name, path: c.path }));
|
|
152
|
+
}
|
|
153
|
+
function matchCollection(filePath, collections) {
|
|
154
|
+
const sorted = [...collections].sort((a, b) => b.path.length - a.path.length);
|
|
155
|
+
for (const col of sorted) {
|
|
156
|
+
const prefix = col.path.endsWith("/") ? col.path : col.path + "/";
|
|
157
|
+
if (filePath.startsWith(prefix)) {
|
|
158
|
+
return { collection: col.name, relPath: filePath.slice(prefix.length) };
|
|
159
|
+
}
|
|
160
|
+
if (filePath === col.path) {
|
|
161
|
+
return { collection: col.name, relPath: "" };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function parseRgOutput(stdout) {
|
|
167
|
+
return stdout.split("\n").filter((line) => line.trim().length > 0).flatMap((line) => {
|
|
168
|
+
try {
|
|
169
|
+
const obj = JSON.parse(line);
|
|
170
|
+
return obj.type === "match" ? [obj] : [];
|
|
171
|
+
} catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function makeHandlers(deps = {}) {
|
|
177
|
+
const spawnFn = deps.spawnFn ?? spawnRg;
|
|
178
|
+
const readFn = deps.readFn ?? readFileLines;
|
|
179
|
+
return {
|
|
180
|
+
initialize() {
|
|
181
|
+
return {
|
|
182
|
+
name: "diffwiki-ripgrep",
|
|
183
|
+
kind: "search",
|
|
184
|
+
version: VERSION,
|
|
185
|
+
capabilities: {
|
|
186
|
+
searchTypes: ["text", "regex"],
|
|
187
|
+
collectionFilter: true,
|
|
188
|
+
hooks: false,
|
|
189
|
+
setup: true
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
async setup(params, log) {
|
|
194
|
+
void params;
|
|
195
|
+
const rgPresent = await rgOnPath(spawnFn);
|
|
196
|
+
if (!rgPresent) {
|
|
197
|
+
log("error", "ripgrep (rg) not found \u2014 install it: https://github.com/BurntSushi/ripgrep");
|
|
198
|
+
return {
|
|
199
|
+
ready: false,
|
|
200
|
+
steps: [
|
|
201
|
+
{
|
|
202
|
+
step: "verify-rg",
|
|
203
|
+
ok: false,
|
|
204
|
+
message: "rg not on PATH \u2014 install ripgrep: https://github.com/BurntSushi/ripgrep"
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
ready: true,
|
|
211
|
+
steps: [{ step: "verify-rg", ok: true }]
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
index() {
|
|
215
|
+
return { indexed: 0 };
|
|
216
|
+
},
|
|
217
|
+
notify() {
|
|
218
|
+
return { accepted: true };
|
|
219
|
+
},
|
|
220
|
+
async search(params) {
|
|
221
|
+
const term = typeof params.term === "string" ? params.term : "";
|
|
222
|
+
const collections = asCollections(params);
|
|
223
|
+
const collectionFilter = typeof params.collection === "string" && params.collection.length > 0 ? params.collection : null;
|
|
224
|
+
const type = typeof params.type === "string" ? params.type : "text";
|
|
225
|
+
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20;
|
|
226
|
+
const fixedStrings = type !== "regex";
|
|
227
|
+
let roots;
|
|
228
|
+
let activeCollections;
|
|
229
|
+
if (collectionFilter !== null) {
|
|
230
|
+
const found = collections.find((c) => c.name === collectionFilter);
|
|
231
|
+
roots = found ? [found.path] : [];
|
|
232
|
+
activeCollections = found ? [found] : [];
|
|
233
|
+
} else {
|
|
234
|
+
roots = collections.map((c) => c.path);
|
|
235
|
+
activeCollections = collections;
|
|
236
|
+
}
|
|
237
|
+
if (roots.length === 0 || term.length === 0) {
|
|
238
|
+
return { hits: [] };
|
|
239
|
+
}
|
|
240
|
+
const stdout = await runRg(spawnFn, term, roots, { fixedStrings, limit });
|
|
241
|
+
const events = parseRgOutput(stdout);
|
|
242
|
+
const seen = /* @__PURE__ */ new Set();
|
|
243
|
+
const hits = [];
|
|
244
|
+
for (const event of events) {
|
|
245
|
+
if (hits.length >= limit) break;
|
|
246
|
+
const filePath = event.data.path.text;
|
|
247
|
+
if (seen.has(filePath)) continue;
|
|
248
|
+
seen.add(filePath);
|
|
249
|
+
const matched = matchCollection(filePath, activeCollections);
|
|
250
|
+
if (!matched) continue;
|
|
251
|
+
const title = await cheapTitle(filePath, readFn);
|
|
252
|
+
const snippet = (event.data.lines.text || "").trim();
|
|
253
|
+
hits.push({
|
|
254
|
+
collection: matched.collection,
|
|
255
|
+
relPath: matched.relPath,
|
|
256
|
+
title,
|
|
257
|
+
snippet: snippet.length > 0 ? snippet : void 0
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return { hits };
|
|
261
|
+
},
|
|
262
|
+
async health() {
|
|
263
|
+
const rgPresent = await rgOnPath(spawnFn);
|
|
264
|
+
return {
|
|
265
|
+
ready: rgPresent,
|
|
266
|
+
readiness: {
|
|
267
|
+
toolInstalled: rgPresent,
|
|
268
|
+
modelsPresent: true,
|
|
269
|
+
indexBuilt: true,
|
|
270
|
+
docsIndexed: 0,
|
|
271
|
+
embeddingsPending: 0,
|
|
272
|
+
firstSearchCost: "instant"
|
|
273
|
+
},
|
|
274
|
+
diagnostics: rgPresent ? [{ level: "ok", message: "ripgrep ready" }] : [
|
|
275
|
+
{
|
|
276
|
+
level: "error",
|
|
277
|
+
message: "ripgrep (rg) not found \u2014 install it: https://github.com/BurntSushi/ripgrep"
|
|
278
|
+
}
|
|
279
|
+
]
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
shutdown() {
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
var isMain = (() => {
|
|
288
|
+
try {
|
|
289
|
+
return process.argv[1] === fileURLToPath(import.meta.url);
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
})();
|
|
294
|
+
if (isMain) {
|
|
295
|
+
serve(makeHandlers({ spawnFn: spawnRg, readFn: readFileLines })).then(() => process.exit(0)).catch((err) => {
|
|
296
|
+
process.stderr.write(`diffwiki-ripgrep: fatal: ${String(err)}
|
|
297
|
+
`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
export {
|
|
302
|
+
makeHandlers,
|
|
303
|
+
matchCollection,
|
|
304
|
+
parseRgOutput
|
|
305
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "diffwiki-ripgrep",
|
|
3
|
+
"version": "0.4.0-rc.202607220502.c0ac143",
|
|
4
|
+
"description": "diffwiki search plugin driving ripgrep (rg) — instant literal/regex search, no index",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"diffwiki-ripgrep": "./dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.0.0"
|
|
15
|
+
},
|
|
16
|
+
"diffwiki": {
|
|
17
|
+
"kind": "search"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"test": "vitest run --passWithNoTests",
|
|
28
|
+
"lint": "eslint src tests --no-error-on-unmatched-pattern && tsc --noEmit",
|
|
29
|
+
"lint:fix": "eslint src tests --no-error-on-unmatched-pattern --fix"
|
|
30
|
+
}
|
|
31
|
+
}
|