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.
@@ -0,0 +1,226 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fetchWithRetry, IS_WINDOWS } from "./common";
5
+ import { githubHeaders } from "./github";
6
+ const REPO = "itlackey/agentikit";
7
+ export function detectInstallMethod() {
8
+ // Bun-compiled binaries: Bun.main equals process.execPath
9
+ if (typeof Bun !== "undefined" && Bun.main === process.execPath) {
10
+ return "binary";
11
+ }
12
+ // npm/bun global install: import.meta.dir contains node_modules
13
+ if (import.meta.dir?.includes("node_modules")) {
14
+ return "npm";
15
+ }
16
+ return "unknown";
17
+ }
18
+ export function getAkmBinaryName() {
19
+ const platform = process.platform;
20
+ const arch = process.arch;
21
+ if (platform === "linux" && arch === "x64")
22
+ return "akm-linux-x64";
23
+ if (platform === "linux" && arch === "arm64")
24
+ return "akm-linux-arm64";
25
+ if (platform === "darwin" && arch === "x64")
26
+ return "akm-darwin-x64";
27
+ if (platform === "darwin" && arch === "arm64")
28
+ return "akm-darwin-arm64";
29
+ if (platform === "win32" && arch === "x64")
30
+ return "akm-windows-x64.exe";
31
+ throw new Error(`Unsupported platform for binary upgrade: ${platform}/${arch}`);
32
+ }
33
+ export async function checkForUpdate(currentVersion) {
34
+ const installMethod = detectInstallMethod();
35
+ const url = `https://api.github.com/repos/${REPO}/releases/latest`;
36
+ const response = await fetchWithRetry(url, { headers: githubHeaders() });
37
+ if (!response.ok) {
38
+ throw new Error(`Failed to check for updates: ${response.status} ${response.statusText}`);
39
+ }
40
+ const release = (await response.json());
41
+ const latestTag = release.tag_name ?? "";
42
+ const latestVersion = latestTag.replace(/^v/, "");
43
+ return {
44
+ currentVersion,
45
+ latestVersion,
46
+ updateAvailable: latestVersion !== "" && Bun.semver.order(currentVersion, latestVersion) < 0,
47
+ installMethod,
48
+ };
49
+ }
50
+ export async function performUpgrade(check, opts) {
51
+ const { currentVersion, latestVersion, installMethod } = check;
52
+ const force = opts?.force === true;
53
+ if (installMethod === "npm") {
54
+ return {
55
+ currentVersion,
56
+ newVersion: latestVersion,
57
+ upgraded: false,
58
+ installMethod,
59
+ message: `akm installed via npm. Run: bun install -g akm-cli@latest`,
60
+ };
61
+ }
62
+ if (installMethod === "unknown") {
63
+ return {
64
+ currentVersion,
65
+ newVersion: latestVersion,
66
+ upgraded: false,
67
+ installMethod,
68
+ message: `Unable to detect install method. Upgrade manually from https://github.com/${REPO}/releases`,
69
+ };
70
+ }
71
+ // Binary install
72
+ if (!check.updateAvailable && !force) {
73
+ return {
74
+ currentVersion,
75
+ newVersion: latestVersion,
76
+ upgraded: false,
77
+ installMethod,
78
+ message: `akm v${currentVersion} is already the latest version`,
79
+ };
80
+ }
81
+ if (!latestVersion) {
82
+ throw new Error("Unable to determine latest version from GitHub releases. Check https://github.com/itlackey/agentikit/releases");
83
+ }
84
+ const tag = `v${latestVersion}`;
85
+ const binaryName = getAkmBinaryName();
86
+ const binaryUrl = `https://github.com/${REPO}/releases/download/${tag}/${binaryName}`;
87
+ const checksumsUrl = `https://github.com/${REPO}/releases/download/${tag}/checksums.txt`;
88
+ // Download binary
89
+ const binaryResponse = await fetchWithRetry(binaryUrl);
90
+ if (!binaryResponse.ok) {
91
+ throw new Error(`Failed to download binary: ${binaryResponse.status} ${binaryResponse.statusText}`);
92
+ }
93
+ const binaryData = new Uint8Array(await binaryResponse.arrayBuffer());
94
+ // Download and verify checksum
95
+ let checksumVerified = false;
96
+ try {
97
+ const checksumsResponse = await fetchWithRetry(checksumsUrl);
98
+ if (checksumsResponse.ok) {
99
+ const checksumsText = await checksumsResponse.text();
100
+ const expectedHash = parseChecksumForFile(checksumsText, binaryName);
101
+ if (expectedHash) {
102
+ const actualHash = createHash("sha256").update(binaryData).digest("hex");
103
+ if (actualHash !== expectedHash) {
104
+ throw new Error(`Checksum mismatch for ${binaryName}.\n` + `Expected: ${expectedHash}\n` + `Got: ${actualHash}`);
105
+ }
106
+ checksumVerified = true;
107
+ }
108
+ }
109
+ }
110
+ catch (err) {
111
+ if (err instanceof Error && err.message.includes("Checksum mismatch")) {
112
+ throw err;
113
+ }
114
+ // Non-fatal: checksum file missing or unparseable
115
+ }
116
+ const execPath = process.execPath;
117
+ const execDir = path.dirname(execPath);
118
+ const execName = path.basename(execPath);
119
+ if (IS_WINDOWS) {
120
+ // Windows: rename running exe, write new one, clean up old on success
121
+ const oldPath = `${execPath}.old`;
122
+ try {
123
+ fs.renameSync(execPath, oldPath);
124
+ }
125
+ catch (err) {
126
+ const code = err.code;
127
+ if (code === "EPERM" || code === "EACCES") {
128
+ throw new Error(`Permission denied. Cannot rename ${execPath}.\n` +
129
+ `Try running as Administrator, or re-download from https://github.com/${REPO}/releases`);
130
+ }
131
+ const detail = err instanceof Error ? err.message : String(err);
132
+ throw new Error(`Failed to rename ${execPath}: ${detail}`);
133
+ }
134
+ try {
135
+ fs.writeFileSync(execPath, binaryData);
136
+ }
137
+ catch (err) {
138
+ // Restore from old
139
+ fs.renameSync(oldPath, execPath);
140
+ throw err;
141
+ }
142
+ // Best-effort cleanup of .old
143
+ try {
144
+ fs.unlinkSync(oldPath);
145
+ }
146
+ catch {
147
+ // Windows may lock the old exe — it will be cleaned up on next startup or manually
148
+ }
149
+ }
150
+ else {
151
+ // Unix: write to temp file, chmod +x, atomic rename
152
+ const tmpPath = path.join(execDir, `.${execName}.tmp.${process.pid}`);
153
+ const bakPath = `${execPath}.bak`;
154
+ try {
155
+ fs.writeFileSync(tmpPath, binaryData);
156
+ fs.chmodSync(tmpPath, 0o755);
157
+ }
158
+ catch (err) {
159
+ // Clean up temp file on failure
160
+ try {
161
+ fs.unlinkSync(tmpPath);
162
+ }
163
+ catch {
164
+ /* ignore */
165
+ }
166
+ const code = err.code;
167
+ if (code === "EACCES" || code === "EPERM") {
168
+ throw new Error(`Permission denied writing to ${execDir}.\n` +
169
+ `Run: sudo akm upgrade\n` +
170
+ `Or re-run the install script: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`);
171
+ }
172
+ throw err;
173
+ }
174
+ // Backup current, then atomic rename
175
+ try {
176
+ fs.copyFileSync(execPath, bakPath);
177
+ fs.renameSync(tmpPath, execPath);
178
+ }
179
+ catch (err) {
180
+ // Restore from backup if rename failed
181
+ try {
182
+ fs.unlinkSync(tmpPath);
183
+ }
184
+ catch {
185
+ /* ignore */
186
+ }
187
+ try {
188
+ if (fs.existsSync(bakPath) && !fs.existsSync(execPath)) {
189
+ fs.renameSync(bakPath, execPath);
190
+ }
191
+ }
192
+ catch {
193
+ /* ignore */
194
+ }
195
+ throw err;
196
+ }
197
+ // Cleanup backup
198
+ try {
199
+ fs.unlinkSync(bakPath);
200
+ }
201
+ catch {
202
+ /* ignore */
203
+ }
204
+ }
205
+ return {
206
+ currentVersion,
207
+ newVersion: latestVersion,
208
+ upgraded: true,
209
+ installMethod,
210
+ binaryPath: execPath,
211
+ checksumVerified,
212
+ };
213
+ }
214
+ function parseChecksumForFile(checksumsText, filename) {
215
+ for (const line of checksumsText.split("\n")) {
216
+ const trimmed = line.trim();
217
+ if (!trimmed)
218
+ continue;
219
+ // Format: <hash> <filename>
220
+ const match = trimmed.match(/^([0-9a-f]{64})\s+(.+)$/);
221
+ if (match && match[2] === filename) {
222
+ return match[1];
223
+ }
224
+ }
225
+ return undefined;
226
+ }
@@ -0,0 +1,71 @@
1
+ import fs from "node:fs";
2
+ import { resolveStashDir } from "./common";
3
+ import { loadConfig } from "./config";
4
+ import { UsageError } from "./errors";
5
+ import { agentikitIndex } from "./indexer";
6
+ import { upsertLockEntry } from "./lockfile";
7
+ import { installRegistryRef, upsertInstalledRegistryEntry } from "./registry-install";
8
+ export async function agentikitAdd(input) {
9
+ const ref = input.ref.trim();
10
+ if (!ref)
11
+ throw new UsageError("Install ref or local directory is required.");
12
+ const stashDir = resolveStashDir();
13
+ const installed = await installRegistryRef(ref);
14
+ const replaced = (loadConfig().installed ?? []).find((entry) => entry.id === installed.id);
15
+ const config = upsertInstalledRegistryEntry({
16
+ id: installed.id,
17
+ source: installed.source,
18
+ ref: installed.ref,
19
+ artifactUrl: installed.artifactUrl,
20
+ resolvedVersion: installed.resolvedVersion,
21
+ resolvedRevision: installed.resolvedRevision,
22
+ stashRoot: installed.stashRoot,
23
+ cacheDir: installed.cacheDir,
24
+ installedAt: installed.installedAt,
25
+ });
26
+ upsertLockEntry({
27
+ id: installed.id,
28
+ source: installed.source,
29
+ ref: installed.ref,
30
+ resolvedVersion: installed.resolvedVersion,
31
+ resolvedRevision: installed.resolvedRevision,
32
+ integrity: installed.integrity ?? (installed.source === "local" ? "local" : undefined),
33
+ });
34
+ // Clean up old cache directory on re-install (skip for local sources — no cache to clean)
35
+ if (replaced && replaced.source !== "local" && replaced.cacheDir !== installed.cacheDir) {
36
+ try {
37
+ fs.rmSync(replaced.cacheDir, { recursive: true, force: true });
38
+ }
39
+ catch {
40
+ // Best-effort cleanup only.
41
+ }
42
+ }
43
+ const index = await agentikitIndex({ stashDir });
44
+ return {
45
+ schemaVersion: 1,
46
+ stashDir,
47
+ ref,
48
+ installed: {
49
+ id: installed.id,
50
+ source: installed.source,
51
+ ref: installed.ref,
52
+ artifactUrl: installed.artifactUrl,
53
+ resolvedVersion: installed.resolvedVersion,
54
+ resolvedRevision: installed.resolvedRevision,
55
+ stashRoot: installed.stashRoot,
56
+ cacheDir: installed.cacheDir,
57
+ extractedDir: installed.extractedDir,
58
+ installedAt: installed.installedAt,
59
+ },
60
+ config: {
61
+ searchPaths: config.searchPaths,
62
+ installedKitCount: config.installed?.length ?? 0,
63
+ },
64
+ index: {
65
+ mode: index.mode,
66
+ totalEntries: index.totalEntries,
67
+ directoriesScanned: index.directoriesScanned,
68
+ directoriesSkipped: index.directoriesSkipped,
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,115 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { TYPE_DIRS } from "./asset-spec";
4
+ import { isRemoteOrigin, resolveSourcesForOrigin } from "./origin-resolve";
5
+ import { installRegistryRef } from "./registry-install";
6
+ import { makeAssetRef, parseAssetRef } from "./stash-ref";
7
+ import { resolveAssetPath } from "./stash-resolve";
8
+ import { findSourceForPath, getPrimarySource, resolveStashSources } from "./stash-source";
9
+ export async function agentikitClone(options) {
10
+ const parsed = parseAssetRef(options.sourceRef);
11
+ // When --dest is provided, the working stash is optional
12
+ let allSources;
13
+ try {
14
+ allSources = resolveStashSources();
15
+ }
16
+ catch (err) {
17
+ if (options.dest) {
18
+ allSources = [];
19
+ }
20
+ else {
21
+ throw err;
22
+ }
23
+ }
24
+ const primarySource = getPrimarySource(allSources);
25
+ const destRoot = options.dest ? path.resolve(options.dest) : primarySource?.path;
26
+ if (!destRoot) {
27
+ throw new Error("No working stash configured and no --dest provided. Run `akm init` or pass --dest.");
28
+ }
29
+ let searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
30
+ // Remote fetch fallback: if no local source matched and origin looks remote, fetch it
31
+ let remoteFetched;
32
+ if (searchSources.length === 0 && parsed.origin && isRemoteOrigin(parsed.origin, allSources)) {
33
+ const installResult = await installRegistryRef(parsed.origin);
34
+ const syntheticSource = {
35
+ path: installResult.stashRoot,
36
+ registryId: installResult.id,
37
+ };
38
+ searchSources = [syntheticSource];
39
+ allSources = [...allSources, syntheticSource];
40
+ remoteFetched = {
41
+ origin: parsed.origin,
42
+ stashRoot: installResult.stashRoot,
43
+ cacheDir: installResult.cacheDir,
44
+ };
45
+ }
46
+ let sourcePath;
47
+ let lastError;
48
+ for (const source of searchSources) {
49
+ try {
50
+ sourcePath = resolveAssetPath(source.path, parsed.type, parsed.name);
51
+ break;
52
+ }
53
+ catch (err) {
54
+ lastError = err instanceof Error ? err : new Error(String(err));
55
+ }
56
+ }
57
+ if (!sourcePath) {
58
+ const context = remoteFetched ? ` (remote package fetched but asset not found inside it)` : "";
59
+ throw lastError ?? new Error(`Source asset not found for ref: ${options.sourceRef}${context}`);
60
+ }
61
+ const sourceSource = findSourceForPath(sourcePath, allSources);
62
+ const destName = options.newName ?? parsed.name;
63
+ const typeDir = TYPE_DIRS[parsed.type];
64
+ const destLabel = options.dest ? "at destination" : "in working stash";
65
+ // Guard against self-clone
66
+ if (parsed.type === "skill") {
67
+ const sourceSkillDir = path.resolve(path.dirname(sourcePath));
68
+ const destSkillDir = path.resolve(path.join(destRoot, typeDir, destName));
69
+ if (sourceSkillDir === destSkillDir) {
70
+ throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
71
+ }
72
+ }
73
+ else {
74
+ const resolvedSource = path.resolve(sourcePath);
75
+ const resolvedDest = path.resolve(path.join(destRoot, typeDir, destName));
76
+ if (resolvedSource === resolvedDest) {
77
+ throw new Error(`Source and destination are the same path. Use --name to provide a new name for the clone.`);
78
+ }
79
+ }
80
+ let destPath;
81
+ if (parsed.type === "skill") {
82
+ const sourceSkillDir = path.dirname(sourcePath);
83
+ const destSkillDir = path.join(destRoot, typeDir, destName);
84
+ const overwritten = fs.existsSync(destSkillDir);
85
+ if (overwritten && !options.force) {
86
+ throw new Error(`Asset already exists ${destLabel}: ${destSkillDir}. Use --force to overwrite.`);
87
+ }
88
+ if (overwritten) {
89
+ fs.rmSync(destSkillDir, { recursive: true, force: true });
90
+ }
91
+ fs.cpSync(sourceSkillDir, destSkillDir, { recursive: true });
92
+ destPath = path.join(destSkillDir, "SKILL.md");
93
+ const ref = makeAssetRef(parsed.type, destName, "local");
94
+ return {
95
+ source: { path: sourcePath, registryId: sourceSource?.registryId },
96
+ destination: { path: destPath, ref },
97
+ overwritten,
98
+ ...(remoteFetched ? { remoteFetched } : {}),
99
+ };
100
+ }
101
+ destPath = path.join(destRoot, typeDir, destName);
102
+ const overwritten = fs.existsSync(destPath);
103
+ if (overwritten && !options.force) {
104
+ throw new Error(`Asset already exists ${destLabel}: ${destPath}. Use --force to overwrite.`);
105
+ }
106
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
107
+ fs.copyFileSync(sourcePath, destPath);
108
+ const ref = makeAssetRef(parsed.type, destName, "local");
109
+ return {
110
+ source: { path: sourcePath, registryId: sourceSource?.registryId },
111
+ destination: { path: destPath, ref },
112
+ overwritten,
113
+ ...(remoteFetched ? { remoteFetched } : {}),
114
+ };
115
+ }
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ import { isAssetType } from "./common";
3
+ import { UsageError } from "./errors";
4
+ // ── Construction ────────────────────────────────────────────────────────────
5
+ /**
6
+ * Build a ref string from components.
7
+ *
8
+ * Examples:
9
+ * makeAssetRef("script", "deploy.sh")
10
+ * → "script:deploy.sh"
11
+ * makeAssetRef("script", "deploy.sh", "npm:@scope/pkg")
12
+ * → "npm:@scope/pkg//script:deploy.sh"
13
+ * makeAssetRef("skill", "code-review", "local")
14
+ * → "local//skill:code-review"
15
+ * makeAssetRef("script", "db/migrate/run.sh", "owner/repo")
16
+ * → "owner/repo//script:db/migrate/run.sh"
17
+ */
18
+ export function makeAssetRef(type, name, origin) {
19
+ validateName(name);
20
+ const normalized = normalizeName(name);
21
+ const asset = `${type}:${normalized}`;
22
+ if (!origin)
23
+ return asset;
24
+ return `${origin}//${asset}`;
25
+ }
26
+ // ── Parsing ─────────────────────────────────────────────────────────────────
27
+ /**
28
+ * Parse a ref string in the format `[origin//]type:name`.
29
+ */
30
+ export function parseAssetRef(ref) {
31
+ const trimmed = ref.trim();
32
+ if (!trimmed)
33
+ throw new UsageError("Empty ref.");
34
+ let origin;
35
+ let body = trimmed;
36
+ const boundary = trimmed.indexOf("//");
37
+ if (boundary >= 0) {
38
+ origin = trimmed.slice(0, boundary);
39
+ body = trimmed.slice(boundary + 2);
40
+ if (!origin)
41
+ throw new UsageError("Empty origin in ref.");
42
+ }
43
+ const colon = body.indexOf(":");
44
+ if (colon <= 0) {
45
+ throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name`);
46
+ }
47
+ const rawType = body.slice(0, colon);
48
+ const rawName = body.slice(colon + 1);
49
+ if (!isAssetType(rawType)) {
50
+ throw new UsageError(`Invalid asset type: "${rawType}".`);
51
+ }
52
+ validateName(rawName);
53
+ const name = normalizeName(rawName);
54
+ return { type: rawType, name, origin: origin || undefined };
55
+ }
56
+ // ── Validation ──────────────────────────────────────────────────────────────
57
+ function validateName(name) {
58
+ if (!name)
59
+ throw new UsageError("Empty asset name.");
60
+ if (name.includes("\0"))
61
+ throw new UsageError("Null byte in asset name.");
62
+ if (/^[A-Za-z]:/.test(name))
63
+ throw new UsageError("Windows drive path in asset name.");
64
+ const normalized = path.posix.normalize(name.replace(/\\/g, "/"));
65
+ if (path.posix.isAbsolute(normalized))
66
+ throw new UsageError("Absolute path in asset name.");
67
+ if (normalized === ".." || normalized.startsWith("../")) {
68
+ throw new UsageError("Path traversal in asset name.");
69
+ }
70
+ }
71
+ function normalizeName(name) {
72
+ return path.posix.normalize(name.replace(/\\/g, "/"));
73
+ }
@@ -0,0 +1,206 @@
1
+ import fs from "node:fs";
2
+ import { resolveStashDir } from "./common";
3
+ import { loadConfig } from "./config";
4
+ import { NotFoundError, UsageError } from "./errors";
5
+ import { agentikitIndex } from "./indexer";
6
+ import { removeLockEntry, upsertLockEntry } from "./lockfile";
7
+ import { installRegistryRef, removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./registry-install";
8
+ import { parseRegistryRef } from "./registry-resolve";
9
+ export async function agentikitList(input) {
10
+ const stashDir = input?.stashDir ?? resolveStashDir();
11
+ const config = loadConfig();
12
+ const installed = config.installed ?? [];
13
+ return {
14
+ schemaVersion: 1,
15
+ stashDir,
16
+ installed: installed.map((entry) => ({
17
+ ...entry,
18
+ status: {
19
+ cacheDirExists: directoryExists(entry.cacheDir),
20
+ stashRootExists: directoryExists(entry.stashRoot),
21
+ },
22
+ })),
23
+ totalInstalled: installed.length,
24
+ };
25
+ }
26
+ export async function agentikitRemove(input) {
27
+ const target = input.target.trim();
28
+ if (!target)
29
+ throw new UsageError("Target is required.");
30
+ const stashDir = input.stashDir ?? resolveStashDir();
31
+ const config = loadConfig();
32
+ const installed = config.installed ?? [];
33
+ const entry = resolveInstalledTarget(installed, target);
34
+ const updatedConfig = removeInstalledRegistryEntry(entry.id);
35
+ removeLockEntry(entry.id);
36
+ // Only clean up cache for non-local sources — local sources point to the
37
+ // user's real directory on disk and must never be deleted.
38
+ if (entry.source !== "local") {
39
+ cleanupDirectoryBestEffort(entry.cacheDir);
40
+ }
41
+ const index = await agentikitIndex({ stashDir });
42
+ return {
43
+ schemaVersion: 1,
44
+ stashDir,
45
+ target,
46
+ removed: {
47
+ id: entry.id,
48
+ source: entry.source,
49
+ ref: entry.ref,
50
+ cacheDir: entry.cacheDir,
51
+ stashRoot: entry.stashRoot,
52
+ },
53
+ config: {
54
+ searchPaths: updatedConfig.searchPaths,
55
+ installedKitCount: updatedConfig.installed?.length ?? 0,
56
+ },
57
+ index: {
58
+ mode: index.mode,
59
+ totalEntries: index.totalEntries,
60
+ directoriesScanned: index.directoriesScanned,
61
+ directoriesSkipped: index.directoriesSkipped,
62
+ },
63
+ };
64
+ }
65
+ export async function agentikitUpdate(input) {
66
+ const stashDir = input?.stashDir ?? resolveStashDir();
67
+ const target = input?.target?.trim();
68
+ const all = input?.all === true;
69
+ const force = input?.force === true;
70
+ const installedEntries = loadConfig().installed ?? [];
71
+ const selectedEntries = selectTargets(installedEntries, target, all);
72
+ const processed = [];
73
+ for (const entry of selectedEntries) {
74
+ if (force && shouldCleanupCache(entry)) {
75
+ cleanupDirectoryBestEffort(entry.cacheDir);
76
+ }
77
+ const installed = await installRegistryRef(entry.ref);
78
+ upsertInstalledRegistryEntry(toInstalledEntry(installed));
79
+ upsertLockEntry({
80
+ id: installed.id,
81
+ source: installed.source,
82
+ ref: installed.ref,
83
+ resolvedVersion: installed.resolvedVersion,
84
+ resolvedRevision: installed.resolvedRevision,
85
+ integrity: installed.integrity ?? (installed.source === "local" ? "local" : undefined),
86
+ });
87
+ if (entry.cacheDir !== installed.cacheDir && shouldCleanupCache(entry)) {
88
+ cleanupDirectoryBestEffort(entry.cacheDir);
89
+ }
90
+ const versionChanged = (entry.resolvedVersion ?? "") !== (installed.resolvedVersion ?? "");
91
+ const revisionChanged = (entry.resolvedRevision ?? "") !== (installed.resolvedRevision ?? "");
92
+ processed.push({
93
+ id: entry.id,
94
+ source: entry.source,
95
+ ref: entry.ref,
96
+ previous: {
97
+ resolvedVersion: entry.resolvedVersion,
98
+ resolvedRevision: entry.resolvedRevision,
99
+ cacheDir: entry.cacheDir,
100
+ },
101
+ installed: toInstallStatus(installed),
102
+ changed: {
103
+ version: versionChanged,
104
+ revision: revisionChanged,
105
+ any: versionChanged || revisionChanged,
106
+ },
107
+ });
108
+ }
109
+ const index = await agentikitIndex({ stashDir });
110
+ const config = loadConfig();
111
+ return {
112
+ schemaVersion: 1,
113
+ stashDir,
114
+ target,
115
+ all,
116
+ processed,
117
+ config: {
118
+ searchPaths: config.searchPaths,
119
+ installedKitCount: config.installed?.length ?? 0,
120
+ },
121
+ index: {
122
+ mode: index.mode,
123
+ totalEntries: index.totalEntries,
124
+ directoriesScanned: index.directoriesScanned,
125
+ directoriesSkipped: index.directoriesSkipped,
126
+ },
127
+ };
128
+ }
129
+ function selectTargets(installed, target, all) {
130
+ if (all && target) {
131
+ throw new UsageError("Specify either <target> or --all, not both.");
132
+ }
133
+ if (all)
134
+ return installed;
135
+ if (!target) {
136
+ throw new UsageError("Either <target> or --all is required.");
137
+ }
138
+ return [resolveInstalledTarget(installed, target)];
139
+ }
140
+ function resolveInstalledTarget(installed, target) {
141
+ const byId = installed.find((entry) => entry.id === target);
142
+ if (byId)
143
+ return byId;
144
+ const byRef = installed.find((entry) => entry.ref === target);
145
+ if (byRef)
146
+ return byRef;
147
+ let parsedId;
148
+ try {
149
+ parsedId = parseRegistryRef(target).id;
150
+ }
151
+ catch {
152
+ parsedId = undefined;
153
+ }
154
+ if (parsedId) {
155
+ const byParsedId = installed.find((entry) => entry.id === parsedId);
156
+ if (byParsedId)
157
+ return byParsedId;
158
+ }
159
+ throw new NotFoundError(`No installed kit matched target: ${target}`);
160
+ }
161
+ function toInstalledEntry(status) {
162
+ return {
163
+ id: status.id,
164
+ source: status.source,
165
+ ref: status.ref,
166
+ artifactUrl: status.artifactUrl,
167
+ resolvedVersion: status.resolvedVersion,
168
+ resolvedRevision: status.resolvedRevision,
169
+ stashRoot: status.stashRoot,
170
+ cacheDir: status.cacheDir,
171
+ installedAt: status.installedAt,
172
+ };
173
+ }
174
+ function toInstallStatus(status) {
175
+ return {
176
+ id: status.id,
177
+ source: status.source,
178
+ ref: status.ref,
179
+ artifactUrl: status.artifactUrl,
180
+ resolvedVersion: status.resolvedVersion,
181
+ resolvedRevision: status.resolvedRevision,
182
+ stashRoot: status.stashRoot,
183
+ cacheDir: status.cacheDir,
184
+ extractedDir: status.extractedDir,
185
+ installedAt: status.installedAt,
186
+ };
187
+ }
188
+ function cleanupDirectoryBestEffort(target) {
189
+ try {
190
+ fs.rmSync(target, { recursive: true, force: true });
191
+ }
192
+ catch {
193
+ // Best-effort cleanup only.
194
+ }
195
+ }
196
+ function shouldCleanupCache(entry) {
197
+ return entry.source !== "local";
198
+ }
199
+ function directoryExists(target) {
200
+ try {
201
+ return fs.statSync(target).isDirectory();
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ }