agentikit 0.0.7 → 0.0.8
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 +113 -77
- package/dist/index.d.ts +13 -3
- package/dist/index.js +7 -2
- package/dist/src/asset-spec.d.ts +14 -0
- package/dist/src/asset-spec.js +46 -0
- package/dist/src/cli.js +154 -52
- package/dist/src/common.d.ts +8 -0
- package/dist/src/common.js +46 -0
- package/dist/src/config.d.ts +31 -0
- package/dist/src/config.js +74 -0
- package/dist/src/embedder.d.ts +10 -0
- package/dist/src/embedder.js +87 -0
- package/dist/src/frontmatter.d.ts +30 -0
- package/dist/src/frontmatter.js +86 -0
- package/dist/src/indexer.d.ts +20 -2
- package/dist/src/indexer.js +212 -80
- package/dist/src/init.d.ts +19 -0
- package/dist/src/init.js +87 -0
- package/dist/src/llm.d.ts +15 -0
- package/dist/src/llm.js +91 -0
- package/dist/src/markdown.d.ts +18 -0
- package/dist/src/markdown.js +77 -0
- package/dist/src/metadata.d.ts +10 -2
- package/dist/src/metadata.js +146 -30
- package/dist/src/ripgrep-install.d.ts +12 -0
- package/dist/src/ripgrep-install.js +169 -0
- package/dist/src/ripgrep-resolve.d.ts +13 -0
- package/dist/src/ripgrep-resolve.js +68 -0
- package/dist/src/ripgrep.d.ts +3 -36
- package/dist/src/ripgrep.js +2 -262
- package/dist/src/similarity.d.ts +1 -2
- package/dist/src/similarity.js +11 -0
- package/dist/src/stash-ref.d.ts +7 -0
- package/dist/src/stash-ref.js +33 -0
- package/dist/src/stash-resolve.d.ts +2 -0
- package/dist/src/stash-resolve.js +45 -0
- package/dist/src/stash-search.d.ts +6 -0
- package/dist/src/stash-search.js +269 -0
- package/dist/src/stash-show.d.ts +5 -0
- package/dist/src/stash-show.js +107 -0
- package/dist/src/stash-types.d.ts +53 -0
- package/dist/src/stash-types.js +1 -0
- package/dist/src/stash.d.ts +8 -63
- package/dist/src/stash.js +4 -633
- package/dist/src/tool-runner.d.ts +35 -0
- package/dist/src/tool-runner.js +100 -0
- package/dist/src/walker.d.ts +19 -0
- package/dist/src/walker.js +47 -0
- package/package.json +8 -14
- package/src/asset-spec.ts +69 -0
- package/src/cli.ts +164 -48
- package/src/common.ts +58 -0
- package/src/config.ts +124 -0
- package/src/embedder.ts +117 -0
- package/src/frontmatter.ts +95 -0
- package/src/indexer.ts +244 -84
- package/src/init.ts +106 -0
- package/src/llm.ts +124 -0
- package/src/markdown.ts +106 -0
- package/src/metadata.ts +157 -29
- package/src/ripgrep-install.ts +200 -0
- package/src/ripgrep-resolve.ts +72 -0
- package/src/ripgrep.ts +3 -315
- package/src/similarity.ts +13 -1
- package/src/stash-ref.ts +41 -0
- package/src/stash-resolve.ts +47 -0
- package/src/stash-search.ts +343 -0
- package/src/stash-show.ts +104 -0
- package/src/stash-types.ts +46 -0
- package/src/stash.ts +16 -760
- package/src/tool-runner.ts +129 -0
- package/src/walker.ts +53 -0
- package/.claude-plugin/plugin.json +0 -21
- package/commands/open.md +0 -11
- package/commands/run.md +0 -11
- package/commands/search.md +0 -11
- package/dist/src/plugin.d.ts +0 -2
- package/dist/src/plugin.js +0 -55
- package/skills/stash/SKILL.md +0 -73
- package/src/plugin.ts +0 -56
package/dist/src/metadata.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentikitAssetType } from "./common";
|
|
2
|
+
import { type TocHeading } from "./markdown";
|
|
2
3
|
export interface StashIntent {
|
|
3
4
|
when?: string;
|
|
4
5
|
input?: string;
|
|
@@ -10,9 +11,15 @@ export interface StashEntry {
|
|
|
10
11
|
description?: string;
|
|
11
12
|
tags?: string[];
|
|
12
13
|
examples?: string[];
|
|
14
|
+
intents?: string[];
|
|
13
15
|
intent?: StashIntent;
|
|
14
16
|
entry?: string;
|
|
15
17
|
generated?: boolean;
|
|
18
|
+
quality?: "generated" | "curated";
|
|
19
|
+
confidence?: number;
|
|
20
|
+
source?: "package" | "frontmatter" | "comments" | "filename" | "manual" | "llm";
|
|
21
|
+
aliases?: string[];
|
|
22
|
+
toc?: TocHeading[];
|
|
16
23
|
}
|
|
17
24
|
export interface StashFile {
|
|
18
25
|
entries: StashEntry[];
|
|
@@ -21,7 +28,8 @@ export declare function stashFilePath(dirPath: string): string;
|
|
|
21
28
|
export declare function loadStashFile(dirPath: string): StashFile | null;
|
|
22
29
|
export declare function writeStashFile(dirPath: string, stash: StashFile): void;
|
|
23
30
|
export declare function validateStashEntry(entry: unknown): StashEntry | null;
|
|
24
|
-
export declare function generateMetadata(dirPath: string, assetType: AgentikitAssetType, files: string[]): StashFile;
|
|
31
|
+
export declare function generateMetadata(dirPath: string, assetType: AgentikitAssetType, files: string[], typeRoot?: string): StashFile;
|
|
32
|
+
export declare function generateIntents(description: string, tags: string[], name: string): string[];
|
|
25
33
|
export declare function extractDescriptionFromComments(filePath: string): string | null;
|
|
26
34
|
export declare function extractFrontmatterDescription(filePath: string): string | null;
|
|
27
35
|
export declare function extractPackageMetadata(dirPath: string): {
|
package/dist/src/metadata.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { isAssetType } from "./common";
|
|
4
|
+
import { SCRIPT_EXTENSIONS, isRelevantAssetFile, deriveCanonicalAssetName } from "./asset-spec";
|
|
5
|
+
import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
|
|
6
|
+
import { parseMarkdownToc } from "./markdown";
|
|
3
7
|
// ── Load / Write ────────────────────────────────────────────────────────────
|
|
4
8
|
const STASH_FILENAME = ".stash.json";
|
|
5
9
|
export function stashFilePath(dirPath) {
|
|
@@ -35,7 +39,7 @@ export function validateStashEntry(entry) {
|
|
|
35
39
|
const e = entry;
|
|
36
40
|
if (typeof e.name !== "string" || !e.name)
|
|
37
41
|
return null;
|
|
38
|
-
if (typeof e.type !== "string" || !
|
|
42
|
+
if (typeof e.type !== "string" || !isAssetType(e.type))
|
|
39
43
|
return null;
|
|
40
44
|
const result = {
|
|
41
45
|
name: e.name,
|
|
@@ -47,6 +51,11 @@ export function validateStashEntry(entry) {
|
|
|
47
51
|
result.tags = e.tags.filter((t) => typeof t === "string");
|
|
48
52
|
if (Array.isArray(e.examples))
|
|
49
53
|
result.examples = e.examples.filter((x) => typeof x === "string");
|
|
54
|
+
if (Array.isArray(e.intents)) {
|
|
55
|
+
const filtered = e.intents.filter((s) => typeof s === "string" && s.trim().length > 0);
|
|
56
|
+
if (filtered.length > 0)
|
|
57
|
+
result.intents = filtered;
|
|
58
|
+
}
|
|
50
59
|
if (typeof e.intent === "object" && e.intent !== null) {
|
|
51
60
|
const intent = e.intent;
|
|
52
61
|
result.intent = {};
|
|
@@ -61,62 +70,176 @@ export function validateStashEntry(entry) {
|
|
|
61
70
|
result.entry = e.entry;
|
|
62
71
|
if (e.generated === true)
|
|
63
72
|
result.generated = true;
|
|
73
|
+
if (e.quality === "generated" || e.quality === "curated")
|
|
74
|
+
result.quality = e.quality;
|
|
75
|
+
if (typeof e.confidence === "number" && Number.isFinite(e.confidence))
|
|
76
|
+
result.confidence = Math.max(0, Math.min(1, e.confidence));
|
|
77
|
+
if (typeof e.source === "string" && ["package", "frontmatter", "comments", "filename", "manual", "llm"].includes(e.source)) {
|
|
78
|
+
result.source = e.source;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(e.aliases)) {
|
|
81
|
+
const filtered = e.aliases.filter((a) => typeof a === "string" && a.trim().length > 0);
|
|
82
|
+
if (filtered.length > 0)
|
|
83
|
+
result.aliases = normalizeTerms(filtered);
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(e.toc)) {
|
|
86
|
+
const validated = e.toc.filter((h) => {
|
|
87
|
+
if (typeof h !== "object" || h === null)
|
|
88
|
+
return false;
|
|
89
|
+
const rec = h;
|
|
90
|
+
return typeof rec.level === "number"
|
|
91
|
+
&& typeof rec.text === "string"
|
|
92
|
+
&& typeof rec.line === "number";
|
|
93
|
+
});
|
|
94
|
+
if (validated.length > 0)
|
|
95
|
+
result.toc = validated;
|
|
96
|
+
}
|
|
64
97
|
return result;
|
|
65
98
|
}
|
|
66
|
-
function isValidType(type) {
|
|
67
|
-
return type === "tool" || type === "skill" || type === "command" || type === "agent";
|
|
68
|
-
}
|
|
69
99
|
// ── Metadata Generation ─────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
export function generateMetadata(dirPath, assetType, files) {
|
|
100
|
+
export function generateMetadata(dirPath, assetType, files, typeRoot = dirPath) {
|
|
72
101
|
const entries = [];
|
|
102
|
+
const pkgMeta = extractPackageMetadata(dirPath);
|
|
73
103
|
for (const file of files) {
|
|
74
104
|
const ext = path.extname(file).toLowerCase();
|
|
75
105
|
const baseName = path.basename(file, ext);
|
|
106
|
+
const fileName = path.basename(file);
|
|
76
107
|
// Skip non-relevant files
|
|
77
|
-
if (assetType
|
|
78
|
-
continue;
|
|
79
|
-
if ((assetType === "command" || assetType === "agent") && ext !== ".md")
|
|
80
|
-
continue;
|
|
81
|
-
if (assetType === "skill" && path.basename(file) !== "SKILL.md")
|
|
108
|
+
if (!isRelevantAssetFile(assetType, fileName))
|
|
82
109
|
continue;
|
|
110
|
+
const canonicalName = assetType === "skill"
|
|
111
|
+
? deriveCanonicalAssetName(assetType, typeRoot, file) ?? baseName
|
|
112
|
+
: baseName;
|
|
83
113
|
const entry = {
|
|
84
|
-
name:
|
|
114
|
+
name: canonicalName,
|
|
85
115
|
type: assetType,
|
|
86
116
|
generated: true,
|
|
117
|
+
quality: "generated",
|
|
118
|
+
confidence: 0.55,
|
|
119
|
+
source: "filename",
|
|
87
120
|
};
|
|
88
|
-
// Priority
|
|
89
|
-
const pkgMeta = extractPackageMetadata(dirPath);
|
|
121
|
+
// Priority 1: package.json metadata
|
|
90
122
|
if (pkgMeta) {
|
|
91
|
-
if (pkgMeta.description && !entry.description)
|
|
123
|
+
if (pkgMeta.description && !entry.description) {
|
|
92
124
|
entry.description = pkgMeta.description;
|
|
125
|
+
entry.source = "package";
|
|
126
|
+
entry.confidence = 0.8;
|
|
127
|
+
}
|
|
93
128
|
if (pkgMeta.keywords && pkgMeta.keywords.length > 0)
|
|
94
|
-
entry.tags = pkgMeta.keywords;
|
|
129
|
+
entry.tags = normalizeTerms(pkgMeta.keywords);
|
|
95
130
|
}
|
|
96
|
-
// Priority 2: Frontmatter (for .md files)
|
|
131
|
+
// Priority 2: Frontmatter (for .md files — overrides package.json description)
|
|
97
132
|
if (ext === ".md") {
|
|
98
133
|
const fm = extractFrontmatterDescription(file);
|
|
99
|
-
if (fm)
|
|
134
|
+
if (fm) {
|
|
100
135
|
entry.description = fm;
|
|
136
|
+
entry.source = "frontmatter";
|
|
137
|
+
entry.confidence = 0.9;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Knowledge entries: generate TOC from headings
|
|
141
|
+
if (assetType === "knowledge") {
|
|
142
|
+
try {
|
|
143
|
+
const mdContent = fs.readFileSync(file, "utf8");
|
|
144
|
+
const toc = parseMarkdownToc(mdContent);
|
|
145
|
+
if (toc.headings.length > 0)
|
|
146
|
+
entry.toc = toc.headings;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Non-fatal: skip TOC if file can't be read
|
|
150
|
+
}
|
|
101
151
|
}
|
|
102
|
-
// Priority
|
|
152
|
+
// Priority 3: Code comments (for script files)
|
|
103
153
|
if (SCRIPT_EXTENSIONS.has(ext) && ext !== ".md") {
|
|
104
154
|
const commentDesc = extractDescriptionFromComments(file);
|
|
105
|
-
if (commentDesc && !entry.description)
|
|
155
|
+
if (commentDesc && !entry.description) {
|
|
106
156
|
entry.description = commentDesc;
|
|
157
|
+
entry.source = "comments";
|
|
158
|
+
entry.confidence = 0.7;
|
|
159
|
+
}
|
|
107
160
|
}
|
|
108
|
-
// Priority
|
|
161
|
+
// Priority 4: Filename heuristics (fallback)
|
|
109
162
|
if (!entry.description) {
|
|
110
163
|
entry.description = fileNameToDescription(baseName);
|
|
164
|
+
entry.source = "filename";
|
|
165
|
+
entry.confidence = Math.min(entry.confidence ?? 0.55, 0.55);
|
|
111
166
|
}
|
|
112
167
|
if (!entry.tags || entry.tags.length === 0) {
|
|
113
168
|
entry.tags = extractTagsFromPath(file, dirPath);
|
|
114
169
|
}
|
|
170
|
+
entry.tags = normalizeTerms(entry.tags ?? []);
|
|
171
|
+
entry.aliases = buildAliases(canonicalName, entry.tags);
|
|
172
|
+
// Intents are only generated when LLM is configured (via enhanceStashWithLlm)
|
|
173
|
+
// Heuristic intents are too noisy to be useful for search quality
|
|
115
174
|
entry.entry = path.basename(file);
|
|
116
175
|
entries.push(entry);
|
|
117
176
|
}
|
|
118
177
|
return { entries };
|
|
119
178
|
}
|
|
179
|
+
function normalizeTerms(values) {
|
|
180
|
+
const normalized = new Set();
|
|
181
|
+
for (const value of values) {
|
|
182
|
+
const cleaned = value.toLowerCase().replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
183
|
+
if (!cleaned)
|
|
184
|
+
continue;
|
|
185
|
+
normalized.add(cleaned);
|
|
186
|
+
if (cleaned.endsWith("s") && cleaned.length > 3) {
|
|
187
|
+
normalized.add(cleaned.slice(0, -1));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return Array.from(normalized);
|
|
191
|
+
}
|
|
192
|
+
function buildAliases(name, tags) {
|
|
193
|
+
const aliases = new Set();
|
|
194
|
+
const spaced = name.replace(/[-_]+/g, " ").trim().toLowerCase();
|
|
195
|
+
if (spaced && spaced !== name.toLowerCase())
|
|
196
|
+
aliases.add(spaced);
|
|
197
|
+
if (tags.length > 1)
|
|
198
|
+
aliases.add(tags.join(" "));
|
|
199
|
+
return Array.from(aliases);
|
|
200
|
+
}
|
|
201
|
+
// ── Intent Generation ────────────────────────────────────────────────────────
|
|
202
|
+
export function generateIntents(description, tags, name) {
|
|
203
|
+
const intents = new Set();
|
|
204
|
+
// Split name on separators to extract tokens and potential verb
|
|
205
|
+
const nameTokens = name
|
|
206
|
+
.replace(/[-_]+/g, " ")
|
|
207
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
208
|
+
.toLowerCase()
|
|
209
|
+
.trim()
|
|
210
|
+
.split(/\s+/)
|
|
211
|
+
.filter((t) => t.length > 1);
|
|
212
|
+
// Intent from name as phrase (e.g. "summarize diff")
|
|
213
|
+
const namePhrase = nameTokens.join(" ");
|
|
214
|
+
if (namePhrase.length > 2)
|
|
215
|
+
intents.add(namePhrase);
|
|
216
|
+
// Intent from description (lowercased)
|
|
217
|
+
const desc = description.toLowerCase().trim();
|
|
218
|
+
if (desc.length > 2)
|
|
219
|
+
intents.add(desc);
|
|
220
|
+
// Combine first name token (potential verb) with tags
|
|
221
|
+
// e.g. name "summarize-diff", tags ["git"] → "summarize git diff"
|
|
222
|
+
if (nameTokens.length >= 1 && tags.length > 0) {
|
|
223
|
+
const verb = nameTokens[0];
|
|
224
|
+
const rest = nameTokens.slice(1).join(" ");
|
|
225
|
+
for (const tag of tags) {
|
|
226
|
+
const tagLower = tag.toLowerCase();
|
|
227
|
+
// verb + tag + rest (e.g. "summarize git diff")
|
|
228
|
+
const parts = [verb, tagLower, rest].filter((p) => p.length > 0);
|
|
229
|
+
const phrase = parts.join(" ");
|
|
230
|
+
if (phrase !== namePhrase && phrase.length > 2)
|
|
231
|
+
intents.add(phrase);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Join tag pairs (e.g. ["git", "diff"] → "git diff")
|
|
235
|
+
if (tags.length >= 2) {
|
|
236
|
+
const tagPhrase = tags.map((t) => t.toLowerCase()).join(" ");
|
|
237
|
+
if (tagPhrase.length > 2)
|
|
238
|
+
intents.add(tagPhrase);
|
|
239
|
+
}
|
|
240
|
+
// Cap at 8 intents
|
|
241
|
+
return Array.from(intents).slice(0, 8);
|
|
242
|
+
}
|
|
120
243
|
export function extractDescriptionFromComments(filePath) {
|
|
121
244
|
let content;
|
|
122
245
|
try {
|
|
@@ -170,15 +293,8 @@ export function extractFrontmatterDescription(filePath) {
|
|
|
170
293
|
catch {
|
|
171
294
|
return null;
|
|
172
295
|
}
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
return null;
|
|
176
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
177
|
-
const m = line.match(/^description:\s*"?(.+?)"?\s*$/);
|
|
178
|
-
if (m)
|
|
179
|
-
return m[1];
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
296
|
+
const parsed = parseFrontmatter(content);
|
|
297
|
+
return toStringOrUndefined(parsed.data.description) ?? null;
|
|
182
298
|
}
|
|
183
299
|
export function extractPackageMetadata(dirPath) {
|
|
184
300
|
const pkgPath = path.join(dirPath, "package.json");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface EnsureRgResult {
|
|
2
|
+
rgPath: string;
|
|
3
|
+
installed: boolean;
|
|
4
|
+
version: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Ensure ripgrep is available. If not found on PATH or in stash/bin,
|
|
8
|
+
* download and install it to stash/bin.
|
|
9
|
+
*
|
|
10
|
+
* Returns the path to the ripgrep binary and whether it was newly installed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureRg(stashDir: string): EnsureRgResult;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { IS_WINDOWS } from "./common";
|
|
5
|
+
import { RG_BINARY, resolveRg } from "./ripgrep-resolve";
|
|
6
|
+
/**
|
|
7
|
+
* Platform and architecture detection for ripgrep binary downloads.
|
|
8
|
+
*/
|
|
9
|
+
function getRgPlatformTarget() {
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const arch = process.arch;
|
|
12
|
+
if (platform === "linux" && arch === "x64") {
|
|
13
|
+
return { platform: "x86_64-unknown-linux-musl", arch: "x64", ext: ".tar.gz" };
|
|
14
|
+
}
|
|
15
|
+
if (platform === "linux" && arch === "arm64") {
|
|
16
|
+
return { platform: "aarch64-unknown-linux-gnu", arch: "arm64", ext: ".tar.gz" };
|
|
17
|
+
}
|
|
18
|
+
if (platform === "darwin" && arch === "x64") {
|
|
19
|
+
return { platform: "x86_64-apple-darwin", arch: "x64", ext: ".tar.gz" };
|
|
20
|
+
}
|
|
21
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
22
|
+
return { platform: "aarch64-apple-darwin", arch: "arm64", ext: ".tar.gz" };
|
|
23
|
+
}
|
|
24
|
+
if (platform === "win32" && arch === "x64") {
|
|
25
|
+
return { platform: "x86_64-pc-windows-msvc", arch: "x64", ext: ".zip" };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const RG_VERSION = "14.1.1";
|
|
30
|
+
/**
|
|
31
|
+
* Ensure ripgrep is available. If not found on PATH or in stash/bin,
|
|
32
|
+
* download and install it to stash/bin.
|
|
33
|
+
*
|
|
34
|
+
* Returns the path to the ripgrep binary and whether it was newly installed.
|
|
35
|
+
*/
|
|
36
|
+
export function ensureRg(stashDir) {
|
|
37
|
+
// Already available?
|
|
38
|
+
const existing = resolveRg(stashDir);
|
|
39
|
+
if (existing) {
|
|
40
|
+
return { rgPath: existing, installed: false, version: getRgVersion(existing) };
|
|
41
|
+
}
|
|
42
|
+
// Determine platform
|
|
43
|
+
const target = getRgPlatformTarget();
|
|
44
|
+
if (!target) {
|
|
45
|
+
throw new Error(`Unsupported platform for ripgrep auto-install: ${process.platform}/${process.arch}. ` +
|
|
46
|
+
`Install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`);
|
|
47
|
+
}
|
|
48
|
+
const binDir = path.join(stashDir, "bin");
|
|
49
|
+
if (!fs.existsSync(binDir)) {
|
|
50
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
const archiveName = `ripgrep-${RG_VERSION}-${target.platform}`;
|
|
53
|
+
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${archiveName}${target.ext}`;
|
|
54
|
+
const destBinary = path.join(binDir, RG_BINARY);
|
|
55
|
+
if (target.ext === ".tar.gz") {
|
|
56
|
+
downloadAndExtractTarGz(url, archiveName, destBinary);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
downloadAndExtractZip(url, archiveName, destBinary);
|
|
60
|
+
}
|
|
61
|
+
// Make executable
|
|
62
|
+
if (!IS_WINDOWS) {
|
|
63
|
+
fs.chmodSync(destBinary, 0o755);
|
|
64
|
+
}
|
|
65
|
+
return { rgPath: destBinary, installed: true, version: RG_VERSION };
|
|
66
|
+
}
|
|
67
|
+
function downloadAndExtractTarGz(url, archiveName, destBinary) {
|
|
68
|
+
const destDir = path.dirname(destBinary);
|
|
69
|
+
const tmpTarGz = path.join(destDir, "rg-download.tar.gz");
|
|
70
|
+
try {
|
|
71
|
+
// Download archive to a temporary file without using a shell
|
|
72
|
+
const curlResult = spawnSync("curl", ["-fsSL", "-o", tmpTarGz, url], {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
timeout: 60_000,
|
|
75
|
+
});
|
|
76
|
+
if (curlResult.status !== 0) {
|
|
77
|
+
const err = curlResult.stderr?.trim() || curlResult.error?.message || "unknown error";
|
|
78
|
+
throw new Error(`Failed to download ripgrep from ${url}: ${err}`);
|
|
79
|
+
}
|
|
80
|
+
// Extract the specific binary from the archive into destDir
|
|
81
|
+
const tarResult = spawnSync("tar", [
|
|
82
|
+
"xzf",
|
|
83
|
+
tmpTarGz,
|
|
84
|
+
"--strip-components=1",
|
|
85
|
+
"-C",
|
|
86
|
+
destDir,
|
|
87
|
+
`${archiveName}/rg`,
|
|
88
|
+
], {
|
|
89
|
+
encoding: "utf8",
|
|
90
|
+
timeout: 60_000,
|
|
91
|
+
});
|
|
92
|
+
if (tarResult.status !== 0) {
|
|
93
|
+
const err = tarResult.stderr?.trim() || tarResult.error?.message || "unknown error";
|
|
94
|
+
throw new Error(`Failed to extract ripgrep from ${url}: ${err}`);
|
|
95
|
+
}
|
|
96
|
+
if (!fs.existsSync(destBinary)) {
|
|
97
|
+
throw new Error(`ripgrep binary not found at ${destBinary} after extraction`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
// Best-effort cleanup of temporary archive
|
|
102
|
+
try {
|
|
103
|
+
if (fs.existsSync(tmpTarGz)) {
|
|
104
|
+
fs.unlinkSync(tmpTarGz);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// ignore cleanup errors
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function downloadAndExtractZip(url, archiveName, destBinary) {
|
|
113
|
+
const destDir = path.dirname(destBinary);
|
|
114
|
+
const tmpZip = path.join(destDir, "rg-download.zip");
|
|
115
|
+
const expandedDir = path.join(destDir, archiveName);
|
|
116
|
+
try {
|
|
117
|
+
// Download
|
|
118
|
+
const dlResult = spawnSync("curl", ["-fsSL", "-o", tmpZip, url], {
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
timeout: 60_000,
|
|
121
|
+
});
|
|
122
|
+
if (dlResult.status !== 0) {
|
|
123
|
+
throw new Error(dlResult.stderr?.trim() || "download failed");
|
|
124
|
+
}
|
|
125
|
+
// Extract the zip archive using separate spawnSync calls with argument arrays
|
|
126
|
+
// to avoid shell injection via path interpolation in PowerShell -Command strings
|
|
127
|
+
const expandResult = spawnSync("powershell", [
|
|
128
|
+
"-Command",
|
|
129
|
+
"Expand-Archive",
|
|
130
|
+
"-Path", tmpZip,
|
|
131
|
+
"-DestinationPath", destDir,
|
|
132
|
+
"-Force",
|
|
133
|
+
], {
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
timeout: 60_000,
|
|
136
|
+
});
|
|
137
|
+
if (expandResult.status !== 0) {
|
|
138
|
+
throw new Error(expandResult.stderr?.trim() || "extraction failed");
|
|
139
|
+
}
|
|
140
|
+
const srcRgExe = path.join(destDir, archiveName, "rg.exe");
|
|
141
|
+
const moveResult = spawnSync("powershell", [
|
|
142
|
+
"-Command",
|
|
143
|
+
"Move-Item",
|
|
144
|
+
"-Force",
|
|
145
|
+
"-Path", srcRgExe,
|
|
146
|
+
"-Destination", destBinary,
|
|
147
|
+
], {
|
|
148
|
+
encoding: "utf8",
|
|
149
|
+
timeout: 60_000,
|
|
150
|
+
});
|
|
151
|
+
if (moveResult.status !== 0) {
|
|
152
|
+
throw new Error(moveResult.stderr?.trim() || "move failed");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
if (fs.existsSync(tmpZip))
|
|
157
|
+
fs.unlinkSync(tmpZip);
|
|
158
|
+
if (fs.existsSync(expandedDir))
|
|
159
|
+
fs.rmSync(expandedDir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function getRgVersion(rgPath) {
|
|
163
|
+
const result = spawnSync(rgPath, ["--version"], { encoding: "utf8", timeout: 5_000 });
|
|
164
|
+
if (result.status === 0 && result.stdout) {
|
|
165
|
+
const match = result.stdout.match(/ripgrep\s+([\d.]+)/);
|
|
166
|
+
return match ? match[1] : "unknown";
|
|
167
|
+
}
|
|
168
|
+
return "unknown";
|
|
169
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const RG_BINARY: string;
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the path to a usable ripgrep binary.
|
|
4
|
+
* Checks in order:
|
|
5
|
+
* 1. stashDir/bin/rg
|
|
6
|
+
* 2. system PATH (rg)
|
|
7
|
+
* Returns null if ripgrep is not available.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveRg(stashDir?: string): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Check if ripgrep is available (either in stash/bin or system PATH).
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRgAvailable(stashDir?: string): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { IS_WINDOWS } from "./common";
|
|
4
|
+
export const RG_BINARY = IS_WINDOWS ? "rg.exe" : "rg";
|
|
5
|
+
function canExecute(filePath) {
|
|
6
|
+
if (!fs.existsSync(filePath))
|
|
7
|
+
return false;
|
|
8
|
+
if (IS_WINDOWS)
|
|
9
|
+
return true;
|
|
10
|
+
try {
|
|
11
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function resolveFromPath() {
|
|
19
|
+
const rawPath = process.env.PATH;
|
|
20
|
+
if (!rawPath)
|
|
21
|
+
return null;
|
|
22
|
+
const pathEntries = rawPath.split(path.delimiter).filter(Boolean);
|
|
23
|
+
if (IS_WINDOWS) {
|
|
24
|
+
const pathext = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM")
|
|
25
|
+
.split(";")
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.map((ext) => ext.toLowerCase());
|
|
28
|
+
for (const entry of pathEntries) {
|
|
29
|
+
const directCandidate = path.join(entry, "rg");
|
|
30
|
+
if (canExecute(directCandidate))
|
|
31
|
+
return directCandidate;
|
|
32
|
+
for (const ext of pathext) {
|
|
33
|
+
const candidate = path.join(entry, `rg${ext}`);
|
|
34
|
+
if (canExecute(candidate))
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
for (const entry of pathEntries) {
|
|
41
|
+
const candidate = path.join(entry, "rg");
|
|
42
|
+
if (canExecute(candidate))
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the path to a usable ripgrep binary.
|
|
49
|
+
* Checks in order:
|
|
50
|
+
* 1. stashDir/bin/rg
|
|
51
|
+
* 2. system PATH (rg)
|
|
52
|
+
* Returns null if ripgrep is not available.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveRg(stashDir) {
|
|
55
|
+
// Check stash bin directory first
|
|
56
|
+
if (stashDir) {
|
|
57
|
+
const stashRg = path.join(stashDir, "bin", RG_BINARY);
|
|
58
|
+
if (canExecute(stashRg))
|
|
59
|
+
return stashRg;
|
|
60
|
+
}
|
|
61
|
+
return resolveFromPath();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if ripgrep is available (either in stash/bin or system PATH).
|
|
65
|
+
*/
|
|
66
|
+
export function isRgAvailable(stashDir) {
|
|
67
|
+
return resolveRg(stashDir) !== null;
|
|
68
|
+
}
|
package/dist/src/ripgrep.d.ts
CHANGED
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* 1. stashDir/bin/rg
|
|
5
|
-
* 2. system PATH (rg)
|
|
6
|
-
* Returns null if ripgrep is not available.
|
|
7
|
-
*/
|
|
8
|
-
export declare function resolveRg(stashDir?: string): string | null;
|
|
9
|
-
/**
|
|
10
|
-
* Check if ripgrep is available (either in stash/bin or system PATH).
|
|
11
|
-
*/
|
|
12
|
-
export declare function isRgAvailable(stashDir?: string): boolean;
|
|
13
|
-
export interface RgCandidateResult {
|
|
14
|
-
matchedFiles: string[];
|
|
15
|
-
usedRg: boolean;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Use ripgrep to find .stash.json files that match query tokens.
|
|
19
|
-
* Returns paths to matching .stash.json files.
|
|
20
|
-
*
|
|
21
|
-
* If ripgrep is not available or the query is empty, returns null
|
|
22
|
-
* to signal that the caller should skip pre-filtering.
|
|
23
|
-
*/
|
|
24
|
-
export declare function rgFilterCandidates(query: string, searchDir: string, stashDir?: string): RgCandidateResult | null;
|
|
25
|
-
export interface EnsureRgResult {
|
|
26
|
-
rgPath: string;
|
|
27
|
-
installed: boolean;
|
|
28
|
-
version: string;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Ensure ripgrep is available. If not found on PATH or in stash/bin,
|
|
32
|
-
* download and install it to stash/bin.
|
|
33
|
-
*
|
|
34
|
-
* Returns the path to the ripgrep binary and whether it was newly installed.
|
|
35
|
-
*/
|
|
36
|
-
export declare function ensureRg(stashDir: string): EnsureRgResult;
|
|
1
|
+
export { resolveRg, isRgAvailable } from "./ripgrep-resolve";
|
|
2
|
+
export { ensureRg } from "./ripgrep-install";
|
|
3
|
+
export type { EnsureRgResult } from "./ripgrep-install";
|