akm-cli 0.5.0 → 0.6.0-rc2

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 (118) hide show
  1. package/CHANGELOG.md +53 -5
  2. package/README.md +9 -9
  3. package/dist/cli.js +379 -1448
  4. package/dist/{completions.js → commands/completions.js} +1 -1
  5. package/dist/{config-cli.js → commands/config-cli.js} +109 -11
  6. package/dist/commands/curate.js +263 -0
  7. package/dist/{info.js → commands/info.js} +17 -11
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +14 -2
  10. package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
  11. package/dist/commands/migration-help.js +141 -0
  12. package/dist/{registry-search.js → commands/registry-search.js} +68 -9
  13. package/dist/commands/remember.js +178 -0
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +106 -81
  17. package/dist/{stash-add.js → commands/source-add.js} +133 -67
  18. package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
  19. package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
  20. package/dist/{vault.js → commands/vault.js} +43 -0
  21. package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
  22. package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
  23. package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
  24. package/dist/{common.js → core/common.js} +147 -50
  25. package/dist/{config.js → core/config.js} +288 -29
  26. package/dist/core/errors.js +90 -0
  27. package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
  28. package/dist/{paths.js → core/paths.js} +4 -4
  29. package/dist/core/write-source.js +280 -0
  30. package/dist/{local-search.js → indexer/db-search.js} +49 -32
  31. package/dist/{db.js → indexer/db.js} +210 -81
  32. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  33. package/dist/{indexer.js → indexer/indexer.js} +153 -30
  34. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  35. package/dist/{matchers.js → indexer/matchers.js} +4 -7
  36. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  37. package/dist/{search-source.js → indexer/search-source.js} +97 -55
  38. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  39. package/dist/{walker.js → indexer/walker.js} +1 -1
  40. package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
  41. package/dist/{llm.js → llm/client.js} +12 -48
  42. package/dist/llm/embedder.js +127 -0
  43. package/dist/llm/embedders/cache.js +47 -0
  44. package/dist/llm/embedders/local.js +152 -0
  45. package/dist/llm/embedders/remote.js +121 -0
  46. package/dist/llm/embedders/types.js +39 -0
  47. package/dist/llm/metadata-enhance.js +53 -0
  48. package/dist/output/cli-hints.js +301 -0
  49. package/dist/output/context.js +95 -0
  50. package/dist/{renderers.js → output/renderers.js} +57 -61
  51. package/dist/output/shapes.js +212 -0
  52. package/dist/output/text.js +520 -0
  53. package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
  54. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  55. package/dist/registry/factory.js +33 -0
  56. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  57. package/dist/registry/providers/index.js +11 -0
  58. package/dist/{providers → registry/providers}/skills-sh.js +60 -4
  59. package/dist/{providers → registry/providers}/static-index.js +126 -56
  60. package/dist/registry/providers/types.js +25 -0
  61. package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
  62. package/dist/{detect.js → setup/detect.js} +0 -27
  63. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  64. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  65. package/dist/{setup.js → setup/setup.js} +162 -129
  66. package/dist/setup/steps.js +45 -0
  67. package/dist/{kit-include.js → sources/include.js} +1 -1
  68. package/dist/sources/provider-factory.js +36 -0
  69. package/dist/sources/provider.js +21 -0
  70. package/dist/sources/providers/filesystem.js +35 -0
  71. package/dist/{stash-providers → sources/providers}/git.js +218 -28
  72. package/dist/{stash-providers → sources/providers}/index.js +4 -4
  73. package/dist/sources/providers/install-types.js +14 -0
  74. package/dist/sources/providers/npm.js +160 -0
  75. package/dist/sources/providers/provider-utils.js +173 -0
  76. package/dist/sources/providers/sync-from-ref.js +45 -0
  77. package/dist/sources/providers/tar-utils.js +154 -0
  78. package/dist/{stash-providers → sources/providers}/website.js +60 -20
  79. package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
  80. package/dist/{wiki.js → wiki/wiki.js} +18 -17
  81. package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
  82. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  83. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  84. package/dist/workflows/document-cache.js +20 -0
  85. package/dist/workflows/parser.js +379 -0
  86. package/dist/workflows/renderer.js +78 -0
  87. package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
  88. package/dist/workflows/schema.js +11 -0
  89. package/dist/workflows/validator.js +48 -0
  90. package/docs/README.md +30 -0
  91. package/docs/migration/release-notes/0.0.13.md +4 -0
  92. package/docs/migration/release-notes/0.1.0.md +6 -0
  93. package/docs/migration/release-notes/0.2.0.md +6 -0
  94. package/docs/migration/release-notes/0.3.0.md +5 -0
  95. package/docs/migration/release-notes/0.5.0.md +6 -0
  96. package/docs/migration/release-notes/0.6.0.md +75 -0
  97. package/docs/migration/release-notes/README.md +21 -0
  98. package/package.json +3 -2
  99. package/dist/embedder.js +0 -351
  100. package/dist/errors.js +0 -34
  101. package/dist/migration-help.js +0 -110
  102. package/dist/registry-factory.js +0 -19
  103. package/dist/registry-install.js +0 -532
  104. package/dist/ripgrep.js +0 -2
  105. package/dist/stash-provider-factory.js +0 -35
  106. package/dist/stash-provider.js +0 -1
  107. package/dist/stash-providers/filesystem.js +0 -41
  108. package/dist/stash-providers/openviking.js +0 -348
  109. package/dist/stash-providers/provider-utils.js +0 -11
  110. package/dist/stash-types.js +0 -1
  111. package/dist/workflow-markdown.js +0 -251
  112. /package/dist/{markdown.js → core/markdown.js} +0 -0
  113. /package/dist/{warn.js → core/warn.js} +0 -0
  114. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  115. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  116. /package/dist/{github.js → integrations/github.js} +0 -0
  117. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  118. /package/dist/{registry-types.js → sources/types.js} +0 -0
@@ -1,110 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";
4
- const EMBEDDED_MIGRATION_GUIDES = {
5
- "0.5.0": `Migration notes for akm v0.5.0
6
-
7
- - New top-level surfaces: \`akm wiki …\`, \`akm workflow …\`, \`akm vault …\`, and \`akm save\`.
8
- - If you tried the unreleased single-wiki LLM prototype, move to the new \`akm wiki …\` workflow.
9
- - Removed from the prototype surface: \`akm lint\`, \`akm import --llm\`, \`akm import --dry-run\`, \`knowledge.pageKinds\`, and the old ingest/lint LLM prompts.
10
- - Existing raw wiki-like content should be moved into \`wikis/<name>/raw/\` and then managed with the new wiki commands.
11
- `,
12
- "0.3.0": `Migration notes for akm v0.3.0
13
-
14
- - The old \`stash\` and \`kit\` command groups were folded into the top-level CLI.
15
- - Use \`akm add\`, \`akm list\`, and \`akm remove\` instead of the older split command surfaces.
16
- - Documentation and examples from older releases should be updated to the unified source model.
17
- `,
18
- "0.2.0": `Migration notes for akm v0.2.0
19
-
20
- - Asset refs are user-facing \`type:name\` values; do not rely on URI-style refs.
21
- - The old fixed asset-type union was replaced by an extensible asset type system.
22
- - \`tool\` assets were removed; use \`script\` assets instead.
23
- - Config and docs should treat remote provider scores and local scores as part of one shared search pipeline.
24
- `,
25
- "0.1.0": `Migration notes for akm v0.1.0
26
-
27
- - The package and project were rebranded from Agent-i-Kit to akm.
28
- - Update package references from \`agent-i-kit\` to \`akm-cli\`.
29
- - Update config, registry, plugin, path, and environment-variable references from \`agent-i-kit\` / \`AGENT_I_KIT_*\` to \`akm\` / \`AKM_*\`.
30
- - The \`tool\` asset type and \`submit\` command were removed.
31
- `,
32
- "0.0.13": `Migration notes for akm v0.0.13
33
-
34
- - Initial public release.
35
- - No migration steps are required for earlier akm versions.
36
- `,
37
- };
38
- function loadChangelog() {
39
- try {
40
- const changelogPath = path.resolve(import.meta.dir, "../CHANGELOG.md");
41
- if (fs.existsSync(changelogPath)) {
42
- return fs.readFileSync(changelogPath, "utf8");
43
- }
44
- }
45
- catch {
46
- // fall through to embedded notes
47
- }
48
- return undefined;
49
- }
50
- function escapeRegexString(value) {
51
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
52
- }
53
- function normalizeRequestedVersion(input) {
54
- const value = input.trim();
55
- if (!value)
56
- return value;
57
- if (value.toLowerCase() === "latest")
58
- return "latest";
59
- const withoutV = value.replace(/^v/i, "");
60
- return withoutV;
61
- }
62
- function versionCandidates(requested) {
63
- if (requested === "latest")
64
- return ["latest"];
65
- const exact = requested;
66
- const stable = requested.replace(/[-+].*$/, "");
67
- return stable === exact ? [exact] : [exact, stable];
68
- }
69
- function resolveLatestVersion(changelog) {
70
- for (const match of changelog.matchAll(/^## \[([^\]]+)\]/gm)) {
71
- const version = match[1];
72
- if (version !== "Unreleased")
73
- return version;
74
- }
75
- return undefined;
76
- }
77
- function extractChangelogSection(changelog, version) {
78
- const pattern = new RegExp(`^## \\[${escapeRegexString(version)}\\][^\\n]*\\n([\\s\\S]*?)(?=^## \\[|\\Z)`, "m");
79
- const match = changelog.match(pattern);
80
- if (!match)
81
- return undefined;
82
- return `## [${version}]\n${match[1].trim()}\n`;
83
- }
84
- function fallbackGuide(version) {
85
- const embedded = EMBEDDED_MIGRATION_GUIDES[version];
86
- if (embedded)
87
- return `${embedded.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
88
- return `No dedicated migration note is bundled for akm v${version}.\n\nSee the full changelog: ${CHANGELOG_URL}\n`;
89
- }
90
- export function renderMigrationHelp(versionInput, changelogText = loadChangelog()) {
91
- const requested = normalizeRequestedVersion(versionInput);
92
- if (!requested) {
93
- return `Version is required.\n\nUsage: akm help migrate <version>\n`;
94
- }
95
- const resolvedLatest = changelogText ? resolveLatestVersion(changelogText) : undefined;
96
- const candidates = requested === "latest" && resolvedLatest ? [resolvedLatest] : versionCandidates(requested);
97
- if (changelogText) {
98
- for (const candidate of candidates) {
99
- const section = extractChangelogSection(changelogText, candidate);
100
- if (section) {
101
- const embedded = EMBEDDED_MIGRATION_GUIDES[candidate];
102
- if (!embedded)
103
- return `${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
104
- return `${embedded.trim()}\n\nRelease notes\n-------------\n${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
105
- }
106
- }
107
- }
108
- const fallbackVersion = candidates.find((candidate) => candidate !== "latest") ?? requested;
109
- return fallbackGuide(fallbackVersion);
110
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * Registry provider factory map.
3
- *
4
- * Maps registry provider type identifiers (e.g. "static-index", "skills-sh")
5
- * to factory functions that create RegistryProvider instances.
6
- *
7
- * "Registry" here refers to the kit discovery registries (npm, GitHub, static
8
- * index files) — not to be confused with the stash provider factory map in
9
- * stash-provider-factory.ts or the installed-kit operations in installed-kits.ts.
10
- */
11
- import { createProviderRegistry } from "./create-provider-registry";
12
- // ── Factory map ─────────────────────────────────────────────────────────────
13
- const registry = createProviderRegistry();
14
- export function registerProvider(type, factory) {
15
- registry.register(type, factory);
16
- }
17
- export function resolveProviderFactory(type) {
18
- return registry.resolve(type);
19
- }
@@ -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
- }
package/dist/ripgrep.js DELETED
@@ -1,2 +0,0 @@
1
- export { ensureRg } from "./ripgrep-install";
2
- export { isRgAvailable, resolveRg } from "./ripgrep-resolve";