akm-cli 0.0.0 → 0.0.16

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,75 @@
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("tool", "deploy.sh")
10
+ * → "tool:deploy.sh"
11
+ * makeAssetRef("tool", "deploy.sh", "npm:@scope/pkg")
12
+ * → "npm:@scope/pkg//tool:deploy.sh"
13
+ * makeAssetRef("skill", "code-review", "local")
14
+ * → "local//skill:code-review"
15
+ * makeAssetRef("tool", "db/migrate/run.sh", "owner/repo")
16
+ * → "owner/repo//tool:db/migrate/run.sh"
17
+ */
18
+ export function makeAssetRef(type, name, origin) {
19
+ validateName(name);
20
+ const normalized = normalizeName(name);
21
+ // "tool" is a transparent alias for "script" -- normalize to "script" in refs
22
+ const resolvedType = type === "tool" ? "script" : type;
23
+ const asset = `${resolvedType}:${normalized}`;
24
+ if (!origin)
25
+ return asset;
26
+ return `${origin}//${asset}`;
27
+ }
28
+ // ── Parsing ─────────────────────────────────────────────────────────────────
29
+ /**
30
+ * Parse a ref string in the format `[origin//]type:name`.
31
+ */
32
+ export function parseAssetRef(ref) {
33
+ const trimmed = ref.trim();
34
+ if (!trimmed)
35
+ throw new UsageError("Empty ref.");
36
+ let origin;
37
+ let body = trimmed;
38
+ const boundary = trimmed.indexOf("//");
39
+ if (boundary >= 0) {
40
+ origin = trimmed.slice(0, boundary);
41
+ body = trimmed.slice(boundary + 2);
42
+ if (!origin)
43
+ throw new UsageError("Empty origin in ref.");
44
+ }
45
+ const colon = body.indexOf(":");
46
+ if (colon <= 0) {
47
+ throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name`);
48
+ }
49
+ const rawType = body.slice(0, colon);
50
+ const rawName = body.slice(colon + 1);
51
+ if (!isAssetType(rawType)) {
52
+ throw new UsageError(`Invalid asset type: "${rawType}".`);
53
+ }
54
+ validateName(rawName);
55
+ const name = normalizeName(rawName);
56
+ return { type: rawType, name, origin: origin || undefined };
57
+ }
58
+ // ── Validation ──────────────────────────────────────────────────────────────
59
+ function validateName(name) {
60
+ if (!name)
61
+ throw new UsageError("Empty asset name.");
62
+ if (name.includes("\0"))
63
+ throw new UsageError("Null byte in asset name.");
64
+ if (/^[A-Za-z]:/.test(name))
65
+ throw new UsageError("Windows drive path in asset name.");
66
+ const normalized = path.posix.normalize(name.replace(/\\/g, "/"));
67
+ if (path.posix.isAbsolute(normalized))
68
+ throw new UsageError("Absolute path in asset name.");
69
+ if (normalized === ".." || normalized.startsWith("../")) {
70
+ throw new UsageError("Path traversal in asset name.");
71
+ }
72
+ }
73
+ function normalizeName(name) {
74
+ return path.posix.normalize(name.replace(/\\/g, "/"));
75
+ }
@@ -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.registry?.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.registry?.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
+ installedRegistryCount: updatedConfig.registry?.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().registry?.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
+ installedRegistryCount: config.registry?.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 registry entry 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
+ }
@@ -0,0 +1,92 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isRelevantAssetFile, resolveAssetPathFromName, TYPE_DIRS } from "./asset-spec";
4
+ import { hasErrnoCode, isWithin, normalizeAssetType } from "./common";
5
+ import { NotFoundError, UsageError } from "./errors";
6
+ /**
7
+ * Resolve an asset path from a stash directory, type, and name.
8
+ *
9
+ * When `type` is "script" or "tool" (which is a transparent alias for "script"),
10
+ * resolution tries both the primary type directory and the alias directory:
11
+ * - script → tries scripts/ then tools/
12
+ * - tool → tries tools/ then scripts/
13
+ * This ensures that `script:deploy.sh` can find files in either `scripts/` or `tools/`.
14
+ */
15
+ export function resolveAssetPath(stashDir, type, name) {
16
+ // For script/tool, try the primary directory first, then the alias directory.
17
+ if (type === "script" || type === "tool") {
18
+ const primaryDir = TYPE_DIRS[type];
19
+ const aliasDir = type === "script" ? "tools" : "scripts";
20
+ const dirsToTry = [primaryDir, aliasDir];
21
+ let primaryError;
22
+ let lastError;
23
+ for (let i = 0; i < dirsToTry.length; i++) {
24
+ try {
25
+ return resolveInTypeDir(stashDir, dirsToTry[i], type, name);
26
+ }
27
+ catch (err) {
28
+ const error = err instanceof Error ? err : new Error(String(err));
29
+ if (i === 0)
30
+ primaryError = error;
31
+ lastError = error;
32
+ // Only fall through on NotFoundError -- rethrow security/usage errors immediately
33
+ if (err instanceof UsageError)
34
+ throw err;
35
+ }
36
+ }
37
+ // Prefer the primary directory's error when it's about extension validation
38
+ // (i.e., the file was found but had the wrong extension) over a generic
39
+ // "not found" from the alias directory.
40
+ const errorToThrow = primaryError?.message.includes("supported script extension")
41
+ ? primaryError
42
+ : (lastError ?? new NotFoundError(`Stash asset not found for ref: ${normalizeAssetType(type)}:${name}`));
43
+ throw errorToThrow;
44
+ }
45
+ return resolveInTypeDir(stashDir, TYPE_DIRS[type], type, name);
46
+ }
47
+ /**
48
+ * Try to resolve an asset path within a specific type directory.
49
+ */
50
+ function resolveInTypeDir(stashDir, typeDir, type, name) {
51
+ const root = path.join(stashDir, typeDir);
52
+ const target = resolveAssetPathFromName(type, root, name);
53
+ const resolvedRoot = resolveAndValidateTypeRoot(root, type, name);
54
+ const resolvedTarget = path.resolve(target);
55
+ if (!isWithin(resolvedTarget, resolvedRoot)) {
56
+ throw new UsageError("Ref resolves outside the stash root.");
57
+ }
58
+ if (!fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isFile()) {
59
+ throw new NotFoundError(`Stash asset not found for ref: ${normalizeAssetType(type)}:${name}`);
60
+ }
61
+ const realTarget = fs.realpathSync(resolvedTarget);
62
+ if (!isWithin(realTarget, resolvedRoot)) {
63
+ throw new UsageError("Ref resolves outside the stash root.");
64
+ }
65
+ // Use "script" for relevance check since tool is an alias for script
66
+ const relevanceType = type === "tool" ? "script" : type;
67
+ if (!isRelevantAssetFile(relevanceType, path.basename(resolvedTarget))) {
68
+ if (type === "tool" || type === "script") {
69
+ throw new NotFoundError("Script ref must resolve to a file with a supported script extension. Refer to the akm documentation for the complete list of supported script extensions.");
70
+ }
71
+ throw new NotFoundError(`Stash asset not found for ref: ${normalizeAssetType(type)}:${name}`);
72
+ }
73
+ return realTarget;
74
+ }
75
+ function resolveAndValidateTypeRoot(root, type, name) {
76
+ const rootStat = readTypeRootStat(root, type, name);
77
+ if (!rootStat.isDirectory()) {
78
+ throw new NotFoundError(`Stash type root is not a directory for ref: ${normalizeAssetType(type)}:${name}`);
79
+ }
80
+ return fs.realpathSync(root);
81
+ }
82
+ function readTypeRootStat(root, type, name) {
83
+ try {
84
+ return fs.statSync(root);
85
+ }
86
+ catch (error) {
87
+ if (hasErrnoCode(error, "ENOENT")) {
88
+ throw new NotFoundError(`Stash type root not found for ref: ${normalizeAssetType(type)}:${name}`);
89
+ }
90
+ throw error;
91
+ }
92
+ }