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.
- package/CHANGELOG.md +53 -5
- package/README.md +9 -9
- package/dist/cli.js +379 -1448
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/commands/curate.js +263 -0
- package/dist/{info.js → commands/info.js} +17 -11
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +14 -2
- package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
- package/dist/commands/migration-help.js +141 -0
- package/dist/{registry-search.js → commands/registry-search.js} +68 -9
- package/dist/commands/remember.js +178 -0
- package/dist/{stash-search.js → commands/search.js} +28 -69
- package/dist/{self-update.js → commands/self-update.js} +3 -3
- package/dist/{stash-show.js → commands/show.js} +106 -81
- package/dist/{stash-add.js → commands/source-add.js} +133 -67
- package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
- package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
- package/dist/{vault.js → commands/vault.js} +43 -0
- package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
- package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
- package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
- package/dist/{common.js → core/common.js} +147 -50
- package/dist/{config.js → core/config.js} +288 -29
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
- package/dist/{paths.js → core/paths.js} +4 -4
- package/dist/core/write-source.js +280 -0
- package/dist/{local-search.js → indexer/db-search.js} +49 -32
- package/dist/{db.js → indexer/db.js} +210 -81
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +153 -30
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +4 -7
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +97 -55
- package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
- package/dist/{walker.js → indexer/walker.js} +1 -1
- package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
- package/dist/{llm.js → llm/client.js} +12 -48
- package/dist/llm/embedder.js +127 -0
- package/dist/llm/embedders/cache.js +47 -0
- package/dist/llm/embedders/local.js +152 -0
- package/dist/llm/embedders/remote.js +121 -0
- package/dist/llm/embedders/types.js +39 -0
- package/dist/llm/metadata-enhance.js +53 -0
- package/dist/output/cli-hints.js +301 -0
- package/dist/output/context.js +95 -0
- package/dist/{renderers.js → output/renderers.js} +57 -61
- package/dist/output/shapes.js +212 -0
- package/dist/output/text.js +520 -0
- package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
- package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
- package/dist/registry/factory.js +33 -0
- package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
- package/dist/registry/providers/index.js +11 -0
- package/dist/{providers → registry/providers}/skills-sh.js +60 -4
- package/dist/{providers → registry/providers}/static-index.js +126 -56
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
- package/dist/{detect.js → setup/detect.js} +0 -27
- package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
- package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
- package/dist/{setup.js → setup/setup.js} +162 -129
- package/dist/setup/steps.js +45 -0
- package/dist/{kit-include.js → sources/include.js} +1 -1
- package/dist/sources/provider-factory.js +36 -0
- package/dist/sources/provider.js +21 -0
- package/dist/sources/providers/filesystem.js +35 -0
- package/dist/{stash-providers → sources/providers}/git.js +218 -28
- package/dist/{stash-providers → sources/providers}/index.js +4 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/sources/providers/npm.js +160 -0
- package/dist/sources/providers/provider-utils.js +173 -0
- package/dist/sources/providers/sync-from-ref.js +45 -0
- package/dist/sources/providers/tar-utils.js +154 -0
- package/dist/{stash-providers → sources/providers}/website.js +60 -20
- package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
- package/dist/{wiki.js → wiki/wiki.js} +18 -17
- package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
- package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
- package/dist/{workflow-db.js → workflows/db.js} +1 -1
- package/dist/workflows/document-cache.js +20 -0
- package/dist/workflows/parser.js +379 -0
- package/dist/workflows/renderer.js +78 -0
- package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/README.md +30 -0
- package/docs/migration/release-notes/0.0.13.md +4 -0
- package/docs/migration/release-notes/0.1.0.md +6 -0
- package/docs/migration/release-notes/0.2.0.md +6 -0
- package/docs/migration/release-notes/0.3.0.md +5 -0
- package/docs/migration/release-notes/0.5.0.md +6 -0
- package/docs/migration/release-notes/0.6.0.md +75 -0
- package/docs/migration/release-notes/README.md +21 -0
- package/package.json +3 -2
- package/dist/embedder.js +0 -351
- package/dist/errors.js +0 -34
- package/dist/migration-help.js +0 -110
- package/dist/registry-factory.js +0 -19
- package/dist/registry-install.js +0 -532
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -1
- package/dist/stash-providers/filesystem.js +0 -41
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-providers/provider-utils.js +0 -11
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -251
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{warn.js → core/warn.js} +0 -0
- /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
- /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
- /package/dist/{github.js → integrations/github.js} +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -6,34 +6,38 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { resolveStashDir } from "
|
|
10
|
-
import { loadConfig } from "
|
|
11
|
-
import { NotFoundError, UsageError } from "
|
|
12
|
-
import { akmIndex } from "
|
|
13
|
-
import { removeLockEntry, upsertLockEntry } from "
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
9
|
+
import { resolveStashDir } from "../core/common";
|
|
10
|
+
import { loadConfig } from "../core/config";
|
|
11
|
+
import { NotFoundError, UsageError } from "../core/errors";
|
|
12
|
+
import { akmIndex } from "../indexer/indexer";
|
|
13
|
+
import { removeLockEntry, upsertLockEntry } from "../integrations/lockfile";
|
|
14
|
+
import { parseRegistryRef } from "../registry/resolve";
|
|
15
|
+
import { syncFromRef } from "../sources/providers/sync-from-ref";
|
|
16
|
+
import { ensureWebsiteMirror } from "../sources/providers/website";
|
|
17
|
+
import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
|
|
18
|
+
import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add";
|
|
19
|
+
import { removeStash } from "./source-manage";
|
|
17
20
|
export async function akmListSources(input) {
|
|
18
21
|
const stashDir = input?.stashDir ?? resolveStashDir();
|
|
19
22
|
const config = loadConfig();
|
|
20
23
|
const kindFilter = input?.kind;
|
|
21
24
|
const sources = [];
|
|
22
|
-
// Stash entries
|
|
23
|
-
for
|
|
24
|
-
|
|
25
|
-
const kind =
|
|
25
|
+
// Stash entries — each entry exposes its provider type as kind (spec §2.1).
|
|
26
|
+
// Writable defaults: true for filesystem, false for git/npm/website (CLAUDE.md "Writes").
|
|
27
|
+
for (const stash of config.sources ?? config.stashes ?? []) {
|
|
28
|
+
const kind = stash.type ?? "filesystem";
|
|
26
29
|
if (kindFilter && !kindFilter.includes(kind))
|
|
27
30
|
continue;
|
|
31
|
+
const isFilesystem = kind === "filesystem";
|
|
32
|
+
const writableDefault = isFilesystem;
|
|
28
33
|
const name = stash.name ?? stash.path ?? stash.url ?? "unknown";
|
|
29
34
|
sources.push({
|
|
30
35
|
name,
|
|
31
36
|
kind,
|
|
32
37
|
wiki: stash.wikiName,
|
|
33
38
|
path: stash.path,
|
|
34
|
-
provider:
|
|
35
|
-
|
|
36
|
-
writable: stash.writable === true,
|
|
39
|
+
provider: stash.url != null ? stash.type : undefined,
|
|
40
|
+
writable: stash.writable !== undefined ? stash.writable : writableDefault,
|
|
37
41
|
status: { exists: stash.path ? directoryExists(stash.path) : true },
|
|
38
42
|
});
|
|
39
43
|
}
|
|
@@ -49,7 +53,6 @@ export async function akmListSources(input) {
|
|
|
49
53
|
path: entry.stashRoot,
|
|
50
54
|
ref: entry.ref,
|
|
51
55
|
version: entry.resolvedVersion,
|
|
52
|
-
updatable: true,
|
|
53
56
|
writable: entry.writable === true,
|
|
54
57
|
status: { exists: directoryExists(entry.stashRoot) },
|
|
55
58
|
});
|
|
@@ -64,7 +67,7 @@ export async function akmListSources(input) {
|
|
|
64
67
|
export async function akmRemove(input) {
|
|
65
68
|
const target = input.target.trim();
|
|
66
69
|
if (!target)
|
|
67
|
-
throw new UsageError("Target is required. Provide the source id, ref, path, URL, or name (e.g. `akm remove npm:@scope/
|
|
70
|
+
throw new UsageError("Target is required. Provide the source id, ref, path, URL, or name (e.g. `akm remove npm:@scope/stash` or `akm remove ~/my-stash`).");
|
|
68
71
|
const stashDir = input.stashDir ?? resolveStashDir();
|
|
69
72
|
const config = loadConfig();
|
|
70
73
|
const installed = config.installed ?? [];
|
|
@@ -89,7 +92,7 @@ export async function akmRemove(input) {
|
|
|
89
92
|
stashRoot: entry.stashRoot,
|
|
90
93
|
},
|
|
91
94
|
config: {
|
|
92
|
-
|
|
95
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
93
96
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
94
97
|
},
|
|
95
98
|
index: {
|
|
@@ -103,7 +106,7 @@ export async function akmRemove(input) {
|
|
|
103
106
|
// Fall through to stashes[] (local/remote sources)
|
|
104
107
|
const stashResult = removeStash(target);
|
|
105
108
|
if (!stashResult.removed || !stashResult.entry) {
|
|
106
|
-
throw new NotFoundError(`No matching source for target: ${target}
|
|
109
|
+
throw new NotFoundError(`No matching source for target: ${target}`, "SOURCE_NOT_FOUND");
|
|
107
110
|
}
|
|
108
111
|
const removedEntry = stashResult.entry;
|
|
109
112
|
const index = await akmIndex({ stashDir });
|
|
@@ -120,7 +123,7 @@ export async function akmRemove(input) {
|
|
|
120
123
|
stashRoot: removedEntry.path ?? "",
|
|
121
124
|
},
|
|
122
125
|
config: {
|
|
123
|
-
|
|
126
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
124
127
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
125
128
|
},
|
|
126
129
|
index: {
|
|
@@ -136,28 +139,102 @@ export async function akmUpdate(input) {
|
|
|
136
139
|
const target = input?.target?.trim();
|
|
137
140
|
const all = input?.all === true;
|
|
138
141
|
const force = input?.force === true;
|
|
139
|
-
const
|
|
142
|
+
const config = loadConfig();
|
|
143
|
+
const installedEntries = config.installed ?? [];
|
|
144
|
+
// Check if the target refers to a website source — those are syncable via
|
|
145
|
+
// ensureWebsiteMirror and are stored in sources[] not installed[].
|
|
146
|
+
if (target && !all) {
|
|
147
|
+
const stashes = config.sources ?? config.stashes ?? [];
|
|
148
|
+
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
149
|
+
const resolvedPath = !isUrl ? path.resolve(target) : undefined;
|
|
150
|
+
const websiteMatch = stashes.find((s) => {
|
|
151
|
+
if (s.type !== "website")
|
|
152
|
+
return false;
|
|
153
|
+
if (isUrl && s.url === target)
|
|
154
|
+
return true;
|
|
155
|
+
if (s.name === target)
|
|
156
|
+
return true;
|
|
157
|
+
if (resolvedPath && s.path && path.resolve(s.path) === resolvedPath)
|
|
158
|
+
return true;
|
|
159
|
+
return false;
|
|
160
|
+
});
|
|
161
|
+
if (websiteMatch) {
|
|
162
|
+
// TODO: full incremental re-crawl with delta tracking (#19)
|
|
163
|
+
await ensureWebsiteMirror(websiteMatch, { requireStashDir: true, force: true });
|
|
164
|
+
const index = await akmIndex({ stashDir });
|
|
165
|
+
const updatedConfig = loadConfig();
|
|
166
|
+
return {
|
|
167
|
+
schemaVersion: 1,
|
|
168
|
+
stashDir,
|
|
169
|
+
target,
|
|
170
|
+
all,
|
|
171
|
+
processed: [],
|
|
172
|
+
config: {
|
|
173
|
+
sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
|
|
174
|
+
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
175
|
+
},
|
|
176
|
+
index: {
|
|
177
|
+
mode: index.mode,
|
|
178
|
+
totalEntries: index.totalEntries,
|
|
179
|
+
directoriesScanned: index.directoriesScanned,
|
|
180
|
+
directoriesSkipped: index.directoriesSkipped,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
140
185
|
const selectedEntries = selectTargets(installedEntries, target, all);
|
|
186
|
+
const auditConfig = config;
|
|
141
187
|
const processed = [];
|
|
142
188
|
for (const entry of selectedEntries) {
|
|
143
189
|
if (force && shouldCleanupCache(entry)) {
|
|
144
190
|
cleanupDirectoryBestEffort(entry.cacheDir);
|
|
145
191
|
}
|
|
146
|
-
const
|
|
147
|
-
|
|
192
|
+
const synced = await syncFromRef(entry.ref, { force });
|
|
193
|
+
// Mirror the post-sync audit hook from akmAdd so `akm update` can't
|
|
194
|
+
// silently land malicious content during refresh.
|
|
195
|
+
const registryLabels = deriveRegistryLabels({
|
|
196
|
+
source: synced.source,
|
|
197
|
+
ref: synced.ref,
|
|
198
|
+
artifactUrl: synced.artifactUrl,
|
|
199
|
+
});
|
|
200
|
+
enforceRegistryInstallPolicy(registryLabels, auditConfig, entry.ref);
|
|
201
|
+
const audit = auditInstallCandidate({
|
|
202
|
+
rootDir: synced.extractedDir,
|
|
203
|
+
source: synced.source,
|
|
204
|
+
ref: synced.ref,
|
|
205
|
+
registryLabels,
|
|
206
|
+
config: auditConfig,
|
|
207
|
+
});
|
|
208
|
+
if (audit.blocked) {
|
|
209
|
+
throw new Error(formatInstallAuditFailure(synced.ref, audit));
|
|
210
|
+
}
|
|
211
|
+
const installedEntry = {
|
|
212
|
+
id: synced.id,
|
|
213
|
+
source: synced.source,
|
|
214
|
+
ref: synced.ref,
|
|
215
|
+
artifactUrl: synced.artifactUrl,
|
|
216
|
+
resolvedVersion: synced.resolvedVersion,
|
|
217
|
+
resolvedRevision: synced.resolvedRevision,
|
|
218
|
+
stashRoot: synced.contentDir,
|
|
219
|
+
cacheDir: synced.cacheDir,
|
|
220
|
+
installedAt: synced.syncedAt,
|
|
221
|
+
writable: synced.writable ?? entry.writable,
|
|
222
|
+
...(entry.wikiName ? { wikiName: entry.wikiName } : {}),
|
|
223
|
+
};
|
|
224
|
+
upsertInstalledRegistryEntry(installedEntry);
|
|
148
225
|
await upsertLockEntry({
|
|
149
|
-
id:
|
|
150
|
-
source:
|
|
151
|
-
ref:
|
|
152
|
-
resolvedVersion:
|
|
153
|
-
resolvedRevision:
|
|
154
|
-
integrity:
|
|
226
|
+
id: synced.id,
|
|
227
|
+
source: synced.source,
|
|
228
|
+
ref: synced.ref,
|
|
229
|
+
resolvedVersion: synced.resolvedVersion,
|
|
230
|
+
resolvedRevision: synced.resolvedRevision,
|
|
231
|
+
integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
|
|
155
232
|
});
|
|
156
|
-
if (entry.cacheDir !==
|
|
233
|
+
if (entry.cacheDir !== synced.cacheDir && shouldCleanupCache(entry)) {
|
|
157
234
|
cleanupDirectoryBestEffort(entry.cacheDir);
|
|
158
235
|
}
|
|
159
|
-
const versionChanged = (entry.resolvedVersion ?? "") !== (
|
|
160
|
-
const revisionChanged = (entry.resolvedRevision ?? "") !== (
|
|
236
|
+
const versionChanged = (entry.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
|
|
237
|
+
const revisionChanged = (entry.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
|
|
161
238
|
processed.push({
|
|
162
239
|
id: entry.id,
|
|
163
240
|
source: entry.source,
|
|
@@ -167,7 +244,7 @@ export async function akmUpdate(input) {
|
|
|
167
244
|
resolvedRevision: entry.resolvedRevision,
|
|
168
245
|
cacheDir: entry.cacheDir,
|
|
169
246
|
},
|
|
170
|
-
installed:
|
|
247
|
+
installed: { ...installedEntry, extractedDir: synced.extractedDir, audit },
|
|
171
248
|
changed: {
|
|
172
249
|
version: versionChanged,
|
|
173
250
|
revision: revisionChanged,
|
|
@@ -176,7 +253,7 @@ export async function akmUpdate(input) {
|
|
|
176
253
|
});
|
|
177
254
|
}
|
|
178
255
|
const index = await akmIndex({ stashDir });
|
|
179
|
-
const
|
|
256
|
+
const finalConfig = loadConfig();
|
|
180
257
|
return {
|
|
181
258
|
schemaVersion: 1,
|
|
182
259
|
stashDir,
|
|
@@ -184,8 +261,8 @@ export async function akmUpdate(input) {
|
|
|
184
261
|
all,
|
|
185
262
|
processed,
|
|
186
263
|
config: {
|
|
187
|
-
|
|
188
|
-
installedKitCount:
|
|
264
|
+
sourceCount: (finalConfig.sources ?? finalConfig.stashes ?? []).length,
|
|
265
|
+
installedKitCount: finalConfig.installed?.length ?? 0,
|
|
189
266
|
},
|
|
190
267
|
index: {
|
|
191
268
|
mode: index.mode,
|
|
@@ -197,19 +274,19 @@ export async function akmUpdate(input) {
|
|
|
197
274
|
}
|
|
198
275
|
function selectTargets(installed, target, all) {
|
|
199
276
|
if (all && target) {
|
|
200
|
-
throw new UsageError("Specify either <target> or --all, not both.");
|
|
277
|
+
throw new UsageError("Specify either <target> or --all, not both.", "MISSING_OR_AMBIGUOUS_TARGET");
|
|
201
278
|
}
|
|
202
279
|
if (all)
|
|
203
280
|
return installed;
|
|
204
281
|
if (!target) {
|
|
205
|
-
throw new UsageError("Either <target> or --all is required.");
|
|
282
|
+
throw new UsageError("Either <target> or --all is required.", "MISSING_OR_AMBIGUOUS_TARGET");
|
|
206
283
|
}
|
|
207
284
|
const found = tryResolveInstalledTarget(installed, target);
|
|
208
285
|
if (found)
|
|
209
286
|
return [found];
|
|
210
287
|
// Check if target matches a stash source and give a helpful message
|
|
211
288
|
const config = loadConfig();
|
|
212
|
-
const stashes = config.stashes ?? [];
|
|
289
|
+
const stashes = config.sources ?? config.stashes ?? [];
|
|
213
290
|
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
214
291
|
const resolvedPath = !isUrl ? path.resolve(target) : undefined;
|
|
215
292
|
const stashMatch = stashes.find((s) => {
|
|
@@ -222,10 +299,13 @@ function selectTargets(installed, target, all) {
|
|
|
222
299
|
return false;
|
|
223
300
|
});
|
|
224
301
|
if (stashMatch) {
|
|
225
|
-
if (stashMatch.
|
|
226
|
-
|
|
302
|
+
if (stashMatch.type === "website") {
|
|
303
|
+
// Website sources should be handled before reaching selectTargets.
|
|
304
|
+
// This path should not be reached; surface a clear message if it is.
|
|
305
|
+
throw new UsageError(`"${target}" is a website source — website caching not yet implemented for --all. ` +
|
|
306
|
+
`Run \`akm update ${target}\` to re-mirror this source individually.`, "TARGET_NOT_UPDATABLE");
|
|
227
307
|
}
|
|
228
|
-
throw new UsageError(`"${target}" is a local directory — it reflects your files in place. To refresh the search index, run: akm index
|
|
308
|
+
throw new UsageError(`"${target}" is a local directory — it reflects your files in place. To refresh the search index, run: akm index`, "TARGET_NOT_UPDATABLE");
|
|
229
309
|
}
|
|
230
310
|
throw new NotFoundError(`No matching source for target: ${target}`);
|
|
231
311
|
}
|
|
@@ -250,14 +330,6 @@ function tryResolveInstalledTarget(installed, target) {
|
|
|
250
330
|
}
|
|
251
331
|
return undefined;
|
|
252
332
|
}
|
|
253
|
-
function toInstalledEntry(status) {
|
|
254
|
-
// KitInstallStatus extends InstalledKitEntry; omit transient install-only fields.
|
|
255
|
-
const { extractedDir: _extractedDir, audit: _audit, ...base } = status;
|
|
256
|
-
return base;
|
|
257
|
-
}
|
|
258
|
-
function toInstallStatus(status) {
|
|
259
|
-
return { ...status };
|
|
260
|
-
}
|
|
261
333
|
function cleanupDirectoryBestEffort(target) {
|
|
262
334
|
try {
|
|
263
335
|
fs.rmSync(target, { recursive: true, force: true });
|
|
@@ -0,0 +1,141 @@
|
|
|
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 MIGRATION_DOC_URL = "https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md";
|
|
5
|
+
/**
|
|
6
|
+
* Directory containing per-version release notes. Resolved relative to
|
|
7
|
+
* `import.meta.dir` so the lookup works whether this module is running
|
|
8
|
+
* from source (`<repo>/src`) or from the published build (`<pkg>/dist`).
|
|
9
|
+
* The `docs/migration/release-notes/` directory is shipped via the
|
|
10
|
+
* `files[]` array in `package.json`.
|
|
11
|
+
*/
|
|
12
|
+
function releaseNotesDir() {
|
|
13
|
+
return path.resolve(import.meta.dir, "../../docs/migration/release-notes");
|
|
14
|
+
}
|
|
15
|
+
function loadChangelog() {
|
|
16
|
+
try {
|
|
17
|
+
const changelogPath = path.resolve(import.meta.dir, "../../CHANGELOG.md");
|
|
18
|
+
if (fs.existsSync(changelogPath)) {
|
|
19
|
+
return fs.readFileSync(changelogPath, "utf8");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// fall through to bundled notes
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Load the bundled migration note for a specific version, if one exists.
|
|
29
|
+
* Returns the file body verbatim (no transformations). Missing files,
|
|
30
|
+
* permission errors, and non-file entries all return `undefined` —
|
|
31
|
+
* callers are responsible for the fallback message.
|
|
32
|
+
*/
|
|
33
|
+
function loadReleaseNote(version) {
|
|
34
|
+
if (!isSafeVersionComponent(version))
|
|
35
|
+
return undefined;
|
|
36
|
+
const notePath = path.join(releaseNotesDir(), `${version}.md`);
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(notePath))
|
|
39
|
+
return undefined;
|
|
40
|
+
const stat = fs.statSync(notePath);
|
|
41
|
+
if (!stat.isFile())
|
|
42
|
+
return undefined;
|
|
43
|
+
return fs.readFileSync(notePath, "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Restrict version lookups to strings that are safe as a single path
|
|
51
|
+
* segment. Accepts typical semver forms (`0.6.0`, `0.6.0-rc1`,
|
|
52
|
+
* `0.6.0+build.5`) and rejects anything with slashes, `..`, or control
|
|
53
|
+
* characters that would let a crafted input escape the release-notes
|
|
54
|
+
* directory.
|
|
55
|
+
*/
|
|
56
|
+
function isSafeVersionComponent(version) {
|
|
57
|
+
if (!version || version.length > 64)
|
|
58
|
+
return false;
|
|
59
|
+
return /^[A-Za-z0-9._+-]+$/.test(version) && !version.includes("..");
|
|
60
|
+
}
|
|
61
|
+
function normalizeRequestedVersion(input) {
|
|
62
|
+
const value = input.trim();
|
|
63
|
+
if (!value)
|
|
64
|
+
return value;
|
|
65
|
+
if (value.toLowerCase() === "latest")
|
|
66
|
+
return "latest";
|
|
67
|
+
const withoutV = value.replace(/^v/i, "");
|
|
68
|
+
return withoutV;
|
|
69
|
+
}
|
|
70
|
+
function versionCandidates(requested) {
|
|
71
|
+
if (requested === "latest")
|
|
72
|
+
return ["latest"];
|
|
73
|
+
const exact = requested;
|
|
74
|
+
const stable = requested.replace(/[-+].*$/, "");
|
|
75
|
+
return stable === exact ? [exact] : [exact, stable];
|
|
76
|
+
}
|
|
77
|
+
function resolveLatestVersion(changelog) {
|
|
78
|
+
for (const match of changelog.matchAll(/^## \[([^\]]+)\]/gm)) {
|
|
79
|
+
const version = match[1];
|
|
80
|
+
if (version !== "Unreleased")
|
|
81
|
+
return version;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
function extractChangelogSection(changelog, version) {
|
|
86
|
+
const pattern = new RegExp(`^## \\[${escapeRegexString(version)}\\][^\\n]*\\n([\\s\\S]*?)(?=^## \\[|\\Z)`, "m");
|
|
87
|
+
const match = changelog.match(pattern);
|
|
88
|
+
if (!match)
|
|
89
|
+
return undefined;
|
|
90
|
+
return `## [${version}]\n${match[1].trim()}\n`;
|
|
91
|
+
}
|
|
92
|
+
function escapeRegexString(value) {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
}
|
|
95
|
+
function fallbackGuide(version) {
|
|
96
|
+
const bundled = loadReleaseNote(version);
|
|
97
|
+
if (bundled)
|
|
98
|
+
return `${bundled.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
99
|
+
const available = listBundledReleaseVersions();
|
|
100
|
+
const availableLine = available.length ? `\nAvailable bundled notes: ${available.join(", ")}\n` : "";
|
|
101
|
+
return (`No dedicated migration note is bundled for akm v${version}.\n${availableLine}\n` +
|
|
102
|
+
`See the full changelog: ${CHANGELOG_URL}\n` +
|
|
103
|
+
`Longform migration guide: ${MIGRATION_DOC_URL}\n`);
|
|
104
|
+
}
|
|
105
|
+
export function renderMigrationHelp(versionInput, changelogText = loadChangelog()) {
|
|
106
|
+
const requested = normalizeRequestedVersion(versionInput);
|
|
107
|
+
if (!requested) {
|
|
108
|
+
return `Version is required.\n\nUsage: akm help migrate <version>\n`;
|
|
109
|
+
}
|
|
110
|
+
const resolvedLatest = changelogText ? resolveLatestVersion(changelogText) : undefined;
|
|
111
|
+
const candidates = requested === "latest" && resolvedLatest ? [resolvedLatest] : versionCandidates(requested);
|
|
112
|
+
if (changelogText) {
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
const section = extractChangelogSection(changelogText, candidate);
|
|
115
|
+
if (section) {
|
|
116
|
+
const bundled = loadReleaseNote(candidate);
|
|
117
|
+
if (!bundled)
|
|
118
|
+
return `${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
119
|
+
return `${bundled.trim()}\n\nRelease notes\n-------------\n${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const fallbackVersion = candidates.find((candidate) => candidate !== "latest") ?? requested;
|
|
124
|
+
return fallbackGuide(fallbackVersion);
|
|
125
|
+
}
|
|
126
|
+
/** Test-only helper — list every version with a bundled release note. */
|
|
127
|
+
export function listBundledReleaseVersions() {
|
|
128
|
+
try {
|
|
129
|
+
const dir = releaseNotesDir();
|
|
130
|
+
if (!fs.existsSync(dir))
|
|
131
|
+
return [];
|
|
132
|
+
return fs
|
|
133
|
+
.readdirSync(dir)
|
|
134
|
+
.filter((name) => name.endsWith(".md") && name !== "README.md")
|
|
135
|
+
.map((name) => name.slice(0, -".md".length))
|
|
136
|
+
.sort();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { toErrorMessage } from "
|
|
2
|
-
import { DEFAULT_CONFIG, loadConfig } from "
|
|
3
|
-
import { resolveProviderFactory } from "
|
|
1
|
+
import { toErrorMessage } from "../core/common";
|
|
2
|
+
import { DEFAULT_CONFIG, loadConfig } from "../core/config";
|
|
3
|
+
import { resolveProviderFactory } from "../registry/factory";
|
|
4
4
|
// ── Eagerly import providers to trigger self-registration ───────────────────
|
|
5
|
-
import "
|
|
6
|
-
import "./providers/skills-sh";
|
|
5
|
+
import "../registry/providers/index";
|
|
7
6
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
8
7
|
export async function searchRegistry(query, options) {
|
|
9
8
|
const trimmed = query.trim();
|
|
@@ -25,7 +24,8 @@ export async function searchRegistry(query, options) {
|
|
|
25
24
|
// Merge results grouped by provider
|
|
26
25
|
const allHits = [];
|
|
27
26
|
const allAssetHits = [];
|
|
28
|
-
for (
|
|
27
|
+
for (let i = 0; i < results.length; i++) {
|
|
28
|
+
const result = results[i];
|
|
29
29
|
if (result.status === "rejected") {
|
|
30
30
|
warnings.push(toErrorMessage(result.reason));
|
|
31
31
|
continue;
|
|
@@ -33,9 +33,29 @@ export async function searchRegistry(query, options) {
|
|
|
33
33
|
const value = result.value;
|
|
34
34
|
if (!value)
|
|
35
35
|
continue;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const registryLabel = entries[i].name ? `"${entries[i].name}"` : entries[i].url;
|
|
37
|
+
let dropped = 0;
|
|
38
|
+
for (const hit of value.hits) {
|
|
39
|
+
if (isCompleteHit(hit)) {
|
|
40
|
+
allHits.push(hit);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
dropped++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (value.assetHits) {
|
|
47
|
+
for (const hit of value.assetHits) {
|
|
48
|
+
if (isCompleteAssetHit(hit)) {
|
|
49
|
+
allAssetHits.push(hit);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
dropped++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (dropped > 0) {
|
|
57
|
+
warnings.push(`Registry ${registryLabel} returned ${dropped} incomplete hit(s); dropped from response.`);
|
|
58
|
+
}
|
|
39
59
|
if (value.warnings)
|
|
40
60
|
warnings.push(...value.warnings);
|
|
41
61
|
}
|
|
@@ -97,3 +117,42 @@ function clampLimit(limit) {
|
|
|
97
117
|
return 20;
|
|
98
118
|
return Math.min(100, Math.max(1, Math.trunc(limit)));
|
|
99
119
|
}
|
|
120
|
+
// A complete hit must have the fields downstream consumers (CLI rendering,
|
|
121
|
+
// `akm add`) rely on. Providers that return partial records would otherwise
|
|
122
|
+
// surface as `{}` in the JSON output.
|
|
123
|
+
function isCompleteHit(hit) {
|
|
124
|
+
if (!hit || typeof hit !== "object")
|
|
125
|
+
return false;
|
|
126
|
+
return (typeof hit.source === "string" &&
|
|
127
|
+
typeof hit.id === "string" &&
|
|
128
|
+
hit.id.length > 0 &&
|
|
129
|
+
typeof hit.title === "string" &&
|
|
130
|
+
hit.title.length > 0 &&
|
|
131
|
+
typeof hit.ref === "string" &&
|
|
132
|
+
hit.ref.length > 0 &&
|
|
133
|
+
typeof hit.installRef === "string" &&
|
|
134
|
+
hit.installRef.length > 0);
|
|
135
|
+
}
|
|
136
|
+
function isCompleteAssetHit(hit) {
|
|
137
|
+
if (!hit || typeof hit !== "object")
|
|
138
|
+
return false;
|
|
139
|
+
if (hit.type !== "registry-asset" ||
|
|
140
|
+
typeof hit.assetType !== "string" ||
|
|
141
|
+
hit.assetType.length === 0 ||
|
|
142
|
+
typeof hit.assetName !== "string" ||
|
|
143
|
+
hit.assetName.length === 0 ||
|
|
144
|
+
typeof hit.action !== "string") {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
// `stash` is required by the consumer (output shaping + asset-action display);
|
|
148
|
+
// rejecting incomplete stashes here keeps malformed objects out of the JSON
|
|
149
|
+
// output. Flagged in PR #168 review (#9).
|
|
150
|
+
const stash = hit.stash;
|
|
151
|
+
if (!stash || typeof stash !== "object")
|
|
152
|
+
return false;
|
|
153
|
+
if (typeof stash.id !== "string" || stash.id.length === 0)
|
|
154
|
+
return false;
|
|
155
|
+
if (typeof stash.name !== "string" || stash.name.length === 0)
|
|
156
|
+
return false;
|
|
157
|
+
return true;
|
|
158
|
+
}
|