agentikit 0.0.13 → 0.0.15

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 (156) hide show
  1. package/LICENSE +385 -0
  2. package/README.md +187 -110
  3. package/dist/{src/asset-spec.js → asset-spec.js} +11 -2
  4. package/dist/{src/asset-type-handler.js → asset-type-handler.js} +4 -3
  5. package/dist/cli.js +709 -0
  6. package/dist/common.js +192 -0
  7. package/dist/{src/config-cli.js → config-cli.js} +36 -30
  8. package/dist/{src/config.js → config.js} +95 -25
  9. package/dist/{src/db.js → db.js} +123 -51
  10. package/dist/{src/embedder.js → embedder.js} +57 -2
  11. package/dist/errors.js +28 -0
  12. package/dist/file-context.js +188 -0
  13. package/dist/{src/frontmatter.js → frontmatter.js} +1 -1
  14. package/dist/{src/github.js → github.js} +1 -3
  15. package/dist/handlers/agent-handler.js +19 -0
  16. package/dist/handlers/command-handler.js +20 -0
  17. package/dist/handlers/handler-bridge.js +51 -0
  18. package/dist/handlers/index.js +19 -0
  19. package/dist/handlers/knowledge-handler.js +32 -0
  20. package/dist/handlers/script-handler.js +42 -0
  21. package/dist/{src/handlers → handlers}/skill-handler.js +5 -6
  22. package/dist/{src/handlers → handlers}/tool-handler.js +8 -24
  23. package/dist/{src/indexer.js → indexer.js} +50 -26
  24. package/dist/init.js +43 -0
  25. package/dist/{src/llm.js → llm.js} +6 -11
  26. package/dist/lockfile.js +60 -0
  27. package/dist/matchers.js +163 -0
  28. package/dist/{src/metadata.js → metadata.js} +36 -16
  29. package/dist/{src/origin-resolve.js → origin-resolve.js} +10 -9
  30. package/dist/paths.js +83 -0
  31. package/dist/{src/registry-install.js → registry-install.js} +151 -19
  32. package/dist/{src/registry-resolve.js → registry-resolve.js} +190 -26
  33. package/dist/{src/registry-search.js → registry-search.js} +13 -21
  34. package/dist/renderers.js +286 -0
  35. package/dist/{src/ripgrep-install.js → ripgrep-install.js} +8 -27
  36. package/dist/{src/ripgrep-resolve.js → ripgrep-resolve.js} +21 -11
  37. package/dist/ripgrep.js +2 -0
  38. package/dist/self-update.js +226 -0
  39. package/dist/{src/stash-add.js → stash-add.js} +14 -4
  40. package/dist/stash-clone.js +115 -0
  41. package/dist/{src/stash-ref.js → stash-ref.js} +10 -9
  42. package/dist/{src/stash-registry.js → stash-registry.js} +21 -46
  43. package/dist/{src/stash-resolve.js → stash-resolve.js} +10 -9
  44. package/dist/{src/stash-search.js → stash-search.js} +89 -74
  45. package/dist/stash-show.js +74 -0
  46. package/dist/stash-source.js +127 -0
  47. package/dist/submit.js +557 -0
  48. package/dist/{src/tool-runner.js → tool-runner.js} +1 -5
  49. package/dist/{src/walker.js → walker.js} +38 -0
  50. package/dist/warn.js +20 -0
  51. package/package.json +13 -18
  52. package/dist/index.d.ts +0 -28
  53. package/dist/index.js +0 -15
  54. package/dist/src/asset-spec.d.ts +0 -16
  55. package/dist/src/asset-type-handler.d.ts +0 -27
  56. package/dist/src/cli.d.ts +0 -2
  57. package/dist/src/cli.js +0 -399
  58. package/dist/src/common.d.ts +0 -13
  59. package/dist/src/common.js +0 -60
  60. package/dist/src/config-cli.d.ts +0 -9
  61. package/dist/src/config.d.ts +0 -50
  62. package/dist/src/db.d.ts +0 -46
  63. package/dist/src/embedder.d.ts +0 -10
  64. package/dist/src/frontmatter.d.ts +0 -30
  65. package/dist/src/github.d.ts +0 -4
  66. package/dist/src/handlers/agent-handler.d.ts +0 -2
  67. package/dist/src/handlers/agent-handler.js +0 -26
  68. package/dist/src/handlers/command-handler.d.ts +0 -2
  69. package/dist/src/handlers/command-handler.js +0 -23
  70. package/dist/src/handlers/index.d.ts +0 -6
  71. package/dist/src/handlers/index.js +0 -23
  72. package/dist/src/handlers/knowledge-handler.d.ts +0 -2
  73. package/dist/src/handlers/knowledge-handler.js +0 -56
  74. package/dist/src/handlers/markdown-helpers.d.ts +0 -7
  75. package/dist/src/handlers/script-handler.d.ts +0 -2
  76. package/dist/src/handlers/script-handler.js +0 -78
  77. package/dist/src/handlers/skill-handler.d.ts +0 -2
  78. package/dist/src/handlers/tool-handler.d.ts +0 -2
  79. package/dist/src/indexer.d.ts +0 -22
  80. package/dist/src/init.d.ts +0 -19
  81. package/dist/src/init.js +0 -99
  82. package/dist/src/llm.d.ts +0 -15
  83. package/dist/src/markdown.d.ts +0 -18
  84. package/dist/src/metadata.d.ts +0 -41
  85. package/dist/src/origin-resolve.d.ts +0 -19
  86. package/dist/src/registry-install.d.ts +0 -11
  87. package/dist/src/registry-resolve.d.ts +0 -3
  88. package/dist/src/registry-search.d.ts +0 -27
  89. package/dist/src/registry-types.d.ts +0 -62
  90. package/dist/src/ripgrep-install.d.ts +0 -12
  91. package/dist/src/ripgrep-resolve.d.ts +0 -13
  92. package/dist/src/ripgrep.d.ts +0 -3
  93. package/dist/src/ripgrep.js +0 -2
  94. package/dist/src/stash-add.d.ts +0 -4
  95. package/dist/src/stash-clone.d.ts +0 -22
  96. package/dist/src/stash-clone.js +0 -83
  97. package/dist/src/stash-ref.d.ts +0 -31
  98. package/dist/src/stash-registry.d.ts +0 -18
  99. package/dist/src/stash-resolve.d.ts +0 -2
  100. package/dist/src/stash-search.d.ts +0 -8
  101. package/dist/src/stash-show.d.ts +0 -5
  102. package/dist/src/stash-show.js +0 -46
  103. package/dist/src/stash-source.d.ts +0 -24
  104. package/dist/src/stash-source.js +0 -81
  105. package/dist/src/stash-types.d.ts +0 -227
  106. package/dist/src/stash.d.ts +0 -16
  107. package/dist/src/stash.js +0 -9
  108. package/dist/src/tool-runner.d.ts +0 -35
  109. package/dist/src/walker.d.ts +0 -19
  110. package/src/asset-spec.ts +0 -85
  111. package/src/asset-type-handler.ts +0 -77
  112. package/src/cli.ts +0 -427
  113. package/src/common.ts +0 -76
  114. package/src/config-cli.ts +0 -499
  115. package/src/config.ts +0 -305
  116. package/src/db.ts +0 -411
  117. package/src/embedder.ts +0 -128
  118. package/src/frontmatter.ts +0 -95
  119. package/src/github.ts +0 -21
  120. package/src/handlers/agent-handler.ts +0 -32
  121. package/src/handlers/command-handler.ts +0 -29
  122. package/src/handlers/index.ts +0 -25
  123. package/src/handlers/knowledge-handler.ts +0 -62
  124. package/src/handlers/markdown-helpers.ts +0 -19
  125. package/src/handlers/script-handler.ts +0 -92
  126. package/src/handlers/skill-handler.ts +0 -37
  127. package/src/handlers/tool-handler.ts +0 -71
  128. package/src/indexer.ts +0 -392
  129. package/src/init.ts +0 -114
  130. package/src/llm.ts +0 -125
  131. package/src/markdown.ts +0 -106
  132. package/src/metadata.ts +0 -333
  133. package/src/origin-resolve.ts +0 -67
  134. package/src/registry-install.ts +0 -361
  135. package/src/registry-resolve.ts +0 -341
  136. package/src/registry-search.ts +0 -335
  137. package/src/registry-types.ts +0 -72
  138. package/src/ripgrep-install.ts +0 -200
  139. package/src/ripgrep-resolve.ts +0 -72
  140. package/src/ripgrep.ts +0 -3
  141. package/src/stash-add.ts +0 -63
  142. package/src/stash-clone.ts +0 -127
  143. package/src/stash-ref.ts +0 -99
  144. package/src/stash-registry.ts +0 -259
  145. package/src/stash-resolve.ts +0 -50
  146. package/src/stash-search.ts +0 -613
  147. package/src/stash-show.ts +0 -55
  148. package/src/stash-source.ts +0 -103
  149. package/src/stash-types.ts +0 -231
  150. package/src/stash.ts +0 -39
  151. package/src/tool-runner.ts +0 -142
  152. package/src/walker.ts +0 -53
  153. /package/dist/{src/handlers → handlers}/markdown-helpers.js +0 -0
  154. /package/dist/{src/markdown.js → markdown.js} +0 -0
  155. /package/dist/{src/registry-types.js → registry-types.js} +0 -0
  156. /package/dist/{src/stash-types.js → stash-types.js} +0 -0
@@ -1,361 +0,0 @@
1
- import { spawnSync } from "node:child_process"
2
- import fs from "node:fs"
3
- import path from "node:path"
4
- import { fetchWithTimeout, isWithin, TYPE_DIRS } from "./common"
5
- import { loadConfig, saveConfig, type AgentikitConfig } from "./config"
6
- import { parseRegistryRef, resolveRegistryArtifact } from "./registry-resolve"
7
- import type { ParsedGitRef, RegistryInstallResult, RegistryInstalledEntry, RegistrySource } from "./registry-types"
8
-
9
- const REGISTRY_STASH_DIR_NAMES = new Set<string>(Object.values(TYPE_DIRS))
10
-
11
- export interface InstallRegistryRefOptions {
12
- cacheRootDir?: string
13
- now?: Date
14
- }
15
-
16
- export async function installRegistryRef(ref: string, options?: InstallRegistryRefOptions): Promise<RegistryInstallResult> {
17
- const parsed = parseRegistryRef(ref)
18
- if (parsed.source === "git") {
19
- return installGitRegistryRef(parsed, options)
20
- }
21
- const resolved = await resolveRegistryArtifact(parsed)
22
-
23
- const installedAt = (options?.now ?? new Date()).toISOString()
24
- const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir()
25
- const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id)
26
- const archivePath = path.join(cacheDir, "artifact.tar.gz")
27
- const extractedDir = path.join(cacheDir, "extracted")
28
-
29
- fs.mkdirSync(cacheDir, { recursive: true })
30
-
31
- await downloadArchive(resolved.artifactUrl, archivePath)
32
- extractTarGzSecure(archivePath, extractedDir)
33
-
34
- const provisionalKitRoot = detectStashRoot(extractedDir)
35
- const installRoot = applyAgentikitIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot
36
- const stashRoot = detectStashRoot(installRoot)
37
-
38
- return {
39
- id: resolved.id,
40
- source: resolved.source,
41
- ref: resolved.ref,
42
- artifactUrl: resolved.artifactUrl,
43
- resolvedVersion: resolved.resolvedVersion,
44
- resolvedRevision: resolved.resolvedRevision,
45
- installedAt,
46
- cacheDir,
47
- extractedDir,
48
- stashRoot,
49
- }
50
- }
51
-
52
- async function installGitRegistryRef(parsed: ParsedGitRef, options?: InstallRegistryRefOptions): Promise<RegistryInstallResult> {
53
- const resolved = await resolveRegistryArtifact(parsed)
54
- const installedAt = (options?.now ?? new Date()).toISOString()
55
- const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir()
56
- const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id)
57
- const extractedDir = path.join(cacheDir, "extracted")
58
-
59
- fs.mkdirSync(cacheDir, { recursive: true })
60
- fs.rmSync(extractedDir, { recursive: true, force: true })
61
- fs.mkdirSync(extractedDir, { recursive: true })
62
-
63
- const includeConfig = findNearestAgentikitIncludeConfig(parsed.sourcePath, parsed.repoRoot)
64
- if (includeConfig) {
65
- copyIncludedPaths(includeConfig.baseDir, includeConfig.include, extractedDir)
66
- } else {
67
- copyDirectoryContents(parsed.sourcePath, extractedDir)
68
- }
69
-
70
- const stashRoot = detectStashRoot(extractedDir)
71
-
72
- return {
73
- id: resolved.id,
74
- source: resolved.source,
75
- ref: resolved.ref,
76
- artifactUrl: resolved.artifactUrl,
77
- resolvedVersion: resolved.resolvedVersion,
78
- resolvedRevision: resolved.resolvedRevision,
79
- installedAt,
80
- cacheDir,
81
- extractedDir,
82
- stashRoot,
83
- }
84
- }
85
-
86
- export function upsertInstalledRegistryEntry(entry: RegistryInstalledEntry): AgentikitConfig {
87
- const current = loadConfig()
88
- const currentInstalled = current.registry?.installed ?? []
89
- const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id)
90
- const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)]
91
-
92
- const nextConfig: AgentikitConfig = {
93
- ...current,
94
- registry: { installed: nextInstalled },
95
- }
96
- saveConfig(nextConfig)
97
- return nextConfig
98
- }
99
-
100
- export function removeInstalledRegistryEntry(id: string): AgentikitConfig {
101
- const current = loadConfig()
102
- const currentInstalled = current.registry?.installed ?? []
103
- const nextInstalled = currentInstalled.filter((item) => item.id !== id)
104
-
105
- const nextConfig: AgentikitConfig = {
106
- ...current,
107
- registry: nextInstalled.length > 0 ? { installed: nextInstalled } : undefined,
108
- }
109
- saveConfig(nextConfig)
110
- return nextConfig
111
- }
112
-
113
- export function getRegistryCacheRootDir(): string {
114
- const xdgCache = process.env.XDG_CACHE_HOME?.trim()
115
- if (xdgCache) {
116
- return path.join(path.resolve(xdgCache), "agentikit", "registry")
117
- }
118
- const home = process.env.HOME?.trim()
119
- if (!home) {
120
- throw new Error("Unable to determine cache directory. Set XDG_CACHE_HOME or HOME.")
121
- }
122
- return path.join(path.resolve(home), ".cache", "agentikit", "registry")
123
- }
124
-
125
- export function detectStashRoot(extractedDir: string): string {
126
- const root = path.resolve(extractedDir)
127
-
128
- const rootDotStash = path.join(root, ".stash")
129
- if (isDirectory(rootDotStash)) {
130
- return root
131
- }
132
-
133
- if (hasStashDirs(root)) {
134
- return root
135
- }
136
-
137
- const opencodeDir = path.join(root, "opencode")
138
- if (hasStashDirs(opencodeDir)) {
139
- return opencodeDir
140
- }
141
-
142
- const shallowest = findShallowestDotStashRoot(root)
143
- if (shallowest) return shallowest
144
-
145
- return root
146
- }
147
-
148
- function buildInstallCacheDir(cacheRootDir: string, source: RegistrySource, id: string): string {
149
- const slug = `${source}-${id.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "")}`
150
- const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
151
- return path.join(cacheRootDir, slug || source, stamp)
152
- }
153
-
154
- function applyAgentikitIncludeConfig(
155
- sourceRoot: string,
156
- cacheDir: string,
157
- searchRoot: string = sourceRoot,
158
- ): string | undefined {
159
- const includeConfig = findNearestAgentikitIncludeConfig(sourceRoot, searchRoot)
160
- if (!includeConfig) return undefined
161
-
162
- const selectedDir = path.join(cacheDir, "selected")
163
- fs.rmSync(selectedDir, { recursive: true, force: true })
164
- fs.mkdirSync(selectedDir, { recursive: true })
165
- copyIncludedPaths(includeConfig.baseDir, includeConfig.include, selectedDir)
166
- return selectedDir
167
- }
168
-
169
- async function downloadArchive(url: string, destination: string): Promise<void> {
170
- const response = await fetchWithTimeout(url, undefined, 120_000)
171
- if (!response.ok) {
172
- throw new Error(`Failed to download archive (${response.status}) from ${url}`)
173
- }
174
- // Stream response to disk instead of buffering the entire archive in memory.
175
- // Uses Bun.write which handles Response streaming natively.
176
- const BunRuntime: { write(path: string, body: Response): Promise<number> } =
177
- (globalThis as Record<string, unknown>).Bun as typeof BunRuntime
178
- if (BunRuntime?.write) {
179
- await BunRuntime.write(destination, response)
180
- } else {
181
- // Fallback for non-Bun environments (e.g., tests)
182
- const arrayBuffer = await response.arrayBuffer()
183
- fs.writeFileSync(destination, Buffer.from(arrayBuffer))
184
- }
185
- }
186
-
187
- function extractTarGzSecure(archivePath: string, destinationDir: string): void {
188
- const listResult = spawnSync("tar", ["tzf", archivePath], { encoding: "utf8" })
189
- if (listResult.status !== 0) {
190
- const err = listResult.stderr?.trim() || listResult.error?.message || "unknown error"
191
- throw new Error(`Failed to inspect archive ${archivePath}: ${err}`)
192
- }
193
-
194
- validateTarEntries(listResult.stdout)
195
-
196
- fs.rmSync(destinationDir, { recursive: true, force: true })
197
- fs.mkdirSync(destinationDir, { recursive: true })
198
-
199
- const extractResult = spawnSync("tar", ["xzf", archivePath, "--strip-components=1", "-C", destinationDir], {
200
- encoding: "utf8",
201
- })
202
- if (extractResult.status !== 0) {
203
- const err = extractResult.stderr?.trim() || extractResult.error?.message || "unknown error"
204
- throw new Error(`Failed to extract archive ${archivePath}: ${err}`)
205
- }
206
- }
207
-
208
- function validateTarEntries(listOutput: string): void {
209
- const lines = listOutput.split(/\r?\n/).filter(Boolean)
210
- for (const rawLine of lines) {
211
- const entry = rawLine.trim()
212
- if (!entry || entry.includes("\0")) {
213
- throw new Error(`Archive contains an invalid entry: ${JSON.stringify(rawLine)}`)
214
- }
215
- if (entry.startsWith("/")) {
216
- throw new Error(`Archive contains an absolute path entry: ${entry}`)
217
- }
218
-
219
- const normalized = path.posix.normalize(entry)
220
- if (normalized === ".." || normalized.startsWith("../")) {
221
- throw new Error(`Archive contains a path traversal entry: ${entry}`)
222
- }
223
-
224
- const parts = normalized.split("/").filter(Boolean)
225
- const stripped = parts.slice(1).join("/")
226
- if (!stripped) continue
227
- const normalizedStripped = path.posix.normalize(stripped)
228
- if (normalizedStripped === ".." || normalizedStripped.startsWith("../") || path.posix.isAbsolute(normalizedStripped)) {
229
- throw new Error(`Archive contains an unsafe entry after strip-components: ${entry}`)
230
- }
231
- }
232
- }
233
-
234
- function isDirectory(target: string): boolean {
235
- try {
236
- return fs.statSync(target).isDirectory()
237
- } catch {
238
- return false
239
- }
240
- }
241
-
242
- function readAgentikitIncludeConfigAtDir(dirPath: string): { baseDir: string; include: string[] } | undefined {
243
- const packageJsonPath = path.join(dirPath, "package.json")
244
- if (!fs.existsSync(packageJsonPath)) return undefined
245
-
246
- let pkg: unknown
247
- try {
248
- pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"))
249
- } catch {
250
- return undefined
251
- }
252
- if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg)) return undefined
253
-
254
- const agentikit = (pkg as Record<string, unknown>).agentikit
255
- if (typeof agentikit !== "object" || agentikit === null || Array.isArray(agentikit)) return undefined
256
-
257
- const include = (agentikit as Record<string, unknown>).include
258
- if (!Array.isArray(include)) return undefined
259
-
260
- const parsedInclude = include
261
- .filter((value): value is string => typeof value === "string")
262
- .map((value) => value.trim())
263
- .filter(Boolean)
264
-
265
- return parsedInclude.length > 0 ? { baseDir: dirPath, include: parsedInclude } : undefined
266
- }
267
-
268
- function findNearestAgentikitIncludeConfig(
269
- startDir: string,
270
- stopDir: string,
271
- ): { baseDir: string; include: string[] } | undefined {
272
- let current = path.resolve(startDir)
273
- const boundary = path.resolve(stopDir)
274
-
275
- while (isWithin(current, boundary)) {
276
- const config = readAgentikitIncludeConfigAtDir(current)
277
- if (config) return config
278
- if (current === boundary) break
279
- const parent = path.dirname(current)
280
- if (parent === current) break
281
- current = parent
282
- }
283
-
284
- return undefined
285
- }
286
-
287
- function copyIncludedPaths(baseDir: string, include: string[], destinationDir: string): void {
288
- for (const entry of include) {
289
- const resolvedSource = path.resolve(baseDir, entry)
290
- if (!isWithin(resolvedSource, baseDir)) {
291
- throw new Error(`Path in agentikit.include escapes the package root: ${entry}`)
292
- }
293
- if (!fs.existsSync(resolvedSource)) {
294
- throw new Error(`Path in agentikit.include does not exist: ${entry}`)
295
- }
296
- if (path.basename(resolvedSource) === ".git") {
297
- continue
298
- }
299
- const relativePath = path.relative(baseDir, resolvedSource)
300
- if (!relativePath || relativePath === ".") {
301
- copyDirectoryContents(baseDir, destinationDir)
302
- continue
303
- }
304
- copyPath(resolvedSource, path.join(destinationDir, relativePath))
305
- }
306
- }
307
-
308
- function copyDirectoryContents(sourceDir: string, destinationDir: string): void {
309
- for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
310
- if (entry.name === ".git") continue
311
- copyPath(path.join(sourceDir, entry.name), path.join(destinationDir, entry.name))
312
- }
313
- }
314
-
315
- function copyPath(sourcePath: string, destinationPath: string): void {
316
- const stat = fs.statSync(sourcePath)
317
- fs.mkdirSync(path.dirname(destinationPath), { recursive: true })
318
- if (stat.isDirectory()) {
319
- fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true })
320
- return
321
- }
322
- fs.copyFileSync(sourcePath, destinationPath)
323
- }
324
-
325
- function hasStashDirs(dirPath: string): boolean {
326
- if (!isDirectory(dirPath)) return false
327
- const entries = fs.readdirSync(dirPath, { withFileTypes: true })
328
- return entries.some((entry) => entry.isDirectory() && REGISTRY_STASH_DIR_NAMES.has(entry.name))
329
- }
330
-
331
- function findShallowestDotStashRoot(root: string): string | undefined {
332
- const queue: string[] = [root]
333
- while (queue.length > 0) {
334
- const current = queue.shift()!
335
- const dotStash = path.join(current, ".stash")
336
- if (isDirectory(dotStash)) {
337
- return current
338
- }
339
- let children: fs.Dirent[]
340
- try {
341
- children = fs.readdirSync(current, { withFileTypes: true })
342
- } catch {
343
- continue
344
- }
345
- for (const child of children) {
346
- if (!child.isDirectory()) continue
347
- if (child.name === ".git" || child.name === "node_modules") continue
348
- queue.push(path.join(current, child.name))
349
- }
350
- }
351
- return undefined
352
- }
353
-
354
- function normalizeInstalledEntry(entry: RegistryInstalledEntry): RegistryInstalledEntry {
355
- return {
356
- ...entry,
357
- stashRoot: path.resolve(entry.stashRoot),
358
- cacheDir: path.resolve(entry.cacheDir),
359
- }
360
- }
361
-
@@ -1,341 +0,0 @@
1
- import { spawnSync } from "node:child_process"
2
- import fs from "node:fs"
3
- import path from "node:path"
4
- import { pathToFileURL } from "node:url"
5
- import { fetchWithTimeout } from "./common"
6
- import type { ParsedGitRef, ParsedGithubRef, ParsedNpmRef, ParsedRegistryRef, ResolvedRegistryArtifact } from "./registry-types"
7
- import { GITHUB_API_BASE, githubHeaders, asRecord, asString } from "./github"
8
-
9
- export function parseRegistryRef(rawRef: string): ParsedRegistryRef {
10
- const ref = rawRef.trim()
11
- if (!ref) throw new Error("Registry ref is required.")
12
-
13
- if (ref.startsWith("npm:")) {
14
- return parseNpmRef(ref.slice(4), ref)
15
- }
16
- if (ref.startsWith("github:")) {
17
- return parseGithubShorthand(ref.slice(7), ref)
18
- }
19
- if (ref.startsWith("http://") || ref.startsWith("https://")) {
20
- return parseGithubUrl(ref)
21
- }
22
- const localGitRef = tryParseLocalGitRef(ref, isPathLikeRef(ref))
23
- if (localGitRef) {
24
- return localGitRef
25
- }
26
-
27
- if (ref.startsWith("@") || !looksLikeGithubOwnerRepo(ref)) {
28
- return parseNpmRef(ref, ref)
29
- }
30
-
31
- return parseGithubShorthand(ref, ref)
32
- }
33
-
34
- export async function resolveRegistryArtifact(parsed: ParsedRegistryRef): Promise<ResolvedRegistryArtifact> {
35
- if (parsed.source === "npm") {
36
- return resolveNpmArtifact(parsed)
37
- }
38
- if (parsed.source === "git") {
39
- return resolveGitArtifact(parsed)
40
- }
41
- return resolveGithubArtifact(parsed)
42
- }
43
-
44
- function parseNpmRef(input: string, originalRef: string): ParsedNpmRef {
45
- const trimmed = input.trim()
46
- if (!trimmed) throw new Error("Invalid npm ref.")
47
-
48
- const parsed = splitNpmNameAndVersion(trimmed)
49
- validateNpmPackageName(parsed.packageName)
50
-
51
- return {
52
- source: "npm",
53
- ref: originalRef,
54
- id: `npm:${parsed.packageName}`,
55
- packageName: parsed.packageName,
56
- requestedVersionOrTag: parsed.requestedVersionOrTag,
57
- }
58
- }
59
-
60
- function parseGithubShorthand(input: string, originalRef: string): ParsedGithubRef {
61
- const [repoPart, requestedRef] = splitRefSuffix(input.trim())
62
- const segments = repoPart.split("/").filter(Boolean)
63
- if (segments.length !== 2) {
64
- throw new Error("Invalid GitHub ref. Expected owner/repo or owner/repo#ref.")
65
- }
66
- const owner = segments[0]
67
- const repo = segments[1].replace(/\.git$/i, "")
68
- if (!owner || !repo) {
69
- throw new Error("Invalid GitHub ref. Expected owner/repo.")
70
- }
71
- return {
72
- source: "github",
73
- ref: originalRef,
74
- id: `github:${owner}/${repo}`,
75
- owner,
76
- repo,
77
- requestedRef,
78
- }
79
- }
80
-
81
- function parseGithubUrl(rawUrl: string): ParsedGithubRef {
82
- let url: URL
83
- try {
84
- url = new URL(rawUrl)
85
- } catch {
86
- throw new Error("Invalid registry URL.")
87
- }
88
- if (url.hostname !== "github.com") {
89
- throw new Error("Only GitHub URLs are currently supported for URL refs.")
90
- }
91
-
92
- const segments = url.pathname.split("/").filter(Boolean)
93
- if (segments.length < 2) {
94
- throw new Error("Invalid GitHub URL. Expected https://github.com/owner/repo.")
95
- }
96
- const owner = segments[0]
97
- const repo = segments[1].replace(/\.git$/i, "")
98
- const requestedRef = url.hash ? decodeURIComponent(url.hash.slice(1)) : undefined
99
-
100
- return {
101
- source: "github",
102
- ref: rawUrl,
103
- id: `github:${owner}/${repo}`,
104
- owner,
105
- repo,
106
- requestedRef,
107
- }
108
- }
109
-
110
- function tryParseLocalGitRef(rawRef: string, explicitPath: boolean): ParsedGitRef | undefined {
111
- if (!explicitPath) {
112
- return undefined
113
- }
114
-
115
- const resolvedPath = path.resolve(rawRef)
116
- let stat: fs.Stats
117
- try {
118
- stat = fs.statSync(resolvedPath)
119
- } catch {
120
- throw new Error(`Local path not found: ${resolvedPath}`)
121
- }
122
-
123
- if (!stat.isDirectory()) {
124
- throw new Error("Local add path must be a directory, but the provided path is not one.")
125
- }
126
-
127
- const repoRoot = findGitRepoRoot(resolvedPath)
128
- if (!repoRoot) {
129
- throw new Error("Local add path must be inside a git repository.")
130
- }
131
-
132
- return {
133
- source: "git",
134
- ref: rawRef,
135
- id: `git:${encodeURIComponent(resolvedPath)}`,
136
- repoRoot,
137
- sourcePath: resolvedPath,
138
- }
139
- }
140
-
141
- function isPathLikeRef(ref: string): boolean {
142
- if (path.isAbsolute(ref)) return true
143
- if (ref.startsWith("./") || ref.startsWith("../") || ref.startsWith(".\\") || ref.startsWith("..\\")) {
144
- return true
145
- }
146
- return ref.includes("/") || ref.includes("\\")
147
- }
148
-
149
- async function resolveNpmArtifact(parsed: ParsedNpmRef): Promise<ResolvedRegistryArtifact> {
150
- const encodedName = encodeURIComponent(parsed.packageName)
151
- const metadata = await fetchJson<Record<string, unknown>>(`https://registry.npmjs.org/${encodedName}`)
152
-
153
- const versions = asRecord(metadata.versions)
154
- const distTags = asRecord(metadata["dist-tags"])
155
-
156
- const requested = parsed.requestedVersionOrTag
157
- let resolvedVersion: string | undefined
158
- if (!requested) {
159
- resolvedVersion = asString(distTags.latest)
160
- } else if (requested in versions) {
161
- resolvedVersion = requested
162
- } else {
163
- resolvedVersion = asString(distTags[requested])
164
- }
165
-
166
- if (!resolvedVersion || !(resolvedVersion in versions)) {
167
- throw new Error(`Unable to resolve npm ref \"${parsed.ref}\".`)
168
- }
169
-
170
- const versionMeta = asRecord(versions[resolvedVersion])
171
- const dist = asRecord(versionMeta.dist)
172
- const tarballUrl = asString(dist.tarball)
173
- if (!tarballUrl) {
174
- throw new Error(`npm package ${parsed.packageName}@${resolvedVersion} does not expose a tarball URL.`)
175
- }
176
-
177
- const resolvedRevision = asString(dist.shasum) ?? asString(dist.integrity)
178
-
179
- return {
180
- id: parsed.id,
181
- source: parsed.source,
182
- ref: parsed.ref,
183
- artifactUrl: tarballUrl,
184
- resolvedVersion,
185
- resolvedRevision,
186
- }
187
- }
188
-
189
- async function resolveGithubArtifact(parsed: ParsedGithubRef): Promise<ResolvedRegistryArtifact> {
190
- const headers = githubHeaders()
191
-
192
- if (parsed.requestedRef) {
193
- const commit = await tryFetchJson<Record<string, unknown>>(
194
- `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(parsed.requestedRef)}`,
195
- headers,
196
- )
197
- const resolvedRevision = asString(commit?.sha) ?? parsed.requestedRef
198
- return {
199
- id: parsed.id,
200
- source: parsed.source,
201
- ref: parsed.ref,
202
- artifactUrl: `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/tarball/${encodeURIComponent(parsed.requestedRef)}`,
203
- resolvedRevision,
204
- resolvedVersion: parsed.requestedRef,
205
- }
206
- }
207
-
208
- const latestRelease = await tryFetchJson<Record<string, unknown>>(
209
- `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/releases/latest`,
210
- headers,
211
- )
212
- if (latestRelease) {
213
- const tarballUrl = asString(latestRelease.tarball_url)
214
- if (tarballUrl) {
215
- return {
216
- id: parsed.id,
217
- source: parsed.source,
218
- ref: parsed.ref,
219
- artifactUrl: tarballUrl,
220
- resolvedVersion: asString(latestRelease.tag_name),
221
- resolvedRevision: asString(latestRelease.target_commitish),
222
- }
223
- }
224
- }
225
-
226
- const repoMeta = await fetchJson<Record<string, unknown>>(
227
- `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}`,
228
- headers,
229
- )
230
- const defaultBranch = asString(repoMeta.default_branch)
231
- if (!defaultBranch) {
232
- throw new Error(`Unable to resolve default branch for ${parsed.owner}/${parsed.repo}.`)
233
- }
234
-
235
- const commit = await tryFetchJson<Record<string, unknown>>(
236
- `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(defaultBranch)}`,
237
- headers,
238
- )
239
-
240
- return {
241
- id: parsed.id,
242
- source: parsed.source,
243
- ref: parsed.ref,
244
- artifactUrl: `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/tarball/${encodeURIComponent(defaultBranch)}`,
245
- resolvedVersion: defaultBranch,
246
- resolvedRevision: asString(commit?.sha) ?? defaultBranch,
247
- }
248
- }
249
-
250
- async function resolveGitArtifact(parsed: ParsedGitRef): Promise<ResolvedRegistryArtifact> {
251
- return {
252
- id: parsed.id,
253
- source: parsed.source,
254
- ref: parsed.ref,
255
- artifactUrl: pathToFileURL(parsed.sourcePath).toString(),
256
- resolvedRevision: readGitValue(parsed.repoRoot, "rev-parse", "HEAD"),
257
- resolvedVersion: readGitValue(parsed.repoRoot, "rev-parse", "--abbrev-ref", "HEAD"),
258
- }
259
- }
260
-
261
- function splitNpmNameAndVersion(input: string): { packageName: string; requestedVersionOrTag?: string } {
262
- if (input.startsWith("@")) {
263
- const secondAt = input.indexOf("@", 1)
264
- if (secondAt > 0) {
265
- return {
266
- packageName: input.slice(0, secondAt),
267
- requestedVersionOrTag: input.slice(secondAt + 1) || undefined,
268
- }
269
- }
270
- return { packageName: input }
271
- }
272
-
273
- const at = input.lastIndexOf("@")
274
- if (at > 0) {
275
- return {
276
- packageName: input.slice(0, at),
277
- requestedVersionOrTag: input.slice(at + 1) || undefined,
278
- }
279
- }
280
- return { packageName: input }
281
- }
282
-
283
- function validateNpmPackageName(name: string): void {
284
- if (!name) throw new Error('Invalid npm package name: name is required.')
285
- if (name.length > 214) throw new Error(`Invalid npm package name: "${name}" exceeds 214 characters.`)
286
- if (name !== name.toLowerCase() && !name.startsWith('@')) {
287
- throw new Error(`Invalid npm package name: "${name}" must be lowercase.`)
288
- }
289
- if (name.startsWith('.') || name.startsWith('_')) {
290
- throw new Error(`Invalid npm package name: "${name}" cannot start with . or _.`)
291
- }
292
- if (/[~'!()*]/.test(name) || name.includes(' ') || encodeURIComponent(name).replace(/%40/g, '@').replace(/%2[Ff]/g, '/') !== name) {
293
- throw new Error(`Invalid npm package name: "${name}" contains invalid characters.`)
294
- }
295
- }
296
-
297
- function looksLikeGithubOwnerRepo(ref: string): boolean {
298
- const [repoPart] = splitRefSuffix(ref)
299
- const parts = repoPart.split("/").filter(Boolean)
300
- return parts.length === 2
301
- }
302
-
303
- function splitRefSuffix(value: string): [string, string | undefined] {
304
- const hash = value.indexOf("#")
305
- if (hash < 0) return [value, undefined]
306
- return [value.slice(0, hash), value.slice(hash + 1) || undefined]
307
- }
308
-
309
- function findGitRepoRoot(startDir: string): string | undefined {
310
- let current = path.resolve(startDir)
311
- while (true) {
312
- if (fs.existsSync(path.join(current, ".git"))) {
313
- return current
314
- }
315
- const parent = path.dirname(current)
316
- if (parent === current) return undefined
317
- current = parent
318
- }
319
- }
320
-
321
- function readGitValue(repoRoot: string, ...args: string[]): string | undefined {
322
- const result = spawnSync("git", ["-C", repoRoot, ...args], { encoding: "utf8" })
323
- if (result.status !== 0) return undefined
324
- const value = result.stdout.trim()
325
- return value || undefined
326
- }
327
-
328
- async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T> {
329
- const response = await fetchWithTimeout(url, { headers })
330
- if (!response.ok) {
331
- throw new Error(`Request failed (${response.status}) for ${url}`)
332
- }
333
- return await response.json() as T
334
- }
335
-
336
- async function tryFetchJson<T>(url: string, headers?: HeadersInit): Promise<T | null> {
337
- const response = await fetchWithTimeout(url, { headers })
338
- if (!response.ok) return null
339
- return await response.json() as T
340
- }
341
-