akm-cli 0.0.22 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/asset-spec.js +1 -1
- package/dist/cli.js +99 -99
- package/dist/config-cli.js +0 -25
- package/dist/config.js +15 -96
- package/dist/github.js +22 -3
- package/dist/indexer.js +2 -2
- package/dist/init.js +1 -1
- package/dist/installed-kits.js +10 -10
- package/dist/kit-include.js +3 -3
- package/dist/local-search.js +1 -1
- package/dist/lockfile.js +42 -3
- package/dist/registry-build-index.js +4 -11
- package/dist/registry-install.js +12 -12
- package/dist/registry-resolve.js +1 -1
- package/dist/registry-search.js +1 -1
- package/dist/{stash-source.js → search-source.js} +4 -8
- package/dist/stash-add.js +26 -7
- package/dist/stash-clone.js +28 -2
- package/dist/stash-provider-factory.js +8 -23
- package/dist/stash-providers/filesystem.js +1 -1
- package/dist/stash-providers/openviking.js +1 -1
- package/dist/stash-search.js +3 -3
- package/dist/stash-show.js +8 -5
- package/dist/stash-source-manage.js +82 -0
- package/package.json +2 -2
package/dist/config.js
CHANGED
|
@@ -4,7 +4,6 @@ import { getConfigDir as _getConfigDir, getConfigPath as _getConfigPath } from "
|
|
|
4
4
|
// ── Defaults ────────────────────────────────────────────────────────────────
|
|
5
5
|
export const DEFAULT_CONFIG = {
|
|
6
6
|
semanticSearch: true,
|
|
7
|
-
searchPaths: [],
|
|
8
7
|
registries: [
|
|
9
8
|
{ url: "https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json", name: "official" },
|
|
10
9
|
{ url: "https://skills.sh", name: "skills.sh", provider: "skills-sh" },
|
|
@@ -23,7 +22,7 @@ export function getConfigPath() {
|
|
|
23
22
|
}
|
|
24
23
|
// ── Load / Save / Update ────────────────────────────────────────────────────
|
|
25
24
|
let cachedConfig;
|
|
26
|
-
export function loadConfig(
|
|
25
|
+
export function loadConfig() {
|
|
27
26
|
const configPath = getConfigPath();
|
|
28
27
|
let stat;
|
|
29
28
|
try {
|
|
@@ -51,94 +50,11 @@ export function loadConfig(opts) {
|
|
|
51
50
|
if (envKey)
|
|
52
51
|
config.llm.apiKey = envKey;
|
|
53
52
|
}
|
|
54
|
-
if (!opts?.readOnly) {
|
|
55
|
-
// Migrate installed[source: "local"] → stashes[type: "filesystem"]
|
|
56
|
-
try {
|
|
57
|
-
migrateLocalInstalledToStashes(config);
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
console.warn("[agentikit] Warning: config migration (local→stashes) failed:", err instanceof Error ? err.message : String(err));
|
|
61
|
-
}
|
|
62
|
-
// Migrate remoteStashSources → stashes[]
|
|
63
|
-
try {
|
|
64
|
-
migrateRemoteStashSourcesToStashes(config);
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
console.warn("[agentikit] Warning: config migration (remoteStashSources→stashes) failed:", err instanceof Error ? err.message : String(err));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
53
|
// Cache the parsed config with its path and mtime for subsequent calls.
|
|
71
54
|
// Reuse the stat already obtained above (avoids a second syscall + TOCTOU gap).
|
|
72
55
|
cachedConfig = { config, path: configPath, mtime: stat.mtimeMs };
|
|
73
56
|
return config;
|
|
74
57
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Migrate installed entries with source "local" to stashes[] as filesystem entries.
|
|
77
|
-
* Local directories are search paths, not registry kits — they don't need version
|
|
78
|
-
* tracking, cache management, or update support.
|
|
79
|
-
*
|
|
80
|
-
* Mutates the config in place and persists to disk if any entries are migrated.
|
|
81
|
-
*/
|
|
82
|
-
function migrateLocalInstalledToStashes(config) {
|
|
83
|
-
const installed = config.installed;
|
|
84
|
-
if (!installed)
|
|
85
|
-
return;
|
|
86
|
-
const localEntries = installed.filter((e) => e.source === "local");
|
|
87
|
-
if (localEntries.length === 0)
|
|
88
|
-
return;
|
|
89
|
-
const stashes = [...(config.stashes ?? [])];
|
|
90
|
-
const existingPaths = new Set(stashes.filter((s) => !!s.path).map((s) => path.resolve(s.path)));
|
|
91
|
-
let migrated = 0;
|
|
92
|
-
for (const entry of localEntries) {
|
|
93
|
-
const resolved = path.resolve(entry.stashRoot);
|
|
94
|
-
if (existingPaths.has(resolved))
|
|
95
|
-
continue;
|
|
96
|
-
stashes.push({
|
|
97
|
-
type: "filesystem",
|
|
98
|
-
path: resolved,
|
|
99
|
-
name: entry.id,
|
|
100
|
-
});
|
|
101
|
-
existingPaths.add(resolved);
|
|
102
|
-
migrated++;
|
|
103
|
-
}
|
|
104
|
-
if (migrated === 0)
|
|
105
|
-
return;
|
|
106
|
-
// Remove local entries from installed, add to stashes
|
|
107
|
-
config.installed = installed.filter((e) => e.source !== "local");
|
|
108
|
-
config.stashes = stashes;
|
|
109
|
-
saveConfig(config);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Migrate remoteStashSources[] to stashes[] entries.
|
|
113
|
-
* Each remote source becomes a typed stash entry (e.g. type: "openviking").
|
|
114
|
-
*
|
|
115
|
-
* Mutates the config in place and persists to disk if any entries are migrated.
|
|
116
|
-
*/
|
|
117
|
-
function migrateRemoteStashSourcesToStashes(config) {
|
|
118
|
-
const remoteSources = config.remoteStashSources;
|
|
119
|
-
if (!remoteSources || remoteSources.length === 0)
|
|
120
|
-
return;
|
|
121
|
-
const stashes = [...(config.stashes ?? [])];
|
|
122
|
-
const existingUrls = new Set(stashes.filter((s) => !!s.url).map((s) => s.url));
|
|
123
|
-
let migrated = 0;
|
|
124
|
-
for (const entry of remoteSources) {
|
|
125
|
-
if (!entry.url || existingUrls.has(entry.url))
|
|
126
|
-
continue;
|
|
127
|
-
stashes.push({
|
|
128
|
-
type: entry.type ?? "openviking",
|
|
129
|
-
url: entry.url,
|
|
130
|
-
name: entry.name,
|
|
131
|
-
options: entry.options,
|
|
132
|
-
});
|
|
133
|
-
existingUrls.add(entry.url);
|
|
134
|
-
migrated++;
|
|
135
|
-
}
|
|
136
|
-
if (migrated === 0)
|
|
137
|
-
return;
|
|
138
|
-
config.stashes = stashes;
|
|
139
|
-
config.remoteStashSources = undefined;
|
|
140
|
-
saveConfig(config);
|
|
141
|
-
}
|
|
142
58
|
export function saveConfig(config) {
|
|
143
59
|
cachedConfig = undefined;
|
|
144
60
|
const configPath = getConfigPath();
|
|
@@ -175,11 +91,7 @@ function sanitizeConfigForWrite(config) {
|
|
|
175
91
|
const { apiKey, ...rest } = config.llm;
|
|
176
92
|
sanitized.llm = rest;
|
|
177
93
|
}
|
|
178
|
-
// Drop empty
|
|
179
|
-
if (!config.searchPaths?.length)
|
|
180
|
-
delete sanitized.searchPaths;
|
|
181
|
-
if (!config.remoteStashSources?.length)
|
|
182
|
-
delete sanitized.remoteStashSources;
|
|
94
|
+
// Drop empty keys to keep config clean
|
|
183
95
|
return sanitized;
|
|
184
96
|
}
|
|
185
97
|
export function updateConfig(partial) {
|
|
@@ -210,8 +122,18 @@ function pickKnownKeys(raw) {
|
|
|
210
122
|
if (typeof raw.semanticSearch === "boolean") {
|
|
211
123
|
config.semanticSearch = raw.semanticSearch;
|
|
212
124
|
}
|
|
125
|
+
// Migrate legacy searchPaths into stashes
|
|
213
126
|
if (Array.isArray(raw.searchPaths)) {
|
|
214
|
-
|
|
127
|
+
const legacyPaths = raw.searchPaths.filter((d) => typeof d === "string");
|
|
128
|
+
if (legacyPaths.length > 0) {
|
|
129
|
+
const existing = config.stashes ?? [];
|
|
130
|
+
const migrated = legacyPaths
|
|
131
|
+
.filter((p) => !existing.some((s) => s.type === "filesystem" && s.path === p))
|
|
132
|
+
.map((p) => ({ type: "filesystem", path: p }));
|
|
133
|
+
if (migrated.length > 0) {
|
|
134
|
+
config.stashes = [...existing, ...migrated];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
215
137
|
}
|
|
216
138
|
const embedding = parseEmbeddingConfig(raw.embedding);
|
|
217
139
|
if (embedding)
|
|
@@ -225,9 +147,6 @@ function pickKnownKeys(raw) {
|
|
|
225
147
|
const registries = parseRegistriesConfig(raw.registries);
|
|
226
148
|
if (registries)
|
|
227
149
|
config.registries = registries;
|
|
228
|
-
const remoteStash = parseStashesConfig(raw.remoteStashSources);
|
|
229
|
-
if (remoteStash)
|
|
230
|
-
config.remoteStashSources = remoteStash;
|
|
231
150
|
const stashes = parseStashesConfig(raw.stashes);
|
|
232
151
|
if (stashes)
|
|
233
152
|
config.stashes = stashes;
|
|
@@ -358,7 +277,7 @@ function parseEmbeddingConfig(value) {
|
|
|
358
277
|
if (typeof obj.endpoint !== "string" || !obj.endpoint)
|
|
359
278
|
return undefined;
|
|
360
279
|
if (!obj.endpoint.startsWith("http://") && !obj.endpoint.startsWith("https://")) {
|
|
361
|
-
console.warn(`[
|
|
280
|
+
console.warn(`[akm] Ignoring embedding config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
|
|
362
281
|
return undefined;
|
|
363
282
|
}
|
|
364
283
|
if (typeof obj.model !== "string" || !obj.model)
|
|
@@ -391,7 +310,7 @@ function parseLlmConfig(value) {
|
|
|
391
310
|
if (typeof obj.endpoint !== "string" || !obj.endpoint)
|
|
392
311
|
return undefined;
|
|
393
312
|
if (!obj.endpoint.startsWith("http://") && !obj.endpoint.startsWith("https://")) {
|
|
394
|
-
console.warn(`[
|
|
313
|
+
console.warn(`[akm] Ignoring llm config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
|
|
395
314
|
return undefined;
|
|
396
315
|
}
|
|
397
316
|
if (typeof obj.model !== "string" || !obj.model)
|
package/dist/github.js
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
export const GITHUB_API_BASE = "https://api.github.com";
|
|
2
|
-
|
|
2
|
+
const GITHUB_TOKEN_DOMAINS = new Set(["api.github.com", "github.com", "uploads.github.com"]);
|
|
3
|
+
/**
|
|
4
|
+
* Build headers for GitHub API requests.
|
|
5
|
+
* When a `url` is provided, the Authorization header is only included if the
|
|
6
|
+
* URL points to a known GitHub domain, preventing token leakage on redirects
|
|
7
|
+
* to third-party hosts.
|
|
8
|
+
*/
|
|
9
|
+
export function githubHeaders(url) {
|
|
3
10
|
const token = process.env.GITHUB_TOKEN?.trim();
|
|
4
11
|
const headers = {
|
|
5
12
|
Accept: "application/vnd.github+json",
|
|
6
13
|
"User-Agent": "akm-registry",
|
|
7
14
|
};
|
|
8
|
-
if (token)
|
|
9
|
-
|
|
15
|
+
if (token) {
|
|
16
|
+
let includeToken = true;
|
|
17
|
+
if (url) {
|
|
18
|
+
try {
|
|
19
|
+
const hostname = new URL(url).hostname;
|
|
20
|
+
includeToken = GITHUB_TOKEN_DOMAINS.has(hostname);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
includeToken = false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (includeToken)
|
|
27
|
+
headers.Authorization = `Bearer ${token}`;
|
|
28
|
+
}
|
|
10
29
|
return headers;
|
|
11
30
|
}
|
|
12
31
|
export function asRecord(value) {
|
package/dist/indexer.js
CHANGED
|
@@ -7,12 +7,12 @@ import { getDbPath } from "./paths";
|
|
|
7
7
|
import { walkStashFlat } from "./walker";
|
|
8
8
|
import { warn } from "./warn";
|
|
9
9
|
// ── Indexer ──────────────────────────────────────────────────────────────────
|
|
10
|
-
export async function
|
|
10
|
+
export async function akmIndex(options) {
|
|
11
11
|
const stashDir = options?.stashDir || resolveStashDir();
|
|
12
12
|
// Load config and resolve all stash sources
|
|
13
13
|
const { loadConfig } = await import("./config.js");
|
|
14
14
|
const config = loadConfig();
|
|
15
|
-
const { resolveAllStashDirs } = await import("./
|
|
15
|
+
const { resolveAllStashDirs } = await import("./search-source.js");
|
|
16
16
|
const allStashDirs = resolveAllStashDirs(stashDir);
|
|
17
17
|
const t0 = Date.now();
|
|
18
18
|
// Open database — pass embedding dimension from config if available
|
package/dist/init.js
CHANGED
|
@@ -10,7 +10,7 @@ import { TYPE_DIRS } from "./asset-spec";
|
|
|
10
10
|
import { getConfigPath, loadConfig, saveConfig } from "./config";
|
|
11
11
|
import { getBinDir, getDefaultStashDir } from "./paths";
|
|
12
12
|
import { ensureRg } from "./ripgrep-install";
|
|
13
|
-
export async function
|
|
13
|
+
export async function akmInit(options) {
|
|
14
14
|
const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
|
|
15
15
|
let created = false;
|
|
16
16
|
if (!fs.existsSync(stashDir)) {
|
package/dist/installed-kits.js
CHANGED
|
@@ -13,11 +13,11 @@ import fs from "node:fs";
|
|
|
13
13
|
import { resolveStashDir } from "./common";
|
|
14
14
|
import { loadConfig } from "./config";
|
|
15
15
|
import { NotFoundError, UsageError } from "./errors";
|
|
16
|
-
import {
|
|
16
|
+
import { akmIndex } from "./indexer";
|
|
17
17
|
import { removeLockEntry, upsertLockEntry } from "./lockfile";
|
|
18
18
|
import { installRegistryRef, removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./registry-install";
|
|
19
19
|
import { parseRegistryRef } from "./registry-resolve";
|
|
20
|
-
export async function
|
|
20
|
+
export async function akmList(input) {
|
|
21
21
|
const stashDir = input?.stashDir ?? resolveStashDir();
|
|
22
22
|
const config = loadConfig();
|
|
23
23
|
const installed = config.installed ?? [];
|
|
@@ -34,22 +34,22 @@ export async function agentikitList(input) {
|
|
|
34
34
|
totalInstalled: installed.length,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
export async function
|
|
37
|
+
export async function akmRemove(input) {
|
|
38
38
|
const target = input.target.trim();
|
|
39
39
|
if (!target)
|
|
40
|
-
throw new UsageError("Target is required.");
|
|
40
|
+
throw new UsageError("Target is required. Provide the kit id or ref (e.g. `akm remove npm:@scope/kit` or `akm remove owner/repo`).");
|
|
41
41
|
const stashDir = input.stashDir ?? resolveStashDir();
|
|
42
42
|
const config = loadConfig();
|
|
43
43
|
const installed = config.installed ?? [];
|
|
44
44
|
const entry = resolveInstalledTarget(installed, target);
|
|
45
45
|
const updatedConfig = removeInstalledRegistryEntry(entry.id);
|
|
46
|
-
removeLockEntry(entry.id);
|
|
46
|
+
await removeLockEntry(entry.id);
|
|
47
47
|
// Only clean up cache for non-local sources — local sources point to the
|
|
48
48
|
// user's real directory on disk and must never be deleted.
|
|
49
49
|
if (entry.source !== "local") {
|
|
50
50
|
cleanupDirectoryBestEffort(entry.cacheDir);
|
|
51
51
|
}
|
|
52
|
-
const index = await
|
|
52
|
+
const index = await akmIndex({ stashDir });
|
|
53
53
|
return {
|
|
54
54
|
schemaVersion: 1,
|
|
55
55
|
stashDir,
|
|
@@ -62,7 +62,7 @@ export async function agentikitRemove(input) {
|
|
|
62
62
|
stashRoot: entry.stashRoot,
|
|
63
63
|
},
|
|
64
64
|
config: {
|
|
65
|
-
|
|
65
|
+
stashCount: updatedConfig.stashes?.length ?? 0,
|
|
66
66
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
67
67
|
},
|
|
68
68
|
index: {
|
|
@@ -73,7 +73,7 @@ export async function agentikitRemove(input) {
|
|
|
73
73
|
},
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
-
export async function
|
|
76
|
+
export async function akmUpdate(input) {
|
|
77
77
|
const stashDir = input?.stashDir ?? resolveStashDir();
|
|
78
78
|
const target = input?.target?.trim();
|
|
79
79
|
const all = input?.all === true;
|
|
@@ -117,7 +117,7 @@ export async function agentikitUpdate(input) {
|
|
|
117
117
|
},
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
|
-
const index = await
|
|
120
|
+
const index = await akmIndex({ stashDir });
|
|
121
121
|
const config = loadConfig();
|
|
122
122
|
return {
|
|
123
123
|
schemaVersion: 1,
|
|
@@ -126,7 +126,7 @@ export async function agentikitUpdate(input) {
|
|
|
126
126
|
all,
|
|
127
127
|
processed,
|
|
128
128
|
config: {
|
|
129
|
-
|
|
129
|
+
stashCount: config.stashes?.length ?? 0,
|
|
130
130
|
installedKitCount: config.installed?.length ?? 0,
|
|
131
131
|
},
|
|
132
132
|
index: {
|
package/dist/kit-include.js
CHANGED
|
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isWithin } from "./common";
|
|
4
4
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
5
|
-
/**
|
|
6
|
-
const INCLUDE_CONFIG_KEYS = ["akm"
|
|
5
|
+
/** Key to check in package.json for akm include configuration. */
|
|
6
|
+
const INCLUDE_CONFIG_KEYS = ["akm"];
|
|
7
7
|
function readPackageJsonAt(dirPath) {
|
|
8
8
|
try {
|
|
9
9
|
const raw = fs.readFileSync(path.join(dirPath, "package.json"), "utf8");
|
|
@@ -39,7 +39,7 @@ function extractIncludeList(pkg) {
|
|
|
39
39
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
40
40
|
/**
|
|
41
41
|
* Walk up the directory tree from `startDir` to `boundary` (inclusive) looking
|
|
42
|
-
* for a package.json that declares an `akm.include`
|
|
42
|
+
* for a package.json that declares an `akm.include` list.
|
|
43
43
|
* Returns the first config found, or `undefined` if none is found within the
|
|
44
44
|
* boundary.
|
|
45
45
|
*/
|
package/dist/local-search.js
CHANGED
|
@@ -15,8 +15,8 @@ import { getRenderer } from "./file-context";
|
|
|
15
15
|
import { buildSearchText } from "./indexer";
|
|
16
16
|
import { generateMetadataFlat, loadStashFile } from "./metadata";
|
|
17
17
|
import { getDbPath } from "./paths";
|
|
18
|
+
import { buildEditHint, findSourceForPath, isEditable } from "./search-source";
|
|
18
19
|
import { makeAssetRef } from "./stash-ref";
|
|
19
|
-
import { buildEditHint, findSourceForPath, isEditable } from "./stash-source";
|
|
20
20
|
import { walkStashFlat } from "./walker";
|
|
21
21
|
import { warn } from "./warn";
|
|
22
22
|
// ── Type renderer/action maps (re-exported so stash-search.ts can register) ──
|
package/dist/lockfile.js
CHANGED
|
@@ -23,6 +23,10 @@ async function acquireLockSentinel() {
|
|
|
23
23
|
catch (err) {
|
|
24
24
|
if (err.code !== "EEXIST")
|
|
25
25
|
throw err;
|
|
26
|
+
// Check for stale lock — if the owning PID is no longer running, reclaim it
|
|
27
|
+
if (tryReclaimStaleSentinel(sentinelPath)) {
|
|
28
|
+
continue; // Sentinel removed — retry immediately
|
|
29
|
+
}
|
|
26
30
|
// Another process holds the lock — wait briefly before retrying
|
|
27
31
|
if (attempt < LOCK_MAX_RETRIES - 1) {
|
|
28
32
|
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
|
|
@@ -32,6 +36,34 @@ async function acquireLockSentinel() {
|
|
|
32
36
|
// Best-effort: proceed without the lock rather than failing the install
|
|
33
37
|
return false;
|
|
34
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if the sentinel was left by a dead process and remove it if so.
|
|
41
|
+
* Returns true if the sentinel was reclaimed (removed).
|
|
42
|
+
*/
|
|
43
|
+
function tryReclaimStaleSentinel(sentinelPath) {
|
|
44
|
+
try {
|
|
45
|
+
const content = fs.readFileSync(sentinelPath, "utf8").trim();
|
|
46
|
+
const pid = parseInt(content, 10);
|
|
47
|
+
if (Number.isNaN(pid) || pid <= 0) {
|
|
48
|
+
// Invalid PID in sentinel — reclaim it
|
|
49
|
+
fs.unlinkSync(sentinelPath);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
// Check if the process is still alive (signal 0 doesn't kill, just checks)
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return false; // Process is alive — lock is valid
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Process is dead — reclaim the stale lock
|
|
59
|
+
fs.unlinkSync(sentinelPath);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false; // Can't read or remove — leave it alone
|
|
65
|
+
}
|
|
66
|
+
}
|
|
35
67
|
function releaseLockSentinel() {
|
|
36
68
|
try {
|
|
37
69
|
fs.unlinkSync(getLockSentinelPath());
|
|
@@ -84,9 +116,16 @@ export async function upsertLockEntry(entry) {
|
|
|
84
116
|
releaseLockSentinel();
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
|
-
export function removeLockEntry(id) {
|
|
88
|
-
const
|
|
89
|
-
|
|
119
|
+
export async function removeLockEntry(id) {
|
|
120
|
+
const acquired = await acquireLockSentinel();
|
|
121
|
+
try {
|
|
122
|
+
const entries = readLockfile();
|
|
123
|
+
writeLockfile(entries.filter((e) => e.id !== id));
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
if (acquired)
|
|
127
|
+
releaseLockSentinel();
|
|
128
|
+
}
|
|
90
129
|
}
|
|
91
130
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
92
131
|
function isValidLockfileEntry(value) {
|
|
@@ -11,17 +11,10 @@ import { walkStashFlat } from "./walker";
|
|
|
11
11
|
const DEFAULT_NPM_REGISTRY_BASE = "https://registry.npmjs.org";
|
|
12
12
|
const DEFAULT_MANUAL_ENTRIES_PATH = path.resolve("manual-entries.json");
|
|
13
13
|
const DEFAULT_OUTPUT_PATH = path.resolve("index.json");
|
|
14
|
-
const REQUIRED_KEYWORDS = ["
|
|
15
|
-
const GITHUB_TOPICS = ["
|
|
16
|
-
const EXCLUDED_REPOS = new Set(["itlackey/agentikit
|
|
17
|
-
const EXCLUDED_NPM_PACKAGES = new Set([
|
|
18
|
-
"agentikit",
|
|
19
|
-
"agentikit-claude",
|
|
20
|
-
"agentikit-opencode",
|
|
21
|
-
"agentikit-plugins",
|
|
22
|
-
"akm-cli",
|
|
23
|
-
"akm-opencode",
|
|
24
|
-
]);
|
|
14
|
+
const REQUIRED_KEYWORDS = ["akm-kit"];
|
|
15
|
+
const GITHUB_TOPICS = ["akm-kit"];
|
|
16
|
+
const EXCLUDED_REPOS = new Set(["itlackey/agentikit"]);
|
|
17
|
+
const EXCLUDED_NPM_PACKAGES = new Set(["akm-cli"]);
|
|
25
18
|
const EMPTY_INSPECTION = {};
|
|
26
19
|
export async function buildRegistryIndex(options) {
|
|
27
20
|
const manualEntriesPath = path.resolve(options?.manualEntriesPath ?? DEFAULT_MANUAL_ENTRIES_PATH);
|
package/dist/registry-install.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function installRegistryRef(ref, options) {
|
|
|
60
60
|
integrity = await computeFileHash(archivePath);
|
|
61
61
|
extractTarGzSecure(archivePath, extractedDir);
|
|
62
62
|
provisionalKitRoot = detectStashRoot(extractedDir);
|
|
63
|
-
installRoot =
|
|
63
|
+
installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
64
64
|
stashRoot = detectStashRoot(installRoot);
|
|
65
65
|
}
|
|
66
66
|
catch (err) {
|
|
@@ -118,7 +118,7 @@ async function installGitRegistryRef(parsed, options) {
|
|
|
118
118
|
if (isDirectory(extractedDir)) {
|
|
119
119
|
try {
|
|
120
120
|
const provisionalKitRoot = detectStashRoot(extractedDir);
|
|
121
|
-
const installRoot =
|
|
121
|
+
const installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
122
122
|
const stashRoot = detectStashRoot(installRoot);
|
|
123
123
|
if (stashRoot) {
|
|
124
124
|
return {
|
|
@@ -164,7 +164,7 @@ async function installGitRegistryRef(parsed, options) {
|
|
|
164
164
|
// Clean up the clone dir
|
|
165
165
|
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
166
166
|
provisionalKitRoot = detectStashRoot(extractedDir);
|
|
167
|
-
installRoot =
|
|
167
|
+
installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
168
168
|
stashRoot = detectStashRoot(installRoot);
|
|
169
169
|
}
|
|
170
170
|
catch (err) {
|
|
@@ -226,10 +226,6 @@ export function detectStashRoot(extractedDir) {
|
|
|
226
226
|
if (hasStashDirs(root)) {
|
|
227
227
|
return root;
|
|
228
228
|
}
|
|
229
|
-
const opencodeDir = path.join(root, "opencode");
|
|
230
|
-
if (hasStashDirs(opencodeDir)) {
|
|
231
|
-
return opencodeDir;
|
|
232
|
-
}
|
|
233
229
|
const shallowest = findShallowestStashRoot(root);
|
|
234
230
|
if (shallowest)
|
|
235
231
|
return shallowest;
|
|
@@ -242,7 +238,7 @@ function buildInstallCacheDir(cacheRootDir, source, id, version) {
|
|
|
242
238
|
: (version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
|
|
243
239
|
return path.join(cacheRootDir, slug || source, versionSlug);
|
|
244
240
|
}
|
|
245
|
-
function
|
|
241
|
+
function applyAkmIncludeConfig(sourceRoot, cacheDir, searchRoot = sourceRoot) {
|
|
246
242
|
const includeConfig = findNearestIncludeConfig(sourceRoot, searchRoot);
|
|
247
243
|
if (!includeConfig)
|
|
248
244
|
return undefined;
|
|
@@ -400,13 +396,15 @@ function countStashDirs(dirPath) {
|
|
|
400
396
|
*
|
|
401
397
|
* Skips `root` itself since the caller already checked it via `hasStashDirs`.
|
|
402
398
|
*/
|
|
399
|
+
const BFS_MAX_DEPTH = 5;
|
|
403
400
|
function findShallowestStashRoot(root) {
|
|
404
|
-
const queue = [root];
|
|
401
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
405
402
|
while (queue.length > 0) {
|
|
406
|
-
const
|
|
407
|
-
if (!
|
|
403
|
+
const item = queue.shift();
|
|
404
|
+
if (!item) {
|
|
408
405
|
continue;
|
|
409
406
|
}
|
|
407
|
+
const { dir: current, depth } = item;
|
|
410
408
|
if (current !== root) {
|
|
411
409
|
// .stash directory is a strong stash marker
|
|
412
410
|
if (isDirectory(path.join(current, ".stash"))) {
|
|
@@ -418,6 +416,8 @@ function findShallowestStashRoot(root) {
|
|
|
418
416
|
return current;
|
|
419
417
|
}
|
|
420
418
|
}
|
|
419
|
+
if (depth >= BFS_MAX_DEPTH)
|
|
420
|
+
continue;
|
|
421
421
|
let children;
|
|
422
422
|
try {
|
|
423
423
|
children = fs.readdirSync(current, { withFileTypes: true });
|
|
@@ -430,7 +430,7 @@ function findShallowestStashRoot(root) {
|
|
|
430
430
|
continue;
|
|
431
431
|
if (child.name === ".git" || child.name === "node_modules")
|
|
432
432
|
continue;
|
|
433
|
-
queue.push(path.join(current, child.name));
|
|
433
|
+
queue.push({ dir: path.join(current, child.name), depth: depth + 1 });
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
436
|
return undefined;
|
package/dist/registry-resolve.js
CHANGED
|
@@ -422,7 +422,7 @@ function fileUriToPath(ref) {
|
|
|
422
422
|
}
|
|
423
423
|
/**
|
|
424
424
|
* Build a human-readable local ID from an absolute path.
|
|
425
|
-
* /home/user
|
|
425
|
+
* /home/user/akm/skills → ~/akm/skills
|
|
426
426
|
* /tmp/my-kit → /tmp/my-kit
|
|
427
427
|
*/
|
|
428
428
|
function toReadableLocalId(absolutePath) {
|
package/dist/registry-search.js
CHANGED
|
@@ -70,7 +70,7 @@ export function resolveRegistries(configRegistries) {
|
|
|
70
70
|
if (!url)
|
|
71
71
|
continue;
|
|
72
72
|
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
73
|
-
console.warn(`[
|
|
73
|
+
console.warn(`[akm] Ignoring AKM_REGISTRY_URL entry: must start with http:// or https://, got "${url}"`);
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
76
|
entries.push({ url });
|
|
@@ -5,13 +5,13 @@ import { loadConfig } from "./config";
|
|
|
5
5
|
import { warn } from "./warn";
|
|
6
6
|
// ── Resolution ──────────────────────────────────────────────────────────────
|
|
7
7
|
/**
|
|
8
|
-
* Build the ordered list of stash sources
|
|
8
|
+
* Build the ordered list of stash sources:
|
|
9
9
|
* 1. Primary stash dir (user's own, destination for clone)
|
|
10
|
-
* 2. Additional
|
|
10
|
+
* 2. Additional stashes (filesystem and remote providers)
|
|
11
11
|
* 3. Installed kit paths (cache-managed, from registry)
|
|
12
12
|
*
|
|
13
13
|
* The first entry is always the primary stash. Additional entries come
|
|
14
|
-
* from `
|
|
14
|
+
* from `stashes` config and `installed` kit entries.
|
|
15
15
|
*/
|
|
16
16
|
export function resolveStashSources(overrideStashDir, existingConfig) {
|
|
17
17
|
const stashDir = overrideStashDir ?? resolveStashDir();
|
|
@@ -30,10 +30,6 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
|
|
|
30
30
|
sources.push({ path: resolved, ...(registryId ? { registryId } : {}) });
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
-
// Legacy: searchPaths[]
|
|
34
|
-
for (const dir of config.searchPaths) {
|
|
35
|
-
addSource(dir);
|
|
36
|
-
}
|
|
37
33
|
// Filesystem entries from stashes[]
|
|
38
34
|
for (const entry of config.stashes ?? []) {
|
|
39
35
|
if (entry.type === "filesystem" && entry.path && entry.enabled !== false) {
|
|
@@ -78,7 +74,7 @@ export function getPrimarySource(sources) {
|
|
|
78
74
|
* managed by the package manager (`installed[].cacheDir`). These
|
|
79
75
|
* will be overwritten by `akm update` without warning.
|
|
80
76
|
*
|
|
81
|
-
* Everything else — working stash,
|
|
77
|
+
* Everything else — working stash, additional stashes, local project dirs — is
|
|
82
78
|
* the user's domain to manage.
|
|
83
79
|
*/
|
|
84
80
|
export function isEditable(filePath, config) {
|
package/dist/stash-add.js
CHANGED
|
@@ -3,14 +3,33 @@ import path from "node:path";
|
|
|
3
3
|
import { resolveStashDir } from "./common";
|
|
4
4
|
import { loadConfig, saveConfig } from "./config";
|
|
5
5
|
import { UsageError } from "./errors";
|
|
6
|
-
import {
|
|
6
|
+
import { akmIndex } from "./indexer";
|
|
7
7
|
import { upsertLockEntry } from "./lockfile";
|
|
8
8
|
import { detectStashRoot, installRegistryRef, upsertInstalledRegistryEntry } from "./registry-install";
|
|
9
9
|
import { parseRegistryRef } from "./registry-resolve";
|
|
10
|
-
export async function
|
|
10
|
+
export async function akmKitAdd(input) {
|
|
11
11
|
const ref = input.ref.trim();
|
|
12
12
|
if (!ref)
|
|
13
|
-
throw new UsageError("
|
|
13
|
+
throw new UsageError("Registry ref is required. " + "Examples: `akm kit add @scope/kit`, `akm kit add github:owner/repo`");
|
|
14
|
+
const stashDir = resolveStashDir();
|
|
15
|
+
try {
|
|
16
|
+
const parsed = parseRegistryRef(ref);
|
|
17
|
+
if (parsed.source === "local") {
|
|
18
|
+
throw new UsageError(`Local directories should be added as stashes, not kits. Use \`akm stash add ${ref}\` instead.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err instanceof UsageError)
|
|
23
|
+
throw err;
|
|
24
|
+
// Not a local ref — fall through to registry install
|
|
25
|
+
}
|
|
26
|
+
return addRegistryKit(ref, stashDir);
|
|
27
|
+
}
|
|
28
|
+
export async function akmAdd(input) {
|
|
29
|
+
const ref = input.ref.trim();
|
|
30
|
+
if (!ref)
|
|
31
|
+
throw new UsageError("Install ref or local directory is required. " +
|
|
32
|
+
"Examples: `akm add @scope/kit`, `akm add github:owner/repo`, `akm add ./local/path`");
|
|
14
33
|
const stashDir = resolveStashDir();
|
|
15
34
|
// Detect local directory refs and route them to stashes[] instead of installed[]
|
|
16
35
|
try {
|
|
@@ -44,7 +63,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir) {
|
|
|
44
63
|
stashes.push(entry);
|
|
45
64
|
saveConfig({ ...config, stashes });
|
|
46
65
|
}
|
|
47
|
-
const index = await
|
|
66
|
+
const index = await akmIndex({ stashDir });
|
|
48
67
|
const updatedConfig = loadConfig();
|
|
49
68
|
return {
|
|
50
69
|
schemaVersion: 1,
|
|
@@ -57,7 +76,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir) {
|
|
|
57
76
|
stashRoot: resolvedPath,
|
|
58
77
|
},
|
|
59
78
|
config: {
|
|
60
|
-
|
|
79
|
+
stashCount: updatedConfig.stashes?.length ?? 0,
|
|
61
80
|
installedKitCount: updatedConfig.installed?.length ?? 0,
|
|
62
81
|
},
|
|
63
82
|
index: {
|
|
@@ -102,7 +121,7 @@ async function addRegistryKit(ref, stashDir) {
|
|
|
102
121
|
// Best-effort cleanup only.
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
|
-
const index = await
|
|
124
|
+
const index = await akmIndex({ stashDir });
|
|
106
125
|
return {
|
|
107
126
|
schemaVersion: 1,
|
|
108
127
|
stashDir,
|
|
@@ -120,7 +139,7 @@ async function addRegistryKit(ref, stashDir) {
|
|
|
120
139
|
installedAt: installed.installedAt,
|
|
121
140
|
},
|
|
122
141
|
config: {
|
|
123
|
-
|
|
142
|
+
stashCount: config.stashes?.length ?? 0,
|
|
124
143
|
installedKitCount: config.installed?.length ?? 0,
|
|
125
144
|
},
|
|
126
145
|
index: {
|