agentikit 0.0.9 → 0.0.12
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 +129 -214
- package/dist/index.d.ts +8 -2
- package/dist/index.js +4 -1
- package/dist/src/asset-spec.d.ts +2 -0
- package/dist/src/asset-spec.js +22 -3
- package/dist/src/asset-type-handler.d.ts +27 -0
- package/dist/src/asset-type-handler.js +33 -0
- package/dist/src/cli.js +201 -75
- package/dist/src/common.d.ts +6 -1
- package/dist/src/common.js +18 -4
- package/dist/src/config-cli.d.ts +9 -0
- package/dist/src/config-cli.js +473 -0
- package/dist/src/config.d.ts +19 -6
- package/dist/src/config.js +139 -29
- package/dist/src/db.d.ts +46 -0
- package/dist/src/db.js +299 -0
- package/dist/src/embedder.js +12 -7
- package/dist/src/github.d.ts +4 -0
- package/dist/src/github.js +19 -0
- package/dist/src/handlers/agent-handler.d.ts +2 -0
- package/dist/src/handlers/agent-handler.js +26 -0
- package/dist/src/handlers/command-handler.d.ts +2 -0
- package/dist/src/handlers/command-handler.js +23 -0
- package/dist/src/handlers/index.d.ts +6 -0
- package/dist/src/handlers/index.js +23 -0
- package/dist/src/handlers/knowledge-handler.d.ts +2 -0
- package/dist/src/handlers/knowledge-handler.js +56 -0
- package/dist/src/handlers/markdown-helpers.d.ts +7 -0
- package/dist/src/handlers/markdown-helpers.js +15 -0
- package/dist/src/handlers/script-handler.d.ts +2 -0
- package/dist/src/handlers/script-handler.js +78 -0
- package/dist/src/handlers/skill-handler.d.ts +2 -0
- package/dist/src/handlers/skill-handler.js +30 -0
- package/dist/src/handlers/tool-handler.d.ts +2 -0
- package/dist/src/handlers/tool-handler.js +58 -0
- package/dist/src/indexer.d.ts +1 -23
- package/dist/src/indexer.js +162 -155
- package/dist/src/init.d.ts +2 -2
- package/dist/src/init.js +21 -9
- package/dist/src/llm.js +4 -3
- package/dist/src/metadata.d.ts +0 -1
- package/dist/src/metadata.js +6 -64
- package/dist/src/origin-resolve.d.ts +19 -0
- package/dist/src/origin-resolve.js +53 -0
- package/dist/src/registry-install.d.ts +2 -2
- package/dist/src/registry-install.js +142 -35
- package/dist/src/registry-resolve.js +90 -22
- package/dist/src/registry-search.d.ts +22 -0
- package/dist/src/registry-search.js +231 -97
- package/dist/src/registry-types.d.ts +9 -2
- package/dist/src/stash-add.js +4 -4
- package/dist/src/stash-clone.d.ts +22 -0
- package/dist/src/stash-clone.js +83 -0
- package/dist/src/stash-ref.d.ts +27 -3
- package/dist/src/stash-ref.js +63 -24
- package/dist/src/stash-registry.js +12 -12
- package/dist/src/stash-resolve.js +3 -0
- package/dist/src/stash-search.js +168 -164
- package/dist/src/stash-show.d.ts +1 -1
- package/dist/src/stash-show.js +28 -96
- package/dist/src/stash-source.d.ts +24 -0
- package/dist/src/stash-source.js +81 -0
- package/dist/src/stash-types.d.ts +14 -4
- package/dist/src/stash.d.ts +6 -0
- package/dist/src/stash.js +3 -0
- package/dist/src/tool-runner.d.ts +1 -1
- package/dist/src/tool-runner.js +18 -5
- package/package.json +7 -2
- package/src/asset-spec.ts +20 -4
- package/src/asset-type-handler.ts +77 -0
- package/src/cli.ts +213 -82
- package/src/common.ts +23 -5
- package/src/config-cli.ts +499 -0
- package/src/config.ts +160 -38
- package/src/db.ts +411 -0
- package/src/embedder.ts +22 -11
- package/src/github.ts +21 -0
- package/src/handlers/agent-handler.ts +32 -0
- package/src/handlers/command-handler.ts +29 -0
- package/src/handlers/index.ts +25 -0
- package/src/handlers/knowledge-handler.ts +62 -0
- package/src/handlers/markdown-helpers.ts +19 -0
- package/src/handlers/script-handler.ts +92 -0
- package/src/handlers/skill-handler.ts +37 -0
- package/src/handlers/tool-handler.ts +71 -0
- package/src/indexer.ts +208 -187
- package/src/init.ts +17 -9
- package/src/llm.ts +4 -3
- package/src/metadata.ts +5 -65
- package/src/origin-resolve.ts +67 -0
- package/src/registry-install.ts +158 -42
- package/src/registry-resolve.ts +92 -23
- package/src/registry-search.ts +288 -98
- package/src/registry-types.ts +10 -2
- package/src/stash-add.ts +14 -17
- package/src/stash-clone.ts +127 -0
- package/src/stash-ref.ts +84 -26
- package/src/stash-registry.ts +12 -12
- package/src/stash-resolve.ts +3 -0
- package/src/stash-search.ts +202 -184
- package/src/stash-show.ts +33 -90
- package/src/stash-source.ts +103 -0
- package/src/stash-types.ts +14 -4
- package/src/stash.ts +8 -0
- package/src/tool-runner.ts +18 -5
- package/dist/src/similarity.d.ts +0 -34
- package/src/similarity.ts +0 -271
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import type { StashSource } from "./stash-source"
|
|
3
|
+
import { parseRegistryRef } from "./registry-resolve"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Given an origin string (from an AssetRef) and the full list of stash
|
|
7
|
+
* sources, return the subset of sources to search.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order:
|
|
10
|
+
* 1. undefined → all sources (working → mounted → installed)
|
|
11
|
+
* 2. "local" → working stash only
|
|
12
|
+
* 3. exact match → installed source whose registryId matches verbatim
|
|
13
|
+
* 4. parsed match → parse origin as a registry ref, match by parsed ID
|
|
14
|
+
* 5. path match → mounted source whose path matches
|
|
15
|
+
* 6. empty → indicates a remote/uninstalled origin (caller decides)
|
|
16
|
+
*/
|
|
17
|
+
export function resolveSourcesForOrigin(
|
|
18
|
+
origin: string | undefined,
|
|
19
|
+
allSources: StashSource[],
|
|
20
|
+
): StashSource[] {
|
|
21
|
+
if (!origin) return allSources
|
|
22
|
+
|
|
23
|
+
if (origin === "local") {
|
|
24
|
+
return allSources.filter((s) => s.kind === "working")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Exact registryId match (e.g. origin is "npm:@scope/pkg")
|
|
28
|
+
const byExactId = allSources.filter(
|
|
29
|
+
(s) => s.kind === "installed" && s.registryId === origin,
|
|
30
|
+
)
|
|
31
|
+
if (byExactId.length > 0) return byExactId
|
|
32
|
+
|
|
33
|
+
// Parse origin as a registry ref and match by parsed ID.
|
|
34
|
+
// Allows shorthand: "owner/repo" matches "github:owner/repo",
|
|
35
|
+
// "@scope/pkg" matches "npm:@scope/pkg".
|
|
36
|
+
try {
|
|
37
|
+
const parsed = parseRegistryRef(origin)
|
|
38
|
+
const byParsedId = allSources.filter(
|
|
39
|
+
(s) => s.kind === "installed" && s.registryId === parsed.id,
|
|
40
|
+
)
|
|
41
|
+
if (byParsedId.length > 0) return byParsedId
|
|
42
|
+
} catch {
|
|
43
|
+
// Not a valid registry ref — continue to path matching
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Mounted stash by resolved path
|
|
47
|
+
const resolvedOrigin = path.resolve(origin)
|
|
48
|
+
const byPath = allSources.filter(
|
|
49
|
+
(s) => s.kind === "mounted" && path.resolve(s.path) === resolvedOrigin,
|
|
50
|
+
)
|
|
51
|
+
if (byPath.length > 0) return byPath
|
|
52
|
+
|
|
53
|
+
// No match — origin may be remote/uninstalled
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check whether an origin refers to something that could be fetched remotely
|
|
59
|
+
* (i.e. it looks like a registry ref but isn't installed locally).
|
|
60
|
+
*/
|
|
61
|
+
export function isRemoteOrigin(
|
|
62
|
+
origin: string,
|
|
63
|
+
allSources: StashSource[],
|
|
64
|
+
): boolean {
|
|
65
|
+
if (origin === "local") return false
|
|
66
|
+
return resolveSourcesForOrigin(origin, allSources).length === 0
|
|
67
|
+
}
|
package/src/registry-install.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process"
|
|
2
2
|
import fs from "node:fs"
|
|
3
3
|
import path from "node:path"
|
|
4
|
-
import { TYPE_DIRS } from "./common"
|
|
4
|
+
import { fetchWithTimeout, isWithin, TYPE_DIRS } from "./common"
|
|
5
5
|
import { loadConfig, saveConfig, type AgentikitConfig } from "./config"
|
|
6
6
|
import { parseRegistryRef, resolveRegistryArtifact } from "./registry-resolve"
|
|
7
|
-
import type { RegistryInstallResult, RegistryInstalledEntry, RegistrySource } from "./registry-types"
|
|
7
|
+
import type { ParsedGitRef, RegistryInstallResult, RegistryInstalledEntry, RegistrySource } from "./registry-types"
|
|
8
8
|
|
|
9
9
|
const REGISTRY_STASH_DIR_NAMES = new Set<string>(Object.values(TYPE_DIRS))
|
|
10
10
|
|
|
@@ -15,6 +15,9 @@ export interface InstallRegistryRefOptions {
|
|
|
15
15
|
|
|
16
16
|
export async function installRegistryRef(ref: string, options?: InstallRegistryRefOptions): Promise<RegistryInstallResult> {
|
|
17
17
|
const parsed = parseRegistryRef(ref)
|
|
18
|
+
if (parsed.source === "git") {
|
|
19
|
+
return installGitRegistryRef(parsed, options)
|
|
20
|
+
}
|
|
18
21
|
const resolved = await resolveRegistryArtifact(parsed)
|
|
19
22
|
|
|
20
23
|
const installedAt = (options?.now ?? new Date()).toISOString()
|
|
@@ -28,6 +31,42 @@ export async function installRegistryRef(ref: string, options?: InstallRegistryR
|
|
|
28
31
|
await downloadArchive(resolved.artifactUrl, archivePath)
|
|
29
32
|
extractTarGzSecure(archivePath, extractedDir)
|
|
30
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
|
+
|
|
31
70
|
const stashRoot = detectStashRoot(extractedDir)
|
|
32
71
|
|
|
33
72
|
return {
|
|
@@ -44,49 +83,30 @@ export async function installRegistryRef(ref: string, options?: InstallRegistryR
|
|
|
44
83
|
}
|
|
45
84
|
}
|
|
46
85
|
|
|
47
|
-
export function upsertInstalledRegistryEntry(entry: RegistryInstalledEntry
|
|
48
|
-
const current = loadConfig(
|
|
86
|
+
export function upsertInstalledRegistryEntry(entry: RegistryInstalledEntry): AgentikitConfig {
|
|
87
|
+
const current = loadConfig()
|
|
49
88
|
const currentInstalled = current.registry?.installed ?? []
|
|
50
|
-
const previousRegistryRoots = new Set(currentInstalled.map((item) => path.resolve(item.stashRoot)))
|
|
51
|
-
|
|
52
89
|
const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id)
|
|
53
90
|
const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)]
|
|
54
|
-
const nextRegistryRoots = new Set(nextInstalled.map((item) => path.resolve(item.stashRoot)))
|
|
55
|
-
const preservedAdditional = current.additionalStashDirs.filter(
|
|
56
|
-
(dir) => !previousRegistryRoots.has(path.resolve(dir)),
|
|
57
|
-
)
|
|
58
|
-
const syncedAdditional = uniquePaths([...preservedAdditional, ...nextRegistryRoots])
|
|
59
91
|
|
|
60
92
|
const nextConfig: AgentikitConfig = {
|
|
61
93
|
...current,
|
|
62
|
-
|
|
63
|
-
registry: {
|
|
64
|
-
installed: nextInstalled,
|
|
65
|
-
},
|
|
94
|
+
registry: { installed: nextInstalled },
|
|
66
95
|
}
|
|
67
|
-
saveConfig(nextConfig
|
|
96
|
+
saveConfig(nextConfig)
|
|
68
97
|
return nextConfig
|
|
69
98
|
}
|
|
70
99
|
|
|
71
|
-
export function removeInstalledRegistryEntry(id: string
|
|
72
|
-
const current = loadConfig(
|
|
100
|
+
export function removeInstalledRegistryEntry(id: string): AgentikitConfig {
|
|
101
|
+
const current = loadConfig()
|
|
73
102
|
const currentInstalled = current.registry?.installed ?? []
|
|
74
|
-
const previousRegistryRoots = new Set(currentInstalled.map((item) => path.resolve(item.stashRoot)))
|
|
75
|
-
|
|
76
103
|
const nextInstalled = currentInstalled.filter((item) => item.id !== id)
|
|
77
|
-
const nextRegistryRoots = new Set(nextInstalled.map((item) => path.resolve(item.stashRoot)))
|
|
78
|
-
|
|
79
|
-
const preservedAdditional = current.additionalStashDirs.filter(
|
|
80
|
-
(dir) => !previousRegistryRoots.has(path.resolve(dir)),
|
|
81
|
-
)
|
|
82
|
-
const syncedAdditional = uniquePaths([...preservedAdditional, ...nextRegistryRoots])
|
|
83
104
|
|
|
84
105
|
const nextConfig: AgentikitConfig = {
|
|
85
106
|
...current,
|
|
86
|
-
additionalStashDirs: syncedAdditional,
|
|
87
107
|
registry: nextInstalled.length > 0 ? { installed: nextInstalled } : undefined,
|
|
88
108
|
}
|
|
89
|
-
saveConfig(nextConfig
|
|
109
|
+
saveConfig(nextConfig)
|
|
90
110
|
return nextConfig
|
|
91
111
|
}
|
|
92
112
|
|
|
@@ -131,13 +151,37 @@ function buildInstallCacheDir(cacheRootDir: string, source: RegistrySource, id:
|
|
|
131
151
|
return path.join(cacheRootDir, slug || source, stamp)
|
|
132
152
|
}
|
|
133
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
|
+
|
|
134
169
|
async function downloadArchive(url: string, destination: string): Promise<void> {
|
|
135
|
-
const response = await
|
|
170
|
+
const response = await fetchWithTimeout(url, undefined, 120_000)
|
|
136
171
|
if (!response.ok) {
|
|
137
172
|
throw new Error(`Failed to download archive (${response.status}) from ${url}`)
|
|
138
173
|
}
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|
|
141
185
|
}
|
|
142
186
|
|
|
143
187
|
function extractTarGzSecure(archivePath: string, destinationDir: string): void {
|
|
@@ -195,6 +239,89 @@ function isDirectory(target: string): boolean {
|
|
|
195
239
|
}
|
|
196
240
|
}
|
|
197
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
|
+
|
|
198
325
|
function hasStashDirs(dirPath: string): boolean {
|
|
199
326
|
if (!isDirectory(dirPath)) return false
|
|
200
327
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
@@ -232,14 +359,3 @@ function normalizeInstalledEntry(entry: RegistryInstalledEntry): RegistryInstall
|
|
|
232
359
|
}
|
|
233
360
|
}
|
|
234
361
|
|
|
235
|
-
function uniquePaths(paths: Iterable<string>): string[] {
|
|
236
|
-
const seen = new Set<string>()
|
|
237
|
-
const result: string[] = []
|
|
238
|
-
for (const candidate of paths) {
|
|
239
|
-
const normalized = path.resolve(candidate)
|
|
240
|
-
if (seen.has(normalized)) continue
|
|
241
|
-
seen.add(normalized)
|
|
242
|
-
result.push(normalized)
|
|
243
|
-
}
|
|
244
|
-
return result
|
|
245
|
-
}
|
package/src/registry-resolve.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
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"
|
|
4
8
|
|
|
5
9
|
export function parseRegistryRef(rawRef: string): ParsedRegistryRef {
|
|
6
10
|
const ref = rawRef.trim()
|
|
@@ -15,6 +19,10 @@ export function parseRegistryRef(rawRef: string): ParsedRegistryRef {
|
|
|
15
19
|
if (ref.startsWith("http://") || ref.startsWith("https://")) {
|
|
16
20
|
return parseGithubUrl(ref)
|
|
17
21
|
}
|
|
22
|
+
const localGitRef = tryParseLocalGitRef(ref, isPathLikeRef(ref))
|
|
23
|
+
if (localGitRef) {
|
|
24
|
+
return localGitRef
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
if (ref.startsWith("@") || !looksLikeGithubOwnerRepo(ref)) {
|
|
20
28
|
return parseNpmRef(ref, ref)
|
|
@@ -27,6 +35,9 @@ export async function resolveRegistryArtifact(parsed: ParsedRegistryRef): Promis
|
|
|
27
35
|
if (parsed.source === "npm") {
|
|
28
36
|
return resolveNpmArtifact(parsed)
|
|
29
37
|
}
|
|
38
|
+
if (parsed.source === "git") {
|
|
39
|
+
return resolveGitArtifact(parsed)
|
|
40
|
+
}
|
|
30
41
|
return resolveGithubArtifact(parsed)
|
|
31
42
|
}
|
|
32
43
|
|
|
@@ -96,6 +107,45 @@ function parseGithubUrl(rawUrl: string): ParsedGithubRef {
|
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
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
|
+
|
|
99
149
|
async function resolveNpmArtifact(parsed: ParsedNpmRef): Promise<ResolvedRegistryArtifact> {
|
|
100
150
|
const encodedName = encodeURIComponent(parsed.packageName)
|
|
101
151
|
const metadata = await fetchJson<Record<string, unknown>>(`https://registry.npmjs.org/${encodedName}`)
|
|
@@ -197,6 +247,17 @@ async function resolveGithubArtifact(parsed: ParsedGithubRef): Promise<ResolvedR
|
|
|
197
247
|
}
|
|
198
248
|
}
|
|
199
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
|
+
|
|
200
261
|
function splitNpmNameAndVersion(input: string): { packageName: string; requestedVersionOrTag?: string } {
|
|
201
262
|
if (input.startsWith("@")) {
|
|
202
263
|
const secondAt = input.indexOf("@", 1)
|
|
@@ -220,8 +281,16 @@ function splitNpmNameAndVersion(input: string): { packageName: string; requested
|
|
|
220
281
|
}
|
|
221
282
|
|
|
222
283
|
function validateNpmPackageName(name: string): void {
|
|
223
|
-
if (!name
|
|
224
|
-
|
|
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.`)
|
|
225
294
|
}
|
|
226
295
|
}
|
|
227
296
|
|
|
@@ -237,18 +306,27 @@ function splitRefSuffix(value: string): [string, string | undefined] {
|
|
|
237
306
|
return [value.slice(0, hash), value.slice(hash + 1) || undefined]
|
|
238
307
|
}
|
|
239
308
|
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
245
318
|
}
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
248
326
|
}
|
|
249
327
|
|
|
250
328
|
async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T> {
|
|
251
|
-
const response = await
|
|
329
|
+
const response = await fetchWithTimeout(url, { headers })
|
|
252
330
|
if (!response.ok) {
|
|
253
331
|
throw new Error(`Request failed (${response.status}) for ${url}`)
|
|
254
332
|
}
|
|
@@ -256,17 +334,8 @@ async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T> {
|
|
|
256
334
|
}
|
|
257
335
|
|
|
258
336
|
async function tryFetchJson<T>(url: string, headers?: HeadersInit): Promise<T | null> {
|
|
259
|
-
const response = await
|
|
337
|
+
const response = await fetchWithTimeout(url, { headers })
|
|
260
338
|
if (!response.ok) return null
|
|
261
339
|
return await response.json() as T
|
|
262
340
|
}
|
|
263
341
|
|
|
264
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
265
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
266
|
-
? value as Record<string, unknown>
|
|
267
|
-
: {}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function asString(value: unknown): string | undefined {
|
|
271
|
-
return typeof value === "string" && value ? value : undefined
|
|
272
|
-
}
|