akm-cli 0.0.0 → 0.0.17
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/LICENSE +385 -0
- package/README.md +249 -6
- package/dist/asset-spec.js +70 -0
- package/dist/cli.js +934 -0
- package/dist/common.js +192 -0
- package/dist/config-cli.js +233 -0
- package/dist/config.js +338 -0
- package/dist/db.js +371 -0
- package/dist/embedder.js +150 -0
- package/dist/errors.js +28 -0
- package/dist/file-context.js +162 -0
- package/dist/frontmatter.js +86 -0
- package/dist/github.js +17 -0
- package/dist/indexer.js +311 -0
- package/dist/init.js +43 -0
- package/dist/llm.js +87 -0
- package/dist/lockfile.js +60 -0
- package/dist/markdown.js +77 -0
- package/dist/matchers.js +159 -0
- package/dist/metadata.js +408 -0
- package/dist/origin-resolve.js +54 -0
- package/dist/paths.js +92 -0
- package/dist/registry-install.js +459 -0
- package/dist/registry-resolve.js +486 -0
- package/dist/registry-search.js +365 -0
- package/dist/registry-types.js +1 -0
- package/dist/renderers.js +386 -0
- package/dist/ripgrep-install.js +155 -0
- package/dist/ripgrep-resolve.js +78 -0
- package/dist/ripgrep.js +2 -0
- package/dist/self-update.js +226 -0
- package/dist/stash-add.js +71 -0
- package/dist/stash-clone.js +115 -0
- package/dist/stash-ref.js +73 -0
- package/dist/stash-registry.js +206 -0
- package/dist/stash-resolve.js +55 -0
- package/dist/stash-search.js +490 -0
- package/dist/stash-show.js +58 -0
- package/dist/stash-source.js +130 -0
- package/dist/stash-types.js +1 -0
- package/dist/walker.js +163 -0
- package/dist/warn.js +20 -0
- package/package.json +53 -7
- package/index.js +0 -4
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fetchWithRetry } from "./common";
|
|
4
|
+
import { DEFAULT_CONFIG, loadConfig } from "./config";
|
|
5
|
+
import { getRegistryIndexCacheDir } from "./paths";
|
|
6
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
7
|
+
/** Cache TTL in milliseconds (1 hour). */
|
|
8
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
9
|
+
/** Maximum age before cache is considered stale but still usable as fallback (7 days). */
|
|
10
|
+
const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
12
|
+
export async function searchRegistry(query, options) {
|
|
13
|
+
const trimmed = query.trim();
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
return { query: "", hits: [], warnings: [] };
|
|
16
|
+
}
|
|
17
|
+
const limit = clampLimit(options?.limit);
|
|
18
|
+
const entries = (options?.registries ?? resolveRegistries()).filter((r) => r.enabled !== false);
|
|
19
|
+
const warnings = [];
|
|
20
|
+
// Load index from all configured registries, merge kits
|
|
21
|
+
const allKits = [];
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
try {
|
|
24
|
+
const index = await loadIndex(entry);
|
|
25
|
+
if (index) {
|
|
26
|
+
const regName = entry.name;
|
|
27
|
+
for (const kit of index.kits) {
|
|
28
|
+
allKits.push({ kit, registryName: regName });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const label = entry.name ? `${entry.name} (${entry.url})` : entry.url;
|
|
34
|
+
warnings.push(`Registry ${label}: ${toErrorMessage(err)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Score and rank
|
|
38
|
+
const hits = scoreKits(allKits, trimmed, limit);
|
|
39
|
+
// When includeAssets is enabled, also search asset-level metadata
|
|
40
|
+
let assetHits = [];
|
|
41
|
+
if (options?.includeAssets) {
|
|
42
|
+
assetHits = scoreAssets(allKits, trimmed, limit);
|
|
43
|
+
}
|
|
44
|
+
return { query: trimmed, hits, warnings, assetHits: assetHits.length > 0 ? assetHits : undefined };
|
|
45
|
+
}
|
|
46
|
+
// ── Registry resolution ─────────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the list of enabled registries.
|
|
49
|
+
*
|
|
50
|
+
* Priority:
|
|
51
|
+
* 1. AKM_REGISTRY_URL env var (CI override, comma-separated)
|
|
52
|
+
* 2. config.registries (filtered by enabled !== false)
|
|
53
|
+
* 3. Default registries from DEFAULT_CONFIG
|
|
54
|
+
*/
|
|
55
|
+
export function resolveRegistries(configRegistries) {
|
|
56
|
+
// Allow env var override (comma-separated URLs) — CI escape hatch
|
|
57
|
+
const envUrls = process.env.AKM_REGISTRY_URL?.trim();
|
|
58
|
+
if (envUrls) {
|
|
59
|
+
return envUrls
|
|
60
|
+
.split(",")
|
|
61
|
+
.map((u) => u.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((url) => ({ url }));
|
|
64
|
+
}
|
|
65
|
+
const registries = configRegistries ?? loadConfig().registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
66
|
+
return registries.filter((r) => r.enabled !== false);
|
|
67
|
+
}
|
|
68
|
+
// ── Index loading with cache ────────────────────────────────────────────────
|
|
69
|
+
async function loadIndex(entry) {
|
|
70
|
+
const cachePath = indexCachePath(entry.url);
|
|
71
|
+
const cached = readCachedIndex(cachePath);
|
|
72
|
+
// Fresh cache: return immediately
|
|
73
|
+
if (cached && !isCacheExpired(cached.mtime)) {
|
|
74
|
+
return cached.index;
|
|
75
|
+
}
|
|
76
|
+
// Try to fetch fresh index
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetchWithRetry(entry.url, undefined, { timeout: 10_000 });
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`HTTP ${response.status}`);
|
|
81
|
+
}
|
|
82
|
+
const data = (await response.json());
|
|
83
|
+
const index = parseRegistryIndex(data);
|
|
84
|
+
if (index) {
|
|
85
|
+
writeCachedIndex(cachePath, index);
|
|
86
|
+
return index;
|
|
87
|
+
}
|
|
88
|
+
throw new Error("Invalid registry index format");
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// Fetch failed — use stale cache if available
|
|
92
|
+
if (cached && !isCacheStale(cached.mtime)) {
|
|
93
|
+
return cached.index;
|
|
94
|
+
}
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function readCachedIndex(cachePath) {
|
|
99
|
+
try {
|
|
100
|
+
const stat = fs.statSync(cachePath);
|
|
101
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
102
|
+
const index = parseRegistryIndex(raw);
|
|
103
|
+
if (!index)
|
|
104
|
+
return null;
|
|
105
|
+
return { index, mtime: stat.mtimeMs };
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function writeCachedIndex(cachePath, index) {
|
|
112
|
+
try {
|
|
113
|
+
const dir = path.dirname(cachePath);
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
115
|
+
const tmpPath = `${cachePath}.tmp.${process.pid}`;
|
|
116
|
+
fs.writeFileSync(tmpPath, JSON.stringify(index), "utf8");
|
|
117
|
+
fs.renameSync(tmpPath, cachePath);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Best-effort caching — don't fail the search if we can't write
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function indexCachePath(url) {
|
|
124
|
+
const indexDir = getRegistryIndexCacheDir();
|
|
125
|
+
// Deterministic filename from URL
|
|
126
|
+
const slug = url
|
|
127
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
128
|
+
.replace(/^-+|-+$/g, "")
|
|
129
|
+
.slice(0, 120);
|
|
130
|
+
return path.join(indexDir, `${slug}.json`);
|
|
131
|
+
}
|
|
132
|
+
function isCacheExpired(mtimeMs) {
|
|
133
|
+
return Date.now() - mtimeMs > CACHE_TTL_MS;
|
|
134
|
+
}
|
|
135
|
+
function isCacheStale(mtimeMs) {
|
|
136
|
+
return Date.now() - mtimeMs > CACHE_STALE_MS;
|
|
137
|
+
}
|
|
138
|
+
// ── Index parsing ───────────────────────────────────────────────────────────
|
|
139
|
+
function parseRegistryIndex(data) {
|
|
140
|
+
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
141
|
+
return null;
|
|
142
|
+
const obj = data;
|
|
143
|
+
if (typeof obj.version !== "number" || (obj.version !== 1 && obj.version !== 2))
|
|
144
|
+
return null;
|
|
145
|
+
if (typeof obj.updatedAt !== "string")
|
|
146
|
+
return null;
|
|
147
|
+
if (!Array.isArray(obj.kits))
|
|
148
|
+
return null;
|
|
149
|
+
const kits = obj.kits.flatMap((raw) => {
|
|
150
|
+
const kit = parseKitEntry(raw);
|
|
151
|
+
return kit ? [kit] : [];
|
|
152
|
+
});
|
|
153
|
+
return { version: obj.version, updatedAt: obj.updatedAt, kits };
|
|
154
|
+
}
|
|
155
|
+
function parseKitEntry(raw) {
|
|
156
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
157
|
+
return null;
|
|
158
|
+
const obj = raw;
|
|
159
|
+
const id = asString(obj.id);
|
|
160
|
+
const name = asString(obj.name);
|
|
161
|
+
const ref = asString(obj.ref);
|
|
162
|
+
const source = asSource(obj.source);
|
|
163
|
+
if (!id || !name || !ref || !source)
|
|
164
|
+
return null;
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
name,
|
|
168
|
+
ref,
|
|
169
|
+
source,
|
|
170
|
+
description: asString(obj.description),
|
|
171
|
+
homepage: asString(obj.homepage),
|
|
172
|
+
tags: asStringArray(obj.tags),
|
|
173
|
+
assetTypes: asStringArray(obj.assetTypes),
|
|
174
|
+
assets: parseAssets(obj.assets),
|
|
175
|
+
author: asString(obj.author),
|
|
176
|
+
license: asString(obj.license),
|
|
177
|
+
latestVersion: asString(obj.latestVersion),
|
|
178
|
+
curated: obj.curated === true ? true : undefined,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ── Scoring ─────────────────────────────────────────────────────────────────
|
|
182
|
+
function scoreKits(kits, query, limit) {
|
|
183
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
184
|
+
const scored = [];
|
|
185
|
+
for (const { kit, registryName } of kits) {
|
|
186
|
+
const score = scoreKit(kit, tokens);
|
|
187
|
+
if (score > 0) {
|
|
188
|
+
scored.push({ kit, registryName, score });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
scored.sort((a, b) => b.score - a.score);
|
|
192
|
+
return scored.slice(0, limit).map(({ kit, registryName, score }) => toSearchHit(kit, score, registryName));
|
|
193
|
+
}
|
|
194
|
+
function scoreKit(kit, tokens) {
|
|
195
|
+
let score = 0;
|
|
196
|
+
const nameLower = kit.name.toLowerCase();
|
|
197
|
+
const descLower = (kit.description ?? "").toLowerCase();
|
|
198
|
+
const tagsLower = (kit.tags ?? []).map((t) => t.toLowerCase());
|
|
199
|
+
for (const token of tokens) {
|
|
200
|
+
// Exact name match is strongest signal
|
|
201
|
+
if (nameLower === token) {
|
|
202
|
+
score += 1.0;
|
|
203
|
+
}
|
|
204
|
+
else if (nameLower.includes(token)) {
|
|
205
|
+
score += 0.6;
|
|
206
|
+
}
|
|
207
|
+
// Tag matches are high-signal (curated keywords)
|
|
208
|
+
if (tagsLower.some((tag) => tag === token)) {
|
|
209
|
+
score += 0.5;
|
|
210
|
+
}
|
|
211
|
+
else if (tagsLower.some((tag) => tag.includes(token))) {
|
|
212
|
+
score += 0.25;
|
|
213
|
+
}
|
|
214
|
+
// Description substring
|
|
215
|
+
if (descLower.includes(token)) {
|
|
216
|
+
score += 0.2;
|
|
217
|
+
}
|
|
218
|
+
// Author match
|
|
219
|
+
if (kit.author?.toLowerCase().includes(token)) {
|
|
220
|
+
score += 0.15;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Normalize by token count so multi-word queries don't inflate scores
|
|
224
|
+
return tokens.length > 0 ? score / tokens.length : 0;
|
|
225
|
+
}
|
|
226
|
+
function toSearchHit(kit, score, registryName) {
|
|
227
|
+
const metadata = {};
|
|
228
|
+
if (kit.latestVersion)
|
|
229
|
+
metadata.version = kit.latestVersion;
|
|
230
|
+
if (kit.author)
|
|
231
|
+
metadata.author = kit.author;
|
|
232
|
+
if (kit.license)
|
|
233
|
+
metadata.license = kit.license;
|
|
234
|
+
if (kit.assetTypes?.length)
|
|
235
|
+
metadata.assetTypes = kit.assetTypes.join(", ");
|
|
236
|
+
return {
|
|
237
|
+
source: kit.source,
|
|
238
|
+
id: kit.id,
|
|
239
|
+
title: kit.name,
|
|
240
|
+
description: kit.description,
|
|
241
|
+
ref: kit.ref,
|
|
242
|
+
homepage: kit.homepage,
|
|
243
|
+
score: Math.round(score * 1000) / 1000,
|
|
244
|
+
metadata,
|
|
245
|
+
curated: kit.curated,
|
|
246
|
+
registryName,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ── Asset parsing ────────────────────────────────────────────────────────────
|
|
250
|
+
function parseAssets(raw) {
|
|
251
|
+
if (!Array.isArray(raw))
|
|
252
|
+
return undefined;
|
|
253
|
+
const parsed = raw.flatMap((item) => {
|
|
254
|
+
const entry = parseAssetEntry(item);
|
|
255
|
+
return entry ? [entry] : [];
|
|
256
|
+
});
|
|
257
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
258
|
+
}
|
|
259
|
+
function parseAssetEntry(raw) {
|
|
260
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
261
|
+
return null;
|
|
262
|
+
const obj = raw;
|
|
263
|
+
const type = asString(obj.type);
|
|
264
|
+
const name = asString(obj.name);
|
|
265
|
+
if (!type || !name)
|
|
266
|
+
return null;
|
|
267
|
+
return {
|
|
268
|
+
type,
|
|
269
|
+
name,
|
|
270
|
+
description: asString(obj.description),
|
|
271
|
+
tags: asStringArray(obj.tags),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// ── Asset-level scoring ──────────────────────────────────────────────────────
|
|
275
|
+
function scoreAssets(kits, query, limit) {
|
|
276
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
277
|
+
if (tokens.length === 0)
|
|
278
|
+
return [];
|
|
279
|
+
const scored = [];
|
|
280
|
+
for (const { kit, registryName } of kits) {
|
|
281
|
+
if (!kit.assets || kit.assets.length === 0)
|
|
282
|
+
continue;
|
|
283
|
+
const installRef = kit.source === "npm"
|
|
284
|
+
? `npm:${kit.ref}`
|
|
285
|
+
: kit.source === "git"
|
|
286
|
+
? `git+${kit.ref}`
|
|
287
|
+
: kit.source === "local"
|
|
288
|
+
? kit.ref
|
|
289
|
+
: `github:${kit.ref}`;
|
|
290
|
+
for (const asset of kit.assets) {
|
|
291
|
+
const score = scoreAsset(asset, tokens);
|
|
292
|
+
if (score > 0) {
|
|
293
|
+
scored.push({
|
|
294
|
+
hit: {
|
|
295
|
+
type: "registry-asset",
|
|
296
|
+
assetType: asset.type,
|
|
297
|
+
assetName: asset.name,
|
|
298
|
+
description: asset.description,
|
|
299
|
+
kit: { id: kit.id, name: kit.name },
|
|
300
|
+
registryName,
|
|
301
|
+
action: `akm add ${installRef}`,
|
|
302
|
+
score: Math.round(score * 1000) / 1000,
|
|
303
|
+
},
|
|
304
|
+
score,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
scored.sort((a, b) => b.score - a.score);
|
|
310
|
+
return scored.slice(0, limit).map(({ hit }) => hit);
|
|
311
|
+
}
|
|
312
|
+
function scoreAsset(asset, tokens) {
|
|
313
|
+
let score = 0;
|
|
314
|
+
const nameLower = asset.name.toLowerCase();
|
|
315
|
+
const descLower = (asset.description ?? "").toLowerCase();
|
|
316
|
+
const tagsLower = (asset.tags ?? []).map((t) => t.toLowerCase());
|
|
317
|
+
const typeLower = asset.type.toLowerCase();
|
|
318
|
+
for (const token of tokens) {
|
|
319
|
+
if (nameLower === token) {
|
|
320
|
+
score += 1.0;
|
|
321
|
+
}
|
|
322
|
+
else if (nameLower.includes(token)) {
|
|
323
|
+
score += 0.6;
|
|
324
|
+
}
|
|
325
|
+
if (typeLower === token) {
|
|
326
|
+
score += 0.4;
|
|
327
|
+
}
|
|
328
|
+
else if (typeLower.includes(token)) {
|
|
329
|
+
score += 0.2;
|
|
330
|
+
}
|
|
331
|
+
if (tagsLower.some((tag) => tag === token)) {
|
|
332
|
+
score += 0.5;
|
|
333
|
+
}
|
|
334
|
+
else if (tagsLower.some((tag) => tag.includes(token))) {
|
|
335
|
+
score += 0.25;
|
|
336
|
+
}
|
|
337
|
+
if (descLower.includes(token)) {
|
|
338
|
+
score += 0.2;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return tokens.length > 0 ? score / tokens.length : 0;
|
|
342
|
+
}
|
|
343
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
344
|
+
function clampLimit(limit) {
|
|
345
|
+
if (!limit || !Number.isFinite(limit))
|
|
346
|
+
return 20;
|
|
347
|
+
return Math.min(100, Math.max(1, Math.trunc(limit)));
|
|
348
|
+
}
|
|
349
|
+
function asString(value) {
|
|
350
|
+
return typeof value === "string" && value ? value : undefined;
|
|
351
|
+
}
|
|
352
|
+
function asSource(value) {
|
|
353
|
+
if (value === "npm" || value === "github" || value === "git" || value === "local")
|
|
354
|
+
return value;
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
function asStringArray(value) {
|
|
358
|
+
if (!Array.isArray(value))
|
|
359
|
+
return undefined;
|
|
360
|
+
const filtered = value.filter((v) => typeof v === "string");
|
|
361
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
362
|
+
}
|
|
363
|
+
function toErrorMessage(error) {
|
|
364
|
+
return error instanceof Error ? error.message : String(error);
|
|
365
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|