agentikit 0.0.12 → 0.0.14

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.
Files changed (146) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +186 -100
  3. package/dist/cli.js +671 -0
  4. package/dist/common.js +192 -0
  5. package/dist/{src/config-cli.js → config-cli.js} +14 -6
  6. package/dist/{src/config.js → config.js} +92 -24
  7. package/dist/{src/db.js → db.js} +109 -35
  8. package/dist/{src/embedder.js → embedder.js} +57 -2
  9. package/dist/file-context.js +158 -0
  10. package/dist/{src/handlers → handlers}/command-handler.js +2 -0
  11. package/dist/{src/handlers → handlers}/index.js +0 -6
  12. package/dist/{src/indexer.js → indexer.js} +34 -10
  13. package/dist/init.js +43 -0
  14. package/dist/lockfile.js +55 -0
  15. package/dist/matchers.js +157 -0
  16. package/dist/{src/metadata.js → metadata.js} +12 -1
  17. package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
  18. package/dist/paths.js +82 -0
  19. package/dist/{src/registry-install.js → registry-install.js} +145 -17
  20. package/dist/{src/registry-resolve.js → registry-resolve.js} +178 -18
  21. package/dist/{src/registry-search.js → registry-search.js} +8 -16
  22. package/dist/renderers.js +276 -0
  23. package/dist/{src/ripgrep-install.js → ripgrep-install.js} +5 -5
  24. package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
  25. package/dist/self-update.js +220 -0
  26. package/dist/{src/stash-add.js → stash-add.js} +11 -2
  27. package/dist/stash-clone.js +115 -0
  28. package/dist/{src/stash-registry.js → stash-registry.js} +15 -41
  29. package/dist/{src/stash-search.js → stash-search.js} +67 -55
  30. package/dist/{src/stash-show.js → stash-show.js} +30 -3
  31. package/dist/{src/stash-source.js → stash-source.js} +56 -9
  32. package/dist/submit.js +552 -0
  33. package/dist/{src/walker.js → walker.js} +38 -0
  34. package/package.json +7 -16
  35. package/dist/index.d.ts +0 -28
  36. package/dist/index.js +0 -15
  37. package/dist/src/asset-spec.d.ts +0 -16
  38. package/dist/src/asset-type-handler.d.ts +0 -27
  39. package/dist/src/cli.d.ts +0 -2
  40. package/dist/src/cli.js +0 -399
  41. package/dist/src/common.d.ts +0 -13
  42. package/dist/src/common.js +0 -60
  43. package/dist/src/config-cli.d.ts +0 -9
  44. package/dist/src/config.d.ts +0 -50
  45. package/dist/src/db.d.ts +0 -46
  46. package/dist/src/embedder.d.ts +0 -10
  47. package/dist/src/frontmatter.d.ts +0 -30
  48. package/dist/src/github.d.ts +0 -4
  49. package/dist/src/handlers/agent-handler.d.ts +0 -2
  50. package/dist/src/handlers/command-handler.d.ts +0 -2
  51. package/dist/src/handlers/index.d.ts +0 -6
  52. package/dist/src/handlers/knowledge-handler.d.ts +0 -2
  53. package/dist/src/handlers/markdown-helpers.d.ts +0 -7
  54. package/dist/src/handlers/script-handler.d.ts +0 -2
  55. package/dist/src/handlers/skill-handler.d.ts +0 -2
  56. package/dist/src/handlers/tool-handler.d.ts +0 -2
  57. package/dist/src/indexer.d.ts +0 -22
  58. package/dist/src/init.d.ts +0 -19
  59. package/dist/src/init.js +0 -99
  60. package/dist/src/llm.d.ts +0 -15
  61. package/dist/src/markdown.d.ts +0 -18
  62. package/dist/src/metadata.d.ts +0 -41
  63. package/dist/src/origin-resolve.d.ts +0 -19
  64. package/dist/src/registry-install.d.ts +0 -11
  65. package/dist/src/registry-resolve.d.ts +0 -3
  66. package/dist/src/registry-search.d.ts +0 -27
  67. package/dist/src/registry-types.d.ts +0 -62
  68. package/dist/src/ripgrep-install.d.ts +0 -12
  69. package/dist/src/ripgrep-resolve.d.ts +0 -13
  70. package/dist/src/ripgrep.d.ts +0 -3
  71. package/dist/src/similarity.js +0 -211
  72. package/dist/src/stash-add.d.ts +0 -4
  73. package/dist/src/stash-clone.d.ts +0 -22
  74. package/dist/src/stash-clone.js +0 -83
  75. package/dist/src/stash-ref.d.ts +0 -31
  76. package/dist/src/stash-registry.d.ts +0 -18
  77. package/dist/src/stash-resolve.d.ts +0 -2
  78. package/dist/src/stash-search.d.ts +0 -8
  79. package/dist/src/stash-show.d.ts +0 -5
  80. package/dist/src/stash-source.d.ts +0 -24
  81. package/dist/src/stash-types.d.ts +0 -227
  82. package/dist/src/stash.d.ts +0 -16
  83. package/dist/src/stash.js +0 -9
  84. package/dist/src/tool-runner.d.ts +0 -35
  85. package/dist/src/walker.d.ts +0 -19
  86. package/src/asset-spec.ts +0 -85
  87. package/src/asset-type-handler.ts +0 -77
  88. package/src/cli.ts +0 -427
  89. package/src/common.ts +0 -76
  90. package/src/config-cli.ts +0 -499
  91. package/src/config.ts +0 -305
  92. package/src/db.ts +0 -411
  93. package/src/embedder.ts +0 -128
  94. package/src/frontmatter.ts +0 -95
  95. package/src/github.ts +0 -21
  96. package/src/handlers/agent-handler.ts +0 -32
  97. package/src/handlers/command-handler.ts +0 -29
  98. package/src/handlers/index.ts +0 -25
  99. package/src/handlers/knowledge-handler.ts +0 -62
  100. package/src/handlers/markdown-helpers.ts +0 -19
  101. package/src/handlers/script-handler.ts +0 -92
  102. package/src/handlers/skill-handler.ts +0 -37
  103. package/src/handlers/tool-handler.ts +0 -71
  104. package/src/indexer.ts +0 -392
  105. package/src/init.ts +0 -114
  106. package/src/llm.ts +0 -125
  107. package/src/markdown.ts +0 -106
  108. package/src/metadata.ts +0 -333
  109. package/src/origin-resolve.ts +0 -67
  110. package/src/registry-install.ts +0 -361
  111. package/src/registry-resolve.ts +0 -341
  112. package/src/registry-search.ts +0 -335
  113. package/src/registry-types.ts +0 -72
  114. package/src/ripgrep-install.ts +0 -200
  115. package/src/ripgrep-resolve.ts +0 -72
  116. package/src/ripgrep.ts +0 -3
  117. package/src/stash-add.ts +0 -63
  118. package/src/stash-clone.ts +0 -127
  119. package/src/stash-ref.ts +0 -99
  120. package/src/stash-registry.ts +0 -259
  121. package/src/stash-resolve.ts +0 -50
  122. package/src/stash-search.ts +0 -613
  123. package/src/stash-show.ts +0 -55
  124. package/src/stash-source.ts +0 -103
  125. package/src/stash-types.ts +0 -231
  126. package/src/stash.ts +0 -39
  127. package/src/tool-runner.ts +0 -142
  128. package/src/walker.ts +0 -53
  129. /package/dist/{src/asset-spec.js → asset-spec.js} +0 -0
  130. /package/dist/{src/asset-type-handler.js → asset-type-handler.js} +0 -0
  131. /package/dist/{src/frontmatter.js → frontmatter.js} +0 -0
  132. /package/dist/{src/github.js → github.js} +0 -0
  133. /package/dist/{src/handlers → handlers}/agent-handler.js +0 -0
  134. /package/dist/{src/handlers → handlers}/knowledge-handler.js +0 -0
  135. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  136. /package/dist/{src/handlers → handlers}/script-handler.js +0 -0
  137. /package/dist/{src/handlers → handlers}/skill-handler.js +0 -0
  138. /package/dist/{src/handlers → handlers}/tool-handler.js +0 -0
  139. /package/dist/{src/llm.js → llm.js} +0 -0
  140. /package/dist/{src/markdown.js → markdown.js} +0 -0
  141. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  142. /package/dist/{src/ripgrep.js → ripgrep.js} +0 -0
  143. /package/dist/{src/stash-ref.js → stash-ref.js} +0 -0
  144. /package/dist/{src/stash-resolve.js → stash-resolve.js} +0 -0
  145. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
  146. /package/dist/{src/tool-runner.js → tool-runner.js} +0 -0
@@ -1,335 +0,0 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
- import { fetchWithTimeout } from "./common"
4
- import type { RegistrySearchHit, RegistrySearchResponse } from "./registry-types"
5
-
6
- // ── Constants ───────────────────────────────────────────────────────────────
7
-
8
- /** Default registry index URL. Override via config or AKM_REGISTRY_URL env var. */
9
- const DEFAULT_REGISTRY_URL =
10
- "https://raw.githubusercontent.com/itlackey/agentikit-registry/main/index.json"
11
-
12
- /** Cache TTL in milliseconds (1 hour). */
13
- const CACHE_TTL_MS = 60 * 60 * 1000
14
-
15
- /** Maximum age before cache is considered stale but still usable as fallback (7 days). */
16
- const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000
17
-
18
- // ── Types ───────────────────────────────────────────────────────────────────
19
-
20
- export interface RegistryIndex {
21
- version: number
22
- updatedAt: string
23
- kits: RegistryKitEntry[]
24
- }
25
-
26
- export interface RegistryKitEntry {
27
- id: string
28
- name: string
29
- description?: string
30
- ref: string
31
- source: "npm" | "github" | "git"
32
- homepage?: string
33
- tags?: string[]
34
- assetTypes?: string[]
35
- author?: string
36
- license?: string
37
- latestVersion?: string
38
- /** Whether this entry was manually reviewed and approved */
39
- curated?: boolean
40
- }
41
-
42
- export interface RegistrySearchOptions {
43
- limit?: number
44
- /** Override registry URL(s). Accepts a single URL or an array. */
45
- registryUrls?: string | string[]
46
- }
47
-
48
- // ── Public API ──────────────────────────────────────────────────────────────
49
-
50
- export async function searchRegistry(
51
- query: string,
52
- options?: RegistrySearchOptions,
53
- ): Promise<RegistrySearchResponse> {
54
- const trimmed = query.trim()
55
- if (!trimmed) {
56
- return { query: "", hits: [], warnings: [] }
57
- }
58
-
59
- const limit = clampLimit(options?.limit)
60
- const urls = resolveRegistryUrls(options?.registryUrls)
61
- const warnings: string[] = []
62
-
63
- // Load index from all configured registries, merge kits
64
- const allKits: RegistryKitEntry[] = []
65
- for (const url of urls) {
66
- try {
67
- const index = await loadIndex(url)
68
- if (index) {
69
- allKits.push(...index.kits)
70
- }
71
- } catch (err) {
72
- warnings.push(`Registry ${url}: ${toErrorMessage(err)}`)
73
- }
74
- }
75
-
76
- // Score and rank
77
- const hits = scoreKits(allKits, trimmed, limit)
78
-
79
- return { query: trimmed, hits, warnings }
80
- }
81
-
82
- // ── Index loading with cache ────────────────────────────────────────────────
83
-
84
- async function loadIndex(url: string): Promise<RegistryIndex | null> {
85
- const cachePath = indexCachePath(url)
86
- const cached = readCachedIndex(cachePath)
87
-
88
- // Fresh cache: return immediately
89
- if (cached && !isCacheExpired(cached.mtime)) {
90
- return cached.index
91
- }
92
-
93
- // Try to fetch fresh index
94
- try {
95
- const response = await fetchWithTimeout(url, undefined, 10_000)
96
- if (!response.ok) {
97
- throw new Error(`HTTP ${response.status}`)
98
- }
99
- const data = (await response.json()) as unknown
100
- const index = parseRegistryIndex(data)
101
- if (index) {
102
- writeCachedIndex(cachePath, index)
103
- return index
104
- }
105
- throw new Error("Invalid registry index format")
106
- } catch (err) {
107
- // Fetch failed — use stale cache if available
108
- if (cached && !isCacheStale(cached.mtime)) {
109
- return cached.index
110
- }
111
- throw err
112
- }
113
- }
114
-
115
- function readCachedIndex(
116
- cachePath: string,
117
- ): { index: RegistryIndex; mtime: number } | null {
118
- try {
119
- const stat = fs.statSync(cachePath)
120
- const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"))
121
- const index = parseRegistryIndex(raw)
122
- if (!index) return null
123
- return { index, mtime: stat.mtimeMs }
124
- } catch {
125
- return null
126
- }
127
- }
128
-
129
- function writeCachedIndex(cachePath: string, index: RegistryIndex): void {
130
- try {
131
- const dir = path.dirname(cachePath)
132
- fs.mkdirSync(dir, { recursive: true })
133
- const tmpPath = cachePath + `.tmp.${process.pid}`
134
- fs.writeFileSync(tmpPath, JSON.stringify(index), "utf8")
135
- fs.renameSync(tmpPath, cachePath)
136
- } catch {
137
- // Best-effort caching — don't fail the search if we can't write
138
- }
139
- }
140
-
141
- function indexCachePath(url: string): string {
142
- const cacheRoot = resolveCacheDir()
143
- // Deterministic filename from URL
144
- const slug = url
145
- .replace(/[^a-zA-Z0-9]+/g, "-")
146
- .replace(/^-+|-+$/g, "")
147
- .slice(0, 120)
148
- return path.join(cacheRoot, "registry-index", `${slug}.json`)
149
- }
150
-
151
- function resolveCacheDir(): string {
152
- const xdgCache = process.env.XDG_CACHE_HOME?.trim()
153
- if (xdgCache) return path.join(path.resolve(xdgCache), "agentikit")
154
- const home = process.env.HOME?.trim()
155
- if (!home) return path.join("/tmp", "agentikit-cache")
156
- return path.join(path.resolve(home), ".cache", "agentikit")
157
- }
158
-
159
- function isCacheExpired(mtimeMs: number): boolean {
160
- return Date.now() - mtimeMs > CACHE_TTL_MS
161
- }
162
-
163
- function isCacheStale(mtimeMs: number): boolean {
164
- return Date.now() - mtimeMs > CACHE_STALE_MS
165
- }
166
-
167
- // ── Index parsing ───────────────────────────────────────────────────────────
168
-
169
- function parseRegistryIndex(data: unknown): RegistryIndex | null {
170
- if (typeof data !== "object" || data === null || Array.isArray(data)) return null
171
- const obj = data as Record<string, unknown>
172
-
173
- if (typeof obj.version !== "number" || obj.version !== 1) return null
174
- if (typeof obj.updatedAt !== "string") return null
175
- if (!Array.isArray(obj.kits)) return null
176
-
177
- const kits = obj.kits.flatMap((raw): RegistryKitEntry[] => {
178
- const kit = parseKitEntry(raw)
179
- return kit ? [kit] : []
180
- })
181
-
182
- return { version: 1, updatedAt: obj.updatedAt, kits }
183
- }
184
-
185
- function parseKitEntry(raw: unknown): RegistryKitEntry | null {
186
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null
187
- const obj = raw as Record<string, unknown>
188
-
189
- const id = asString(obj.id)
190
- const name = asString(obj.name)
191
- const ref = asString(obj.ref)
192
- const source = asSource(obj.source)
193
- if (!id || !name || !ref || !source) return null
194
-
195
- return {
196
- id,
197
- name,
198
- ref,
199
- source,
200
- description: asString(obj.description),
201
- homepage: asString(obj.homepage),
202
- tags: asStringArray(obj.tags),
203
- assetTypes: asStringArray(obj.assetTypes),
204
- author: asString(obj.author),
205
- license: asString(obj.license),
206
- latestVersion: asString(obj.latestVersion),
207
- curated: obj.curated === true ? true : undefined,
208
- }
209
- }
210
-
211
- // ── Scoring ─────────────────────────────────────────────────────────────────
212
-
213
- function scoreKits(
214
- kits: RegistryKitEntry[],
215
- query: string,
216
- limit: number,
217
- ): RegistrySearchHit[] {
218
- const tokens = query
219
- .toLowerCase()
220
- .split(/\s+/)
221
- .filter(Boolean)
222
-
223
- const scored: Array<{ kit: RegistryKitEntry; score: number }> = []
224
-
225
- for (const kit of kits) {
226
- const score = scoreKit(kit, tokens)
227
- if (score > 0) {
228
- scored.push({ kit, score })
229
- }
230
- }
231
-
232
- scored.sort((a, b) => b.score - a.score)
233
-
234
- return scored.slice(0, limit).map(({ kit, score }) => toSearchHit(kit, score))
235
- }
236
-
237
- function scoreKit(kit: RegistryKitEntry, tokens: string[]): number {
238
- let score = 0
239
- const nameLower = kit.name.toLowerCase()
240
- const descLower = (kit.description ?? "").toLowerCase()
241
- const tagsLower = (kit.tags ?? []).map((t) => t.toLowerCase())
242
-
243
- for (const token of tokens) {
244
- // Exact name match is strongest signal
245
- if (nameLower === token) {
246
- score += 1.0
247
- } else if (nameLower.includes(token)) {
248
- score += 0.6
249
- }
250
-
251
- // Tag matches are high-signal (curated keywords)
252
- if (tagsLower.some((tag) => tag === token)) {
253
- score += 0.5
254
- } else if (tagsLower.some((tag) => tag.includes(token))) {
255
- score += 0.25
256
- }
257
-
258
- // Description substring
259
- if (descLower.includes(token)) {
260
- score += 0.2
261
- }
262
-
263
- // Author match
264
- if (kit.author?.toLowerCase().includes(token)) {
265
- score += 0.15
266
- }
267
- }
268
-
269
- // Normalize by token count so multi-word queries don't inflate scores
270
- return tokens.length > 0 ? score / tokens.length : 0
271
- }
272
-
273
- function toSearchHit(kit: RegistryKitEntry, score: number): RegistrySearchHit {
274
- const metadata: Record<string, string> = {}
275
- if (kit.latestVersion) metadata.version = kit.latestVersion
276
- if (kit.author) metadata.author = kit.author
277
- if (kit.license) metadata.license = kit.license
278
- if (kit.assetTypes?.length) metadata.assetTypes = kit.assetTypes.join(", ")
279
-
280
- return {
281
- source: kit.source,
282
- id: kit.id,
283
- title: kit.name,
284
- description: kit.description,
285
- ref: kit.ref,
286
- homepage: kit.homepage,
287
- score: Math.round(score * 1000) / 1000,
288
- metadata,
289
- curated: kit.curated,
290
- }
291
- }
292
-
293
- // ── Registry URL resolution ─────────────────────────────────────────────────
294
-
295
- function resolveRegistryUrls(override?: string | string[]): string[] {
296
- if (override) {
297
- const urls = Array.isArray(override) ? override : [override]
298
- return urls.filter(Boolean)
299
- }
300
-
301
- // Allow env var override (comma-separated)
302
- const envUrls = process.env.AKM_REGISTRY_URL?.trim()
303
- if (envUrls) {
304
- return envUrls.split(",").map((u) => u.trim()).filter(Boolean)
305
- }
306
-
307
- return [DEFAULT_REGISTRY_URL]
308
- }
309
-
310
- // ── Utilities ───────────────────────────────────────────────────────────────
311
-
312
- function clampLimit(limit: number | undefined): number {
313
- if (!limit || !Number.isFinite(limit)) return 20
314
- return Math.min(100, Math.max(1, Math.trunc(limit)))
315
- }
316
-
317
- function asString(value: unknown): string | undefined {
318
- return typeof value === "string" && value ? value : undefined
319
- }
320
-
321
- function asSource(value: unknown): "npm" | "github" | "git" | undefined {
322
- return value === "npm" || value === "github" || value === "git"
323
- ? value
324
- : undefined
325
- }
326
-
327
- function asStringArray(value: unknown): string[] | undefined {
328
- if (!Array.isArray(value)) return undefined
329
- const filtered = value.filter((v): v is string => typeof v === "string")
330
- return filtered.length > 0 ? filtered : undefined
331
- }
332
-
333
- function toErrorMessage(error: unknown): string {
334
- return error instanceof Error ? error.message : String(error)
335
- }
@@ -1,72 +0,0 @@
1
- export type RegistrySource = "npm" | "github" | "git"
2
-
3
- export interface RegistryRefBase {
4
- source: RegistrySource
5
- ref: string
6
- id: string
7
- }
8
-
9
- export interface ParsedNpmRef extends RegistryRefBase {
10
- source: "npm"
11
- packageName: string
12
- requestedVersionOrTag?: string
13
- }
14
-
15
- export interface ParsedGithubRef extends RegistryRefBase {
16
- source: "github"
17
- owner: string
18
- repo: string
19
- requestedRef?: string
20
- }
21
-
22
- export interface ParsedGitRef extends RegistryRefBase {
23
- source: "git"
24
- repoRoot: string
25
- sourcePath: string
26
- }
27
-
28
- export type ParsedRegistryRef = ParsedNpmRef | ParsedGithubRef | ParsedGitRef
29
-
30
- export interface ResolvedRegistryArtifact {
31
- id: string
32
- source: RegistrySource
33
- ref: string
34
- artifactUrl: string
35
- resolvedVersion?: string
36
- resolvedRevision?: string
37
- }
38
-
39
- export interface RegistryInstalledEntry {
40
- id: string
41
- source: RegistrySource
42
- ref: string
43
- resolvedVersion?: string
44
- resolvedRevision?: string
45
- artifactUrl: string
46
- stashRoot: string
47
- cacheDir: string
48
- installedAt: string
49
- }
50
-
51
- export interface RegistryInstallResult extends RegistryInstalledEntry {
52
- extractedDir: string
53
- }
54
-
55
- export interface RegistrySearchHit {
56
- source: RegistrySource
57
- id: string
58
- title: string
59
- description?: string
60
- ref: string
61
- homepage?: string
62
- score?: number
63
- metadata?: Record<string, string>
64
- /** Whether this entry was manually reviewed and approved */
65
- curated?: boolean
66
- }
67
-
68
- export interface RegistrySearchResponse {
69
- query: string
70
- hits: RegistrySearchHit[]
71
- warnings: string[]
72
- }
@@ -1,200 +0,0 @@
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
- /**
8
- * Platform and architecture detection for ripgrep binary downloads.
9
- */
10
- function getRgPlatformTarget(): { platform: string; arch: string; ext: string } | null {
11
- const platform = process.platform
12
- const arch = process.arch
13
-
14
- if (platform === "linux" && arch === "x64") {
15
- return { platform: "x86_64-unknown-linux-musl", arch: "x64", ext: ".tar.gz" }
16
- }
17
- if (platform === "linux" && arch === "arm64") {
18
- return { platform: "aarch64-unknown-linux-gnu", arch: "arm64", ext: ".tar.gz" }
19
- }
20
- if (platform === "darwin" && arch === "x64") {
21
- return { platform: "x86_64-apple-darwin", arch: "x64", ext: ".tar.gz" }
22
- }
23
- if (platform === "darwin" && arch === "arm64") {
24
- return { platform: "aarch64-apple-darwin", arch: "arm64", ext: ".tar.gz" }
25
- }
26
- if (platform === "win32" && arch === "x64") {
27
- return { platform: "x86_64-pc-windows-msvc", arch: "x64", ext: ".zip" }
28
- }
29
-
30
- return null
31
- }
32
-
33
- const RG_VERSION = "14.1.1"
34
-
35
- export interface EnsureRgResult {
36
- rgPath: string
37
- installed: boolean
38
- version: string
39
- }
40
-
41
- /**
42
- * Ensure ripgrep is available. If not found on PATH or in stash/bin,
43
- * download and install it to stash/bin.
44
- *
45
- * Returns the path to the ripgrep binary and whether it was newly installed.
46
- */
47
- export function ensureRg(stashDir: string): EnsureRgResult {
48
- // Already available?
49
- const existing = resolveRg(stashDir)
50
- if (existing) {
51
- return { rgPath: existing, installed: false, version: getRgVersion(existing) }
52
- }
53
-
54
- // Determine platform
55
- const target = getRgPlatformTarget()
56
- if (!target) {
57
- throw new Error(
58
- `Unsupported platform for ripgrep auto-install: ${process.platform}/${process.arch}. ` +
59
- `Install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`
60
- )
61
- }
62
-
63
- const binDir = path.join(stashDir, "bin")
64
- if (!fs.existsSync(binDir)) {
65
- fs.mkdirSync(binDir, { recursive: true })
66
- }
67
-
68
- const archiveName = `ripgrep-${RG_VERSION}-${target.platform}`
69
- const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${archiveName}${target.ext}`
70
- const destBinary = path.join(binDir, RG_BINARY)
71
-
72
- if (target.ext === ".tar.gz") {
73
- downloadAndExtractTarGz(url, archiveName, destBinary)
74
- } else {
75
- downloadAndExtractZip(url, archiveName, destBinary)
76
- }
77
-
78
- // Make executable
79
- if (!IS_WINDOWS) {
80
- fs.chmodSync(destBinary, 0o755)
81
- }
82
-
83
- return { rgPath: destBinary, installed: true, version: RG_VERSION }
84
- }
85
-
86
- function downloadAndExtractTarGz(url: string, archiveName: string, destBinary: string): void {
87
- const destDir = path.dirname(destBinary)
88
- const tmpTarGz = path.join(destDir, "rg-download.tar.gz")
89
-
90
- try {
91
- // Download archive to a temporary file without using a shell
92
- const curlResult = spawnSync(
93
- "curl",
94
- ["-fsSL", "-o", tmpTarGz, url],
95
- {
96
- encoding: "utf8",
97
- timeout: 60_000,
98
- }
99
- )
100
-
101
- if (curlResult.status !== 0) {
102
- const err = curlResult.stderr?.trim() || curlResult.error?.message || "unknown error"
103
- throw new Error(`Failed to download ripgrep from ${url}: ${err}`)
104
- }
105
-
106
- // Extract the specific binary from the archive into destDir
107
- const tarResult = spawnSync(
108
- "tar",
109
- [
110
- "xzf",
111
- tmpTarGz,
112
- "--strip-components=1",
113
- "-C",
114
- destDir,
115
- `${archiveName}/rg`,
116
- ],
117
- {
118
- encoding: "utf8",
119
- timeout: 60_000,
120
- }
121
- )
122
-
123
- if (tarResult.status !== 0) {
124
- const err = tarResult.stderr?.trim() || tarResult.error?.message || "unknown error"
125
- throw new Error(`Failed to extract ripgrep from ${url}: ${err}`)
126
- }
127
-
128
- if (!fs.existsSync(destBinary)) {
129
- throw new Error(`ripgrep binary not found at ${destBinary} after extraction`)
130
- }
131
- } finally {
132
- // Best-effort cleanup of temporary archive
133
- try {
134
- if (fs.existsSync(tmpTarGz)) {
135
- fs.unlinkSync(tmpTarGz)
136
- }
137
- } catch {
138
- // ignore cleanup errors
139
- }
140
- }
141
- }
142
-
143
- function downloadAndExtractZip(url: string, archiveName: string, destBinary: string): void {
144
- const destDir = path.dirname(destBinary)
145
- const tmpZip = path.join(destDir, "rg-download.zip")
146
- const expandedDir = path.join(destDir, archiveName)
147
- try {
148
- // Download
149
- const dlResult = spawnSync("curl", ["-fsSL", "-o", tmpZip, url], {
150
- encoding: "utf8",
151
- timeout: 60_000,
152
- })
153
- if (dlResult.status !== 0) {
154
- throw new Error(dlResult.stderr?.trim() || "download failed")
155
- }
156
-
157
- // Extract the zip archive using separate spawnSync calls with argument arrays
158
- // to avoid shell injection via path interpolation in PowerShell -Command strings
159
- const expandResult = spawnSync("powershell", [
160
- "-Command",
161
- "Expand-Archive",
162
- "-Path", tmpZip,
163
- "-DestinationPath", destDir,
164
- "-Force",
165
- ], {
166
- encoding: "utf8",
167
- timeout: 60_000,
168
- })
169
- if (expandResult.status !== 0) {
170
- throw new Error(expandResult.stderr?.trim() || "extraction failed")
171
- }
172
-
173
- const srcRgExe = path.join(destDir, archiveName, "rg.exe")
174
- const moveResult = spawnSync("powershell", [
175
- "-Command",
176
- "Move-Item",
177
- "-Force",
178
- "-Path", srcRgExe,
179
- "-Destination", destBinary,
180
- ], {
181
- encoding: "utf8",
182
- timeout: 60_000,
183
- })
184
- if (moveResult.status !== 0) {
185
- throw new Error(moveResult.stderr?.trim() || "move failed")
186
- }
187
- } finally {
188
- if (fs.existsSync(tmpZip)) fs.unlinkSync(tmpZip)
189
- if (fs.existsSync(expandedDir)) fs.rmSync(expandedDir, { recursive: true, force: true })
190
- }
191
- }
192
-
193
- function getRgVersion(rgPath: string): string {
194
- const result = spawnSync(rgPath, ["--version"], { encoding: "utf8", timeout: 5_000 })
195
- if (result.status === 0 && result.stdout) {
196
- const match = result.stdout.match(/ripgrep\s+([\d.]+)/)
197
- return match ? match[1] : "unknown"
198
- }
199
- return "unknown"
200
- }
@@ -1,72 +0,0 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
- import { IS_WINDOWS } from "./common"
4
-
5
- export const RG_BINARY = IS_WINDOWS ? "rg.exe" : "rg"
6
-
7
- function canExecute(filePath: string): boolean {
8
- if (!fs.existsSync(filePath)) return false
9
- if (IS_WINDOWS) return true
10
- try {
11
- fs.accessSync(filePath, fs.constants.X_OK)
12
- return true
13
- } catch {
14
- return false
15
- }
16
- }
17
-
18
- function resolveFromPath(): string | null {
19
- const rawPath = process.env.PATH
20
- if (!rawPath) return null
21
-
22
- const pathEntries = rawPath.split(path.delimiter).filter(Boolean)
23
-
24
- if (IS_WINDOWS) {
25
- const pathext = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM")
26
- .split(";")
27
- .filter(Boolean)
28
- .map((ext) => ext.toLowerCase())
29
-
30
- for (const entry of pathEntries) {
31
- const directCandidate = path.join(entry, "rg")
32
- if (canExecute(directCandidate)) return directCandidate
33
-
34
- for (const ext of pathext) {
35
- const candidate = path.join(entry, `rg${ext}`)
36
- if (canExecute(candidate)) return candidate
37
- }
38
- }
39
- return null
40
- }
41
-
42
- for (const entry of pathEntries) {
43
- const candidate = path.join(entry, "rg")
44
- if (canExecute(candidate)) return candidate
45
- }
46
-
47
- return null
48
- }
49
-
50
- /**
51
- * Resolve the path to a usable ripgrep binary.
52
- * Checks in order:
53
- * 1. stashDir/bin/rg
54
- * 2. system PATH (rg)
55
- * Returns null if ripgrep is not available.
56
- */
57
- export function resolveRg(stashDir?: string): string | null {
58
- // Check stash bin directory first
59
- if (stashDir) {
60
- const stashRg = path.join(stashDir, "bin", RG_BINARY)
61
- if (canExecute(stashRg)) return stashRg
62
- }
63
-
64
- return resolveFromPath()
65
- }
66
-
67
- /**
68
- * Check if ripgrep is available (either in stash/bin or system PATH).
69
- */
70
- export function isRgAvailable(stashDir?: string): boolean {
71
- return resolveRg(stashDir) !== null
72
- }
package/src/ripgrep.ts DELETED
@@ -1,3 +0,0 @@
1
- export { resolveRg, isRgAvailable } from "./ripgrep-resolve"
2
- export { ensureRg } from "./ripgrep-install"
3
- export type { EnsureRgResult } from "./ripgrep-install"