akm-cli 0.5.0 → 0.6.0-rc1

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 (74) hide show
  1. package/CHANGELOG.md +32 -5
  2. package/dist/asset-registry.js +29 -5
  3. package/dist/asset-spec.js +12 -5
  4. package/dist/cli-hints.js +300 -0
  5. package/dist/cli.js +218 -1357
  6. package/dist/common.js +147 -50
  7. package/dist/config.js +224 -13
  8. package/dist/create-provider-registry.js +1 -1
  9. package/dist/curate.js +258 -0
  10. package/dist/{local-search.js → db-search.js} +30 -19
  11. package/dist/db.js +168 -62
  12. package/dist/embedder.js +49 -273
  13. package/dist/embedders/cache.js +47 -0
  14. package/dist/embedders/local.js +152 -0
  15. package/dist/embedders/remote.js +121 -0
  16. package/dist/embedders/types.js +39 -0
  17. package/dist/errors.js +14 -3
  18. package/dist/frontmatter.js +61 -7
  19. package/dist/indexer.js +38 -7
  20. package/dist/info.js +2 -2
  21. package/dist/install-audit.js +16 -1
  22. package/dist/{installed-kits.js → installed-stashes.js} +48 -22
  23. package/dist/llm-client.js +92 -0
  24. package/dist/llm.js +14 -126
  25. package/dist/lockfile.js +28 -1
  26. package/dist/matchers.js +1 -1
  27. package/dist/metadata-enhance.js +53 -0
  28. package/dist/migration-help.js +75 -44
  29. package/dist/output-context.js +77 -0
  30. package/dist/output-shapes.js +198 -0
  31. package/dist/output-text.js +520 -0
  32. package/dist/paths.js +4 -4
  33. package/dist/providers/index.js +11 -0
  34. package/dist/providers/skills-sh.js +1 -1
  35. package/dist/providers/static-index.js +47 -45
  36. package/dist/registry-build-index.js +36 -29
  37. package/dist/registry-factory.js +2 -2
  38. package/dist/registry-resolve.js +8 -4
  39. package/dist/registry-search.js +62 -5
  40. package/dist/remember.js +172 -0
  41. package/dist/renderers.js +52 -0
  42. package/dist/search-source.js +73 -42
  43. package/dist/setup-steps.js +45 -0
  44. package/dist/setup.js +149 -76
  45. package/dist/stash-add.js +94 -38
  46. package/dist/stash-clone.js +4 -4
  47. package/dist/stash-provider-factory.js +2 -2
  48. package/dist/stash-provider.js +3 -1
  49. package/dist/stash-providers/filesystem.js +31 -1
  50. package/dist/stash-providers/git.js +209 -8
  51. package/dist/stash-providers/index.js +1 -0
  52. package/dist/stash-providers/npm.js +159 -0
  53. package/dist/stash-providers/provider-utils.js +162 -0
  54. package/dist/stash-providers/sync-from-ref.js +45 -0
  55. package/dist/stash-providers/tar-utils.js +151 -0
  56. package/dist/stash-providers/website.js +80 -4
  57. package/dist/stash-resolve.js +5 -5
  58. package/dist/stash-search.js +4 -4
  59. package/dist/stash-show.js +3 -3
  60. package/dist/wiki.js +6 -6
  61. package/dist/workflow-authoring.js +12 -4
  62. package/dist/workflow-markdown.js +9 -0
  63. package/dist/workflow-runs.js +12 -2
  64. package/docs/README.md +30 -0
  65. package/docs/migration/release-notes/0.0.13.md +4 -0
  66. package/docs/migration/release-notes/0.1.0.md +6 -0
  67. package/docs/migration/release-notes/0.2.0.md +6 -0
  68. package/docs/migration/release-notes/0.3.0.md +5 -0
  69. package/docs/migration/release-notes/0.5.0.md +6 -0
  70. package/docs/migration/release-notes/0.6.0.md +29 -0
  71. package/docs/migration/release-notes/README.md +21 -0
  72. package/package.json +3 -2
  73. package/dist/registry-install.js +0 -532
  74. /package/dist/{kit-include.js → stash-include.js} +0 -0
@@ -1,532 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { TYPE_DIRS } from "./asset-spec";
6
- import { fetchWithRetry, isWithin } from "./common";
7
- import { loadConfig, loadUserConfig, saveConfig } from "./config";
8
- import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
9
- import { copyIncludedPaths, findNearestIncludeConfig } from "./kit-include";
10
- import { getRegistryCacheDir as _getRegistryCacheDir } from "./paths";
11
- import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "./registry-resolve";
12
- import { warn } from "./warn";
13
- const REGISTRY_STASH_DIR_NAMES = new Set(Object.values(TYPE_DIRS));
14
- export async function installRegistryRef(ref, options) {
15
- const parsed = parseRegistryRef(ref);
16
- const config = loadConfig();
17
- if (parsed.source === "local") {
18
- return installLocalRegistryRef(parsed, config, options);
19
- }
20
- if (parsed.source === "git") {
21
- return installGitRegistryRef(parsed, config, options);
22
- }
23
- if (parsed.source === "github") {
24
- return installGithubRegistryRef(parsed, config, options);
25
- }
26
- const resolved = await resolveRegistryArtifact(parsed);
27
- const registryLabels = deriveRegistryLabels({
28
- source: resolved.source,
29
- ref: resolved.ref,
30
- artifactUrl: resolved.artifactUrl,
31
- });
32
- enforceRegistryInstallPolicy(registryLabels, config, ref);
33
- const installedAt = (options?.now ?? new Date()).toISOString();
34
- const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
35
- const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id, resolved.resolvedVersion ?? resolved.resolvedRevision);
36
- const archivePath = path.join(cacheDir, "artifact.tar.gz");
37
- const extractedDir = path.join(cacheDir, "extracted");
38
- // Check for cache hit: if extracted dir already exists and has a valid stash root, reuse it
39
- if (isDirectory(extractedDir)) {
40
- try {
41
- const cachedStashRoot = detectStashRoot(extractedDir);
42
- if (cachedStashRoot) {
43
- const integrity = fs.existsSync(archivePath) ? await computeFileHash(archivePath) : undefined;
44
- const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
45
- return {
46
- id: resolved.id,
47
- source: resolved.source,
48
- ref: resolved.ref,
49
- artifactUrl: resolved.artifactUrl,
50
- resolvedVersion: resolved.resolvedVersion,
51
- resolvedRevision: resolved.resolvedRevision,
52
- installedAt,
53
- cacheDir,
54
- extractedDir,
55
- stashRoot: cachedStashRoot,
56
- integrity,
57
- writable: options?.writable,
58
- audit,
59
- };
60
- }
61
- }
62
- catch {
63
- // Cache invalid, re-download
64
- }
65
- }
66
- fs.mkdirSync(cacheDir, { recursive: true });
67
- let integrity;
68
- let provisionalKitRoot;
69
- let installRoot;
70
- let stashRoot;
71
- let audit;
72
- try {
73
- await downloadArchive(resolved.artifactUrl, archivePath);
74
- verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
75
- integrity = await computeFileHash(archivePath);
76
- extractTarGzSecure(archivePath, extractedDir);
77
- audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
78
- provisionalKitRoot = detectStashRoot(extractedDir);
79
- installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
80
- stashRoot = detectStashRoot(installRoot);
81
- }
82
- catch (err) {
83
- // Clean up the cache directory so stale or partially-extracted artifacts
84
- // don't cause false cache hits on the next install attempt.
85
- try {
86
- fs.rmSync(cacheDir, { recursive: true, force: true });
87
- }
88
- catch {
89
- // Best-effort cleanup; ignore errors
90
- }
91
- throw err;
92
- }
93
- return {
94
- id: resolved.id,
95
- source: resolved.source,
96
- ref: resolved.ref,
97
- artifactUrl: resolved.artifactUrl,
98
- resolvedVersion: resolved.resolvedVersion,
99
- resolvedRevision: resolved.resolvedRevision,
100
- installedAt,
101
- cacheDir,
102
- extractedDir,
103
- stashRoot,
104
- integrity,
105
- writable: options?.writable,
106
- audit,
107
- };
108
- }
109
- async function installGithubRegistryRef(parsed, config, options) {
110
- const gitParsed = {
111
- source: "git",
112
- ref: parsed.ref,
113
- id: parsed.id,
114
- url: `https://github.com/${parsed.owner}/${parsed.repo}.git`,
115
- requestedRef: parsed.requestedRef,
116
- };
117
- const installed = await installGitRegistryRef(gitParsed, config, options);
118
- return {
119
- ...installed,
120
- source: "github",
121
- };
122
- }
123
- async function installLocalRegistryRef(parsed, config, options) {
124
- const resolved = await resolveRegistryArtifact(parsed);
125
- const installedAt = (options?.now ?? new Date()).toISOString();
126
- const registryLabels = deriveRegistryLabels({
127
- source: resolved.source,
128
- ref: resolved.ref,
129
- artifactUrl: resolved.artifactUrl,
130
- });
131
- const audit = runInstallAuditOrThrow(parsed.sourcePath, resolved.source, resolved.ref, registryLabels, config, options);
132
- // For local directories, detect the stash root within the source path.
133
- // If no nested stash is found, the source path itself is used.
134
- const stashRoot = detectStashRoot(parsed.sourcePath);
135
- return {
136
- id: resolved.id,
137
- source: resolved.source,
138
- ref: resolved.ref,
139
- artifactUrl: resolved.artifactUrl,
140
- resolvedVersion: resolved.resolvedVersion,
141
- resolvedRevision: resolved.resolvedRevision,
142
- installedAt,
143
- cacheDir: parsed.sourcePath,
144
- extractedDir: parsed.sourcePath,
145
- stashRoot,
146
- writable: options?.writable,
147
- audit,
148
- };
149
- }
150
- async function installGitRegistryRef(parsed, config, options) {
151
- const resolved = await resolveRegistryArtifact(parsed);
152
- const registryLabels = deriveRegistryLabels({
153
- source: resolved.source,
154
- ref: resolved.ref,
155
- artifactUrl: resolved.artifactUrl,
156
- gitUrl: parsed.url,
157
- });
158
- enforceRegistryInstallPolicy(registryLabels, config, parsed.ref);
159
- const installedAt = (options?.now ?? new Date()).toISOString();
160
- const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
161
- const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, resolved.resolvedRevision);
162
- const cloneDir = path.join(cacheDir, "clone");
163
- const extractedDir = path.join(cacheDir, "extracted");
164
- // Check for cache hit
165
- if (isDirectory(extractedDir)) {
166
- try {
167
- const provisionalKitRoot = detectStashRoot(extractedDir);
168
- const installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
169
- const stashRoot = detectStashRoot(installRoot);
170
- if (stashRoot) {
171
- const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
172
- return {
173
- id: resolved.id,
174
- source: resolved.source,
175
- ref: resolved.ref,
176
- artifactUrl: resolved.artifactUrl,
177
- resolvedVersion: resolved.resolvedVersion,
178
- resolvedRevision: resolved.resolvedRevision,
179
- installedAt,
180
- cacheDir,
181
- extractedDir,
182
- stashRoot,
183
- writable: options?.writable,
184
- audit,
185
- };
186
- }
187
- }
188
- catch {
189
- // Cache invalid, re-clone
190
- }
191
- }
192
- fs.mkdirSync(cacheDir, { recursive: true });
193
- // Validate URL and ref before passing to git to prevent command injection
194
- validateGitUrl(parsed.url);
195
- if (parsed.requestedRef)
196
- validateGitRef(parsed.requestedRef);
197
- let provisionalKitRoot;
198
- let installRoot;
199
- let stashRoot;
200
- let audit;
201
- try {
202
- const cloneArgs = ["clone", "--depth", "1"];
203
- if (parsed.requestedRef) {
204
- cloneArgs.push("--branch", parsed.requestedRef);
205
- }
206
- cloneArgs.push(parsed.url, cloneDir);
207
- const cloneResult = spawnSync("git", cloneArgs, { encoding: "utf8", timeout: 120_000 });
208
- if (cloneResult.status !== 0) {
209
- const err = cloneResult.stderr?.trim() || cloneResult.error?.message || "unknown error";
210
- throw new Error(`Failed to clone ${parsed.url}: ${err}`);
211
- }
212
- // Copy contents to extracted dir without .git
213
- fs.mkdirSync(extractedDir, { recursive: true });
214
- copyDirectoryContents(cloneDir, extractedDir);
215
- // Clean up the clone dir
216
- fs.rmSync(cloneDir, { recursive: true, force: true });
217
- audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
218
- provisionalKitRoot = detectStashRoot(extractedDir);
219
- installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
220
- stashRoot = detectStashRoot(installRoot);
221
- }
222
- catch (err) {
223
- // Clean up the cache directory so stale or partially-cloned artifacts
224
- // don't cause false cache hits on the next install attempt.
225
- try {
226
- fs.rmSync(cacheDir, { recursive: true, force: true });
227
- }
228
- catch {
229
- // Best-effort cleanup; ignore errors
230
- }
231
- throw err;
232
- }
233
- return {
234
- id: resolved.id,
235
- source: resolved.source,
236
- ref: resolved.ref,
237
- artifactUrl: resolved.artifactUrl,
238
- resolvedVersion: resolved.resolvedVersion,
239
- resolvedRevision: resolved.resolvedRevision,
240
- installedAt,
241
- cacheDir,
242
- extractedDir,
243
- stashRoot,
244
- writable: options?.writable,
245
- audit,
246
- };
247
- }
248
- export function upsertInstalledRegistryEntry(entry) {
249
- const current = loadUserConfig();
250
- const currentInstalled = current.installed ?? [];
251
- const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id);
252
- const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)];
253
- const nextConfig = {
254
- ...current,
255
- installed: nextInstalled,
256
- };
257
- saveConfig(nextConfig);
258
- return nextConfig;
259
- }
260
- export function removeInstalledRegistryEntry(id) {
261
- const current = loadUserConfig();
262
- const currentInstalled = current.installed ?? [];
263
- const nextInstalled = currentInstalled.filter((item) => item.id !== id);
264
- const nextConfig = {
265
- ...current,
266
- installed: nextInstalled.length > 0 ? nextInstalled : undefined,
267
- };
268
- saveConfig(nextConfig);
269
- return nextConfig;
270
- }
271
- export function getRegistryCacheRootDir() {
272
- return _getRegistryCacheDir();
273
- }
274
- export function detectStashRoot(extractedDir) {
275
- const root = path.resolve(extractedDir);
276
- const rootDotStash = path.join(root, ".stash");
277
- if (isDirectory(rootDotStash)) {
278
- return root;
279
- }
280
- if (hasStashDirs(root)) {
281
- return root;
282
- }
283
- const shallowest = findShallowestStashRoot(root);
284
- if (shallowest)
285
- return shallowest;
286
- return root;
287
- }
288
- function buildInstallCacheDir(cacheRootDir, source, id, version) {
289
- const slug = `${source}-${id.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "")}`;
290
- const versionSlug = source === "local"
291
- ? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
292
- : (version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
293
- return path.join(cacheRootDir, slug || source, versionSlug);
294
- }
295
- function applyAkmIncludeConfig(sourceRoot, cacheDir, searchRoot = sourceRoot) {
296
- const includeConfig = findNearestIncludeConfig(sourceRoot, searchRoot);
297
- if (!includeConfig)
298
- return undefined;
299
- const selectedDir = path.join(cacheDir, "selected");
300
- fs.rmSync(selectedDir, { recursive: true, force: true });
301
- fs.mkdirSync(selectedDir, { recursive: true });
302
- copyIncludedPaths(includeConfig.include, includeConfig.baseDir, selectedDir);
303
- return selectedDir;
304
- }
305
- async function downloadArchive(url, destination) {
306
- const response = await fetchWithRetry(url, undefined, { timeout: 120_000 });
307
- if (!response.ok) {
308
- throw new Error(`Failed to download archive (${response.status}) from ${url}`);
309
- }
310
- // Stream response to disk instead of buffering the entire archive in memory.
311
- // Uses Bun.write which handles Response streaming natively.
312
- const BunRuntime = globalThis
313
- .Bun;
314
- if (BunRuntime?.write) {
315
- await BunRuntime.write(destination, response);
316
- }
317
- else {
318
- // Fallback for non-Bun environments (e.g., tests)
319
- const arrayBuffer = await response.arrayBuffer();
320
- fs.writeFileSync(destination, Buffer.from(arrayBuffer));
321
- }
322
- }
323
- export function verifyArchiveIntegrity(archivePath, expected, source) {
324
- if (!expected)
325
- return;
326
- // For GitHub and git sources, resolvedRevision is a commit SHA, not a content hash.
327
- // Content integrity cannot be verified from a commit hash, so skip verification.
328
- if (source === "github" || source === "git")
329
- return;
330
- const fileBuffer = fs.readFileSync(archivePath);
331
- // SRI hash format: sha256-<base64> or sha512-<base64>
332
- if (expected.startsWith("sha256-") || expected.startsWith("sha512-")) {
333
- const dashIndex = expected.indexOf("-");
334
- const algorithm = expected.slice(0, dashIndex);
335
- const expectedBase64 = expected.slice(dashIndex + 1);
336
- const actualBase64 = createHash(algorithm).update(fileBuffer).digest("base64");
337
- if (actualBase64 !== expectedBase64) {
338
- fs.unlinkSync(archivePath);
339
- throw new Error(`Integrity check failed for ${archivePath}: expected ${algorithm} digest ${expectedBase64}, got ${actualBase64}`);
340
- }
341
- return;
342
- }
343
- // Hex shasum (SHA-1 from npm)
344
- if (/^[0-9a-f]{40}$/i.test(expected)) {
345
- const actualHex = createHash("sha1").update(fileBuffer).digest("hex");
346
- if (actualHex.toLowerCase() !== expected.toLowerCase()) {
347
- fs.unlinkSync(archivePath);
348
- throw new Error(`Integrity check failed for ${archivePath}: expected sha1 ${expected}, got ${actualHex}`);
349
- }
350
- return;
351
- }
352
- // Unrecognized format — warn and skip verification
353
- warn("Unrecognized integrity format: %s — verification skipped", expected);
354
- }
355
- export function extractTarGzSecure(archivePath, destinationDir) {
356
- const listResult = spawnSync("tar", ["tzf", archivePath], { encoding: "utf8" });
357
- if (listResult.status !== 0) {
358
- const err = listResult.stderr?.trim() || listResult.error?.message || "unknown error";
359
- throw new Error(`Failed to inspect archive ${archivePath}: ${err}`);
360
- }
361
- validateTarEntries(listResult.stdout);
362
- fs.rmSync(destinationDir, { recursive: true, force: true });
363
- fs.mkdirSync(destinationDir, { recursive: true });
364
- const extractResult = spawnSync("tar", ["xzf", archivePath, "--no-same-owner", "--strip-components=1", "-C", destinationDir], { encoding: "utf8" });
365
- if (extractResult.status !== 0) {
366
- const err = extractResult.stderr?.trim() || extractResult.error?.message || "unknown error";
367
- throw new Error(`Failed to extract archive ${archivePath}: ${err}`);
368
- }
369
- // Post-extraction scan: verify all extracted files are within destinationDir
370
- // This mitigates TOCTOU between validateTarEntries (list) and tar extract.
371
- scanExtractedFiles(destinationDir, destinationDir);
372
- }
373
- function scanExtractedFiles(dir, root) {
374
- let entries;
375
- try {
376
- entries = fs.readdirSync(dir, { withFileTypes: true });
377
- }
378
- catch {
379
- return;
380
- }
381
- for (const entry of entries) {
382
- const fullPath = path.join(dir, entry.name);
383
- // Check for ".." segments in names (e.g. symlink tricks or crafted filenames)
384
- if (entry.name.includes("..")) {
385
- throw new Error(`Post-extraction scan: suspicious entry name: ${fullPath}`);
386
- }
387
- // Resolve symlinks to detect escapes outside the destination directory
388
- if (entry.isSymbolicLink()) {
389
- const target = fs.realpathSync(fullPath);
390
- if (!isWithin(target, root)) {
391
- throw new Error(`Post-extraction scan: symlink escapes destination directory: ${fullPath} -> ${target}`);
392
- }
393
- }
394
- if (entry.isDirectory()) {
395
- scanExtractedFiles(fullPath, root);
396
- }
397
- }
398
- }
399
- export function validateTarEntries(listOutput) {
400
- const lines = listOutput.split(/\r?\n/).filter(Boolean);
401
- for (const rawLine of lines) {
402
- const entry = rawLine.trim();
403
- if (!entry || entry.includes("\0")) {
404
- throw new Error(`Archive contains an invalid entry: ${JSON.stringify(rawLine)}`);
405
- }
406
- if (entry.startsWith("/")) {
407
- throw new Error(`Archive contains an absolute path entry: ${entry}`);
408
- }
409
- const normalized = path.posix.normalize(entry);
410
- if (normalized === ".." || normalized.startsWith("../")) {
411
- throw new Error(`Archive contains a path traversal entry: ${entry}`);
412
- }
413
- const parts = normalized.split("/").filter(Boolean);
414
- const stripped = parts.slice(1).join("/");
415
- if (!stripped)
416
- continue;
417
- const normalizedStripped = path.posix.normalize(stripped);
418
- if (normalizedStripped === ".." ||
419
- normalizedStripped.startsWith("../") ||
420
- path.posix.isAbsolute(normalizedStripped)) {
421
- throw new Error(`Archive contains an unsafe entry after strip-components: ${entry}`);
422
- }
423
- }
424
- }
425
- function isDirectory(target) {
426
- try {
427
- return fs.statSync(target).isDirectory();
428
- }
429
- catch {
430
- return false;
431
- }
432
- }
433
- function hasStashDirs(dirPath) {
434
- if (!isDirectory(dirPath))
435
- return false;
436
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
437
- return entries.some((entry) => entry.isDirectory() && REGISTRY_STASH_DIR_NAMES.has(entry.name));
438
- }
439
- function countStashDirs(dirPath) {
440
- if (!isDirectory(dirPath))
441
- return 0;
442
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
443
- return entries.filter((entry) => entry.isDirectory() && REGISTRY_STASH_DIR_NAMES.has(entry.name)).length;
444
- }
445
- /**
446
- * BFS to find the shallowest directory that looks like a stash root.
447
- * Checks for both `.stash` directories and well-known type directories
448
- * (scripts/, skills/, etc.), so nested layouts like `project/my-kit/scripts/`
449
- * are discovered even without a `.stash` marker.
450
- *
451
- * Skips `root` itself since the caller already checked it via `hasStashDirs`.
452
- */
453
- const BFS_MAX_DEPTH = 5;
454
- function findShallowestStashRoot(root) {
455
- const queue = [{ dir: root, depth: 0 }];
456
- while (queue.length > 0) {
457
- const item = queue.shift();
458
- if (!item) {
459
- continue;
460
- }
461
- const { dir: current, depth } = item;
462
- if (current !== root) {
463
- // .stash directory is a strong stash marker
464
- if (isDirectory(path.join(current, ".stash"))) {
465
- return current;
466
- }
467
- // Require 2+ type dirs for BFS candidates to avoid false positives.
468
- // A single "scripts/" is too common (skill dirs, npm packages, etc.).
469
- if (countStashDirs(current) >= 2) {
470
- return current;
471
- }
472
- }
473
- if (depth >= BFS_MAX_DEPTH)
474
- continue;
475
- let children;
476
- try {
477
- children = fs.readdirSync(current, { withFileTypes: true });
478
- }
479
- catch {
480
- continue;
481
- }
482
- for (const child of children) {
483
- if (!child.isDirectory())
484
- continue;
485
- if (child.name === ".git" || child.name === "node_modules")
486
- continue;
487
- queue.push({ dir: path.join(current, child.name), depth: depth + 1 });
488
- }
489
- }
490
- return undefined;
491
- }
492
- function normalizeInstalledEntry(entry) {
493
- return {
494
- ...entry,
495
- stashRoot: path.resolve(entry.stashRoot),
496
- cacheDir: path.resolve(entry.cacheDir),
497
- };
498
- }
499
- function copyDirectoryContents(sourceDir, destinationDir) {
500
- for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
501
- if (entry.name === ".git")
502
- continue;
503
- const src = path.join(sourceDir, entry.name);
504
- const dest = path.join(destinationDir, entry.name);
505
- fs.mkdirSync(path.dirname(dest), { recursive: true });
506
- if (entry.isDirectory()) {
507
- fs.cpSync(src, dest, { recursive: true, force: true });
508
- }
509
- else {
510
- fs.copyFileSync(src, dest);
511
- }
512
- }
513
- }
514
- async function computeFileHash(filePath) {
515
- const data = fs.readFileSync(filePath);
516
- const hash = createHash("sha256").update(data).digest("hex");
517
- return `sha256:${hash}`;
518
- }
519
- function runInstallAuditOrThrow(rootDir, source, ref, registryLabels, config, options) {
520
- const audit = auditInstallCandidate({
521
- rootDir,
522
- source,
523
- ref,
524
- registryLabels,
525
- config,
526
- trustThisInstall: options?.trustThisInstall,
527
- });
528
- if (audit.blocked) {
529
- throw new Error(formatInstallAuditFailure(ref, audit));
530
- }
531
- return audit;
532
- }
File without changes