akm-cli 0.3.1 → 0.4.1
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/dist/cli.js +602 -8
- package/dist/common.js +5 -0
- package/dist/config-cli.js +87 -0
- package/dist/config.js +197 -25
- package/dist/embedder.js +26 -4
- package/dist/init.js +2 -2
- package/dist/install-audit.js +324 -0
- package/dist/installed-kits.js +2 -2
- package/dist/matchers.js +25 -22
- package/dist/registry-install.js +46 -7
- package/dist/setup.js +2 -2
- package/dist/stash-add.js +4 -3
- package/dist/stash-source-manage.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { filterNonEmptyStrings } from "./common";
|
|
4
|
+
const DEFAULT_INSTALL_AUDIT_CONFIG = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
blockOnCritical: true,
|
|
7
|
+
blockUnlistedRegistries: false,
|
|
8
|
+
registryAllowlist: [],
|
|
9
|
+
};
|
|
10
|
+
const MAX_SCANNED_FILE_BYTES = 256 * 1024;
|
|
11
|
+
const LIFECYCLE_SCRIPT_NAMES = new Set([
|
|
12
|
+
"preinstall",
|
|
13
|
+
"install",
|
|
14
|
+
"postinstall",
|
|
15
|
+
"prepublish",
|
|
16
|
+
"prepublishOnly",
|
|
17
|
+
"prepare",
|
|
18
|
+
]);
|
|
19
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
20
|
+
".cjs",
|
|
21
|
+
".cts",
|
|
22
|
+
".js",
|
|
23
|
+
".json",
|
|
24
|
+
".jsonc",
|
|
25
|
+
".jsx",
|
|
26
|
+
".mjs",
|
|
27
|
+
".md",
|
|
28
|
+
".ps1",
|
|
29
|
+
".py",
|
|
30
|
+
".rb",
|
|
31
|
+
".sh",
|
|
32
|
+
".toml",
|
|
33
|
+
".ts",
|
|
34
|
+
".tsx",
|
|
35
|
+
".txt",
|
|
36
|
+
".yaml",
|
|
37
|
+
".yml",
|
|
38
|
+
]);
|
|
39
|
+
const CONTENT_RULES = [
|
|
40
|
+
{
|
|
41
|
+
id: "prompt-ignore-previous-instructions",
|
|
42
|
+
severity: "high",
|
|
43
|
+
category: "prompt-injection",
|
|
44
|
+
message: "Contains instructions to ignore prior prompts or instructions.",
|
|
45
|
+
pattern: /\b(ignore|disregard|forget)\b[^.\n]{0,100}\b(previous|prior|earlier)\b[^.\n]{0,100}\b(instructions?|prompts?|messages?)\b/i,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "prompt-reveal-hidden-secrets",
|
|
49
|
+
severity: "critical",
|
|
50
|
+
category: "prompt-injection",
|
|
51
|
+
message: "Contains instructions to reveal hidden prompts or secrets.",
|
|
52
|
+
pattern: /\b(reveal|print|dump|show|exfiltrat(?:e|ion))\b[^.\n]{0,120}\b(system prompt|hidden instructions?|developer message|api key|token|secret|password)\b/i,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "prompt-bypass-guardrails",
|
|
56
|
+
severity: "high",
|
|
57
|
+
category: "prompt-injection",
|
|
58
|
+
message: "Contains instructions to bypass safety or security controls.",
|
|
59
|
+
pattern: /\b(bypass|disable|ignore)\b[^.\n]{0,100}\b(safety|security|guardrails|restrictions|policies)\b/i,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "remote-shell-pipe",
|
|
63
|
+
severity: "critical",
|
|
64
|
+
category: "malicious-code",
|
|
65
|
+
message: "Downloads remote content and pipes it directly into a shell.",
|
|
66
|
+
pattern: /\b(curl|wget)\b[^\n|]{0,200}\|\s*(sh|bash|zsh)\b/i,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "powershell-download-exec",
|
|
70
|
+
severity: "critical",
|
|
71
|
+
category: "malicious-code",
|
|
72
|
+
message: "Downloads remote content and executes it in PowerShell.",
|
|
73
|
+
pattern: /\b(Invoke-WebRequest|iwr|curl)\b[^\n|]{0,200}\|\s*(iex|Invoke-Expression)\b/i,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "powershell-encoded-command",
|
|
77
|
+
severity: "critical",
|
|
78
|
+
category: "malicious-code",
|
|
79
|
+
message: "Uses an encoded PowerShell command.",
|
|
80
|
+
pattern: /\bpowershell(?:\.exe)?\b[^\n]{0,120}\s-(?:enc|encodedcommand)\b/i,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "credential-exfiltration-language",
|
|
84
|
+
severity: "high",
|
|
85
|
+
category: "malicious-code",
|
|
86
|
+
message: "Contains language associated with credential or secret exfiltration.",
|
|
87
|
+
pattern: /\b(exfiltrat(?:e|ion)|harvest|steal)\b[^.\n]{0,120}\b(credentials?|tokens?|secrets?|ssh keys?|passwords?|cookies?)\b/i,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
export function resolveInstallAuditConfig(config) {
|
|
91
|
+
const installAudit = config?.security?.installAudit;
|
|
92
|
+
const allowlist = filterNonEmptyStrings(installAudit?.registryAllowlist) ??
|
|
93
|
+
filterNonEmptyStrings(installAudit?.registryWhitelist) ??
|
|
94
|
+
[];
|
|
95
|
+
return {
|
|
96
|
+
enabled: installAudit?.enabled ?? DEFAULT_INSTALL_AUDIT_CONFIG.enabled,
|
|
97
|
+
blockOnCritical: installAudit?.blockOnCritical ?? DEFAULT_INSTALL_AUDIT_CONFIG.blockOnCritical,
|
|
98
|
+
blockUnlistedRegistries: installAudit?.blockUnlistedRegistries ?? DEFAULT_INSTALL_AUDIT_CONFIG.blockUnlistedRegistries,
|
|
99
|
+
registryAllowlist: allowlist.map((entry) => entry.trim().toLowerCase()),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function enforceRegistryInstallPolicy(registryLabels, config, ref) {
|
|
103
|
+
const resolved = resolveInstallAuditConfig(config);
|
|
104
|
+
if (!resolved.blockUnlistedRegistries)
|
|
105
|
+
return;
|
|
106
|
+
if (resolved.registryAllowlist.length === 0) {
|
|
107
|
+
throw new Error(`Install blocked for ${ref}: no registries are allowlisted. Configure security.installAudit.registryAllowlist or disable security.installAudit.blockUnlistedRegistries.`);
|
|
108
|
+
}
|
|
109
|
+
const matched = registryLabels.some((label) => resolved.registryAllowlist.includes(label.toLowerCase()));
|
|
110
|
+
if (matched)
|
|
111
|
+
return;
|
|
112
|
+
throw new Error(`Install blocked for ${ref}: registry is not allowlisted. Allowed: ${resolved.registryAllowlist.join(", ")}. Seen: ${registryLabels.join(", ")}.`);
|
|
113
|
+
}
|
|
114
|
+
export function auditInstallCandidate(input) {
|
|
115
|
+
const resolved = resolveInstallAuditConfig(input.config);
|
|
116
|
+
if (!resolved.enabled) {
|
|
117
|
+
return {
|
|
118
|
+
enabled: false,
|
|
119
|
+
passed: true,
|
|
120
|
+
blocked: false,
|
|
121
|
+
registryLabels: [...input.registryLabels],
|
|
122
|
+
findings: [],
|
|
123
|
+
scannedFiles: 0,
|
|
124
|
+
scannedBytes: 0,
|
|
125
|
+
summary: buildSummary([]),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const findings = [];
|
|
129
|
+
const counters = { scannedFiles: 0, scannedBytes: 0 };
|
|
130
|
+
scanDirectory(input.rootDir, input.rootDir, findings, counters);
|
|
131
|
+
const summary = buildSummary(findings);
|
|
132
|
+
const blocked = resolved.blockOnCritical && summary.critical > 0;
|
|
133
|
+
return {
|
|
134
|
+
enabled: true,
|
|
135
|
+
passed: findings.length === 0,
|
|
136
|
+
blocked,
|
|
137
|
+
registryLabels: [...input.registryLabels],
|
|
138
|
+
findings,
|
|
139
|
+
scannedFiles: counters.scannedFiles,
|
|
140
|
+
scannedBytes: counters.scannedBytes,
|
|
141
|
+
summary,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function formatInstallAuditFailure(ref, report) {
|
|
145
|
+
const lines = [`Security audit failed for ${ref}.`, formatInstallAuditSummary(report)];
|
|
146
|
+
for (const finding of report.findings.slice(0, 5)) {
|
|
147
|
+
lines.push(`- [${finding.severity}] ${finding.message}${finding.file ? ` (${finding.file})` : ""}`);
|
|
148
|
+
}
|
|
149
|
+
if (report.findings.length > 5) {
|
|
150
|
+
lines.push(`- ${report.findings.length - 5} more finding(s) omitted`);
|
|
151
|
+
}
|
|
152
|
+
lines.push("Disable blocking with `security.installAudit.blockOnCritical = false`, or disable audits with `security.installAudit.enabled = false`.");
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
export function formatInstallAuditSummary(report) {
|
|
156
|
+
if (!report.enabled)
|
|
157
|
+
return "Audit: disabled";
|
|
158
|
+
const severitySummary = [];
|
|
159
|
+
if (report.summary.critical > 0)
|
|
160
|
+
severitySummary.push(`${report.summary.critical} critical`);
|
|
161
|
+
if (report.summary.high > 0)
|
|
162
|
+
severitySummary.push(`${report.summary.high} high`);
|
|
163
|
+
if (report.summary.moderate > 0)
|
|
164
|
+
severitySummary.push(`${report.summary.moderate} moderate`);
|
|
165
|
+
if (report.summary.low > 0)
|
|
166
|
+
severitySummary.push(`${report.summary.low} low`);
|
|
167
|
+
const detail = severitySummary.length > 0 ? severitySummary.join(", ") : "no findings";
|
|
168
|
+
return `Audit: ${report.blocked ? "blocked" : report.passed ? "passed" : "warnings"} (${detail}; scanned ${report.scannedFiles} file${report.scannedFiles === 1 ? "" : "s"})`;
|
|
169
|
+
}
|
|
170
|
+
export function deriveRegistryLabels(input) {
|
|
171
|
+
const labels = new Set();
|
|
172
|
+
labels.add(input.source);
|
|
173
|
+
if (input.source === "github")
|
|
174
|
+
labels.add("github.com");
|
|
175
|
+
if (input.source === "npm")
|
|
176
|
+
labels.add("npm");
|
|
177
|
+
addUrlLabels(labels, input.artifactUrl);
|
|
178
|
+
addUrlLabels(labels, input.gitUrl);
|
|
179
|
+
if (input.source === "github" && input.ref.startsWith("github:")) {
|
|
180
|
+
labels.add("github");
|
|
181
|
+
}
|
|
182
|
+
return [...labels];
|
|
183
|
+
}
|
|
184
|
+
function scanDirectory(dir, rootDir, findings, counters) {
|
|
185
|
+
let entries;
|
|
186
|
+
try {
|
|
187
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.name === ".git" || entry.name === "node_modules")
|
|
194
|
+
continue;
|
|
195
|
+
const fullPath = path.join(dir, entry.name);
|
|
196
|
+
if (entry.isDirectory()) {
|
|
197
|
+
scanDirectory(fullPath, rootDir, findings, counters);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!entry.isFile())
|
|
201
|
+
continue;
|
|
202
|
+
scanFile(fullPath, rootDir, findings, counters);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function scanFile(filePath, rootDir, findings, counters) {
|
|
206
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
207
|
+
const basename = path.basename(filePath).toLowerCase();
|
|
208
|
+
if (basename !== "package.json" && !TEXT_FILE_EXTENSIONS.has(ext))
|
|
209
|
+
return;
|
|
210
|
+
let fileSize;
|
|
211
|
+
try {
|
|
212
|
+
fileSize = fs.statSync(filePath).size;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const readSize = Math.min(fileSize, MAX_SCANNED_FILE_BYTES);
|
|
218
|
+
const buf = Buffer.alloc(readSize);
|
|
219
|
+
let bytesRead;
|
|
220
|
+
try {
|
|
221
|
+
const fd = fs.openSync(filePath, "r");
|
|
222
|
+
try {
|
|
223
|
+
bytesRead = fs.readSync(fd, buf, 0, readSize, 0);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
fs.closeSync(fd);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (bytesRead === 0)
|
|
233
|
+
return;
|
|
234
|
+
const bytes = buf.subarray(0, bytesRead);
|
|
235
|
+
if (bytes.includes(0))
|
|
236
|
+
return;
|
|
237
|
+
counters.scannedFiles += 1;
|
|
238
|
+
counters.scannedBytes += bytesRead;
|
|
239
|
+
const content = bytes.toString("utf8");
|
|
240
|
+
const relativePath = path.relative(rootDir, filePath) || path.basename(filePath);
|
|
241
|
+
const genericContent = basename === "package.json" ? stripPackageJsonScripts(content) : content;
|
|
242
|
+
for (const rule of CONTENT_RULES) {
|
|
243
|
+
const match = genericContent.match(rule.pattern);
|
|
244
|
+
if (!match)
|
|
245
|
+
continue;
|
|
246
|
+
findings.push({
|
|
247
|
+
id: rule.id,
|
|
248
|
+
severity: rule.severity,
|
|
249
|
+
category: rule.category,
|
|
250
|
+
message: rule.message,
|
|
251
|
+
file: relativePath,
|
|
252
|
+
snippet: clipSnippet(match[0]),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (basename === "package.json") {
|
|
256
|
+
scanPackageJson(content, relativePath, findings);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function stripPackageJsonScripts(content) {
|
|
260
|
+
let parsed;
|
|
261
|
+
try {
|
|
262
|
+
parsed = JSON.parse(content);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return content;
|
|
266
|
+
}
|
|
267
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
268
|
+
return content;
|
|
269
|
+
const packageJson = { ...parsed };
|
|
270
|
+
delete packageJson.scripts;
|
|
271
|
+
return JSON.stringify(packageJson, null, 2);
|
|
272
|
+
}
|
|
273
|
+
function scanPackageJson(content, relativePath, findings) {
|
|
274
|
+
let parsed;
|
|
275
|
+
try {
|
|
276
|
+
parsed = JSON.parse(content);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
282
|
+
return;
|
|
283
|
+
const scripts = parsed.scripts;
|
|
284
|
+
if (typeof scripts !== "object" || scripts === null || Array.isArray(scripts))
|
|
285
|
+
return;
|
|
286
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
287
|
+
if (!LIFECYCLE_SCRIPT_NAMES.has(name) || typeof command !== "string")
|
|
288
|
+
continue;
|
|
289
|
+
for (const rule of CONTENT_RULES) {
|
|
290
|
+
if (!rule.pattern.test(command))
|
|
291
|
+
continue;
|
|
292
|
+
findings.push({
|
|
293
|
+
id: `lifecycle-${name}-${rule.id}`,
|
|
294
|
+
severity: rule.severity,
|
|
295
|
+
category: "install-script",
|
|
296
|
+
message: `Lifecycle script "${name}" is suspicious: ${rule.message.toLowerCase()}`,
|
|
297
|
+
file: relativePath,
|
|
298
|
+
snippet: clipSnippet(command),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function clipSnippet(value) {
|
|
304
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
305
|
+
return normalized.length <= 140 ? normalized : `${normalized.slice(0, 137)}...`;
|
|
306
|
+
}
|
|
307
|
+
function buildSummary(findings) {
|
|
308
|
+
const summary = { low: 0, moderate: 0, high: 0, critical: 0, total: findings.length };
|
|
309
|
+
for (const finding of findings) {
|
|
310
|
+
summary[finding.severity] += 1;
|
|
311
|
+
}
|
|
312
|
+
return summary;
|
|
313
|
+
}
|
|
314
|
+
function addUrlLabels(labels, rawUrl) {
|
|
315
|
+
if (!rawUrl)
|
|
316
|
+
return;
|
|
317
|
+
try {
|
|
318
|
+
const parsed = new URL(rawUrl);
|
|
319
|
+
labels.add(parsed.hostname.toLowerCase());
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// Ignore non-URL refs (for example git@host:path)
|
|
323
|
+
}
|
|
324
|
+
}
|
package/dist/installed-kits.js
CHANGED
|
@@ -247,8 +247,8 @@ function tryResolveInstalledTarget(installed, target) {
|
|
|
247
247
|
return undefined;
|
|
248
248
|
}
|
|
249
249
|
function toInstalledEntry(status) {
|
|
250
|
-
// KitInstallStatus extends InstalledKitEntry; omit
|
|
251
|
-
const { extractedDir: _extractedDir, ...base } = status;
|
|
250
|
+
// KitInstallStatus extends InstalledKitEntry; omit transient install-only fields.
|
|
251
|
+
const { extractedDir: _extractedDir, audit: _audit, ...base } = status;
|
|
252
252
|
return base;
|
|
253
253
|
}
|
|
254
254
|
function toInstallStatus(status) {
|
package/dist/matchers.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - `extensionMatcher` (3) -- classifies any file by extension alone.
|
|
9
9
|
* Ensures every known file type is discoverable regardless of directory.
|
|
10
|
-
* - `directoryMatcher` (10) -- boosts specificity when
|
|
10
|
+
* - `directoryMatcher` (10) -- boosts specificity when an ancestor
|
|
11
11
|
* directory matches a known type name (e.g. `scripts/`, `agents/`).
|
|
12
12
|
* - `parentDirHintMatcher` (15) -- boosts specificity based on the
|
|
13
13
|
* immediate parent directory name.
|
|
@@ -43,31 +43,34 @@ export function extensionMatcher(ctx) {
|
|
|
43
43
|
}
|
|
44
44
|
// ── directoryMatcher (specificity: 10) ──────────────────────────────────────
|
|
45
45
|
/**
|
|
46
|
-
* Directory-based matcher that boosts specificity when
|
|
46
|
+
* Directory-based matcher that boosts specificity when an ancestor
|
|
47
47
|
* directory segment from the stash root matches a known type name.
|
|
48
|
+
*
|
|
49
|
+
* The first matching type-like ancestor wins. This preserves intuitive
|
|
50
|
+
* behavior for nested kit layouts such as `agent-stash/agents/blog/foo.md`
|
|
51
|
+
* while still honoring earlier type roots like `commands/agents/foo.md`.
|
|
48
52
|
*/
|
|
49
53
|
export function directoryMatcher(ctx) {
|
|
50
|
-
const topDir = ctx.ancestorDirs[0];
|
|
51
|
-
if (!topDir)
|
|
52
|
-
return null;
|
|
53
54
|
const ext = ctx.ext;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
55
|
+
for (const dir of ctx.ancestorDirs) {
|
|
56
|
+
if (dir === "scripts" && SCRIPT_EXTENSIONS.has(ext)) {
|
|
57
|
+
return { type: "script", specificity: 10, renderer: "script-source" };
|
|
58
|
+
}
|
|
59
|
+
if (dir === "skills" && ctx.fileName === "SKILL.md") {
|
|
60
|
+
return { type: "skill", specificity: 10, renderer: "skill-md" };
|
|
61
|
+
}
|
|
62
|
+
if (dir === "commands" && ext === ".md") {
|
|
63
|
+
return { type: "command", specificity: 10, renderer: "command-md" };
|
|
64
|
+
}
|
|
65
|
+
if (dir === "agents" && ext === ".md") {
|
|
66
|
+
return { type: "agent", specificity: 10, renderer: "agent-md" };
|
|
67
|
+
}
|
|
68
|
+
if (dir === "knowledge" && ext === ".md") {
|
|
69
|
+
return { type: "knowledge", specificity: 10, renderer: "knowledge-md" };
|
|
70
|
+
}
|
|
71
|
+
if (dir === "memories" && ext === ".md") {
|
|
72
|
+
return { type: "memory", specificity: 10, renderer: "memory-md" };
|
|
73
|
+
}
|
|
71
74
|
}
|
|
72
75
|
return null;
|
|
73
76
|
}
|
package/dist/registry-install.js
CHANGED
|
@@ -4,7 +4,8 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { TYPE_DIRS } from "./asset-spec";
|
|
6
6
|
import { fetchWithRetry, isWithin } from "./common";
|
|
7
|
-
import { loadConfig, saveConfig } from "./config";
|
|
7
|
+
import { loadConfig, loadUserConfig, saveConfig } from "./config";
|
|
8
|
+
import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
|
|
8
9
|
import { copyIncludedPaths, findNearestIncludeConfig } from "./kit-include";
|
|
9
10
|
import { getRegistryCacheDir as _getRegistryCacheDir } from "./paths";
|
|
10
11
|
import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "./registry-resolve";
|
|
@@ -12,13 +13,20 @@ import { warn } from "./warn";
|
|
|
12
13
|
const REGISTRY_STASH_DIR_NAMES = new Set(Object.values(TYPE_DIRS));
|
|
13
14
|
export async function installRegistryRef(ref, options) {
|
|
14
15
|
const parsed = parseRegistryRef(ref);
|
|
16
|
+
const config = loadConfig();
|
|
15
17
|
if (parsed.source === "local") {
|
|
16
|
-
return installLocalRegistryRef(parsed, options);
|
|
18
|
+
return installLocalRegistryRef(parsed, config, options);
|
|
17
19
|
}
|
|
18
20
|
if (parsed.source === "git") {
|
|
19
|
-
return installGitRegistryRef(parsed, options);
|
|
21
|
+
return installGitRegistryRef(parsed, config, options);
|
|
20
22
|
}
|
|
21
23
|
const resolved = await resolveRegistryArtifact(parsed);
|
|
24
|
+
const registryLabels = deriveRegistryLabels({
|
|
25
|
+
source: resolved.source,
|
|
26
|
+
ref: resolved.ref,
|
|
27
|
+
artifactUrl: resolved.artifactUrl,
|
|
28
|
+
});
|
|
29
|
+
enforceRegistryInstallPolicy(registryLabels, config, ref);
|
|
22
30
|
const installedAt = (options?.now ?? new Date()).toISOString();
|
|
23
31
|
const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
|
|
24
32
|
const cacheDir = buildInstallCacheDir(cacheRootDir, resolved.source, resolved.id, resolved.resolvedVersion ?? resolved.resolvedRevision);
|
|
@@ -30,6 +38,7 @@ export async function installRegistryRef(ref, options) {
|
|
|
30
38
|
const cachedStashRoot = detectStashRoot(extractedDir);
|
|
31
39
|
if (cachedStashRoot) {
|
|
32
40
|
const integrity = fs.existsSync(archivePath) ? await computeFileHash(archivePath) : undefined;
|
|
41
|
+
const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
|
|
33
42
|
return {
|
|
34
43
|
id: resolved.id,
|
|
35
44
|
source: resolved.source,
|
|
@@ -42,6 +51,7 @@ export async function installRegistryRef(ref, options) {
|
|
|
42
51
|
extractedDir,
|
|
43
52
|
stashRoot: cachedStashRoot,
|
|
44
53
|
integrity,
|
|
54
|
+
audit,
|
|
45
55
|
};
|
|
46
56
|
}
|
|
47
57
|
}
|
|
@@ -54,11 +64,13 @@ export async function installRegistryRef(ref, options) {
|
|
|
54
64
|
let provisionalKitRoot;
|
|
55
65
|
let installRoot;
|
|
56
66
|
let stashRoot;
|
|
67
|
+
let audit;
|
|
57
68
|
try {
|
|
58
69
|
await downloadArchive(resolved.artifactUrl, archivePath);
|
|
59
70
|
verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
|
|
60
71
|
integrity = await computeFileHash(archivePath);
|
|
61
72
|
extractTarGzSecure(archivePath, extractedDir);
|
|
73
|
+
audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
|
|
62
74
|
provisionalKitRoot = detectStashRoot(extractedDir);
|
|
63
75
|
installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
64
76
|
stashRoot = detectStashRoot(installRoot);
|
|
@@ -86,11 +98,18 @@ export async function installRegistryRef(ref, options) {
|
|
|
86
98
|
extractedDir,
|
|
87
99
|
stashRoot,
|
|
88
100
|
integrity,
|
|
101
|
+
audit,
|
|
89
102
|
};
|
|
90
103
|
}
|
|
91
|
-
async function installLocalRegistryRef(parsed, options) {
|
|
104
|
+
async function installLocalRegistryRef(parsed, config, options) {
|
|
92
105
|
const resolved = await resolveRegistryArtifact(parsed);
|
|
93
106
|
const installedAt = (options?.now ?? new Date()).toISOString();
|
|
107
|
+
const registryLabels = deriveRegistryLabels({
|
|
108
|
+
source: resolved.source,
|
|
109
|
+
ref: resolved.ref,
|
|
110
|
+
artifactUrl: resolved.artifactUrl,
|
|
111
|
+
});
|
|
112
|
+
const audit = runInstallAuditOrThrow(parsed.sourcePath, resolved.source, resolved.ref, registryLabels, config);
|
|
94
113
|
// For local directories, detect the stash root within the source path.
|
|
95
114
|
// If no nested stash is found, the source path itself is used.
|
|
96
115
|
const stashRoot = detectStashRoot(parsed.sourcePath);
|
|
@@ -105,10 +124,18 @@ async function installLocalRegistryRef(parsed, options) {
|
|
|
105
124
|
cacheDir: parsed.sourcePath,
|
|
106
125
|
extractedDir: parsed.sourcePath,
|
|
107
126
|
stashRoot,
|
|
127
|
+
audit,
|
|
108
128
|
};
|
|
109
129
|
}
|
|
110
|
-
async function installGitRegistryRef(parsed, options) {
|
|
130
|
+
async function installGitRegistryRef(parsed, config, options) {
|
|
111
131
|
const resolved = await resolveRegistryArtifact(parsed);
|
|
132
|
+
const registryLabels = deriveRegistryLabels({
|
|
133
|
+
source: resolved.source,
|
|
134
|
+
ref: resolved.ref,
|
|
135
|
+
artifactUrl: resolved.artifactUrl,
|
|
136
|
+
gitUrl: parsed.url,
|
|
137
|
+
});
|
|
138
|
+
enforceRegistryInstallPolicy(registryLabels, config, parsed.ref);
|
|
112
139
|
const installedAt = (options?.now ?? new Date()).toISOString();
|
|
113
140
|
const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheRootDir();
|
|
114
141
|
const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, resolved.resolvedRevision);
|
|
@@ -121,6 +148,7 @@ async function installGitRegistryRef(parsed, options) {
|
|
|
121
148
|
const installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
122
149
|
const stashRoot = detectStashRoot(installRoot);
|
|
123
150
|
if (stashRoot) {
|
|
151
|
+
const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
|
|
124
152
|
return {
|
|
125
153
|
id: resolved.id,
|
|
126
154
|
source: resolved.source,
|
|
@@ -132,6 +160,7 @@ async function installGitRegistryRef(parsed, options) {
|
|
|
132
160
|
cacheDir,
|
|
133
161
|
extractedDir,
|
|
134
162
|
stashRoot,
|
|
163
|
+
audit,
|
|
135
164
|
};
|
|
136
165
|
}
|
|
137
166
|
}
|
|
@@ -147,6 +176,7 @@ async function installGitRegistryRef(parsed, options) {
|
|
|
147
176
|
let provisionalKitRoot;
|
|
148
177
|
let installRoot;
|
|
149
178
|
let stashRoot;
|
|
179
|
+
let audit;
|
|
150
180
|
try {
|
|
151
181
|
const cloneArgs = ["clone", "--depth", "1"];
|
|
152
182
|
if (parsed.requestedRef) {
|
|
@@ -163,6 +193,7 @@ async function installGitRegistryRef(parsed, options) {
|
|
|
163
193
|
copyDirectoryContents(cloneDir, extractedDir);
|
|
164
194
|
// Clean up the clone dir
|
|
165
195
|
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
196
|
+
audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
|
|
166
197
|
provisionalKitRoot = detectStashRoot(extractedDir);
|
|
167
198
|
installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
168
199
|
stashRoot = detectStashRoot(installRoot);
|
|
@@ -189,10 +220,11 @@ async function installGitRegistryRef(parsed, options) {
|
|
|
189
220
|
cacheDir,
|
|
190
221
|
extractedDir,
|
|
191
222
|
stashRoot,
|
|
223
|
+
audit,
|
|
192
224
|
};
|
|
193
225
|
}
|
|
194
226
|
export function upsertInstalledRegistryEntry(entry) {
|
|
195
|
-
const current =
|
|
227
|
+
const current = loadUserConfig();
|
|
196
228
|
const currentInstalled = current.installed ?? [];
|
|
197
229
|
const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id);
|
|
198
230
|
const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)];
|
|
@@ -204,7 +236,7 @@ export function upsertInstalledRegistryEntry(entry) {
|
|
|
204
236
|
return nextConfig;
|
|
205
237
|
}
|
|
206
238
|
export function removeInstalledRegistryEntry(id) {
|
|
207
|
-
const current =
|
|
239
|
+
const current = loadUserConfig();
|
|
208
240
|
const currentInstalled = current.installed ?? [];
|
|
209
241
|
const nextInstalled = currentInstalled.filter((item) => item.id !== id);
|
|
210
242
|
const nextConfig = {
|
|
@@ -462,3 +494,10 @@ async function computeFileHash(filePath) {
|
|
|
462
494
|
const hash = createHash("sha256").update(data).digest("hex");
|
|
463
495
|
return `sha256:${hash}`;
|
|
464
496
|
}
|
|
497
|
+
function runInstallAuditOrThrow(rootDir, source, ref, registryLabels, config) {
|
|
498
|
+
const audit = auditInstallCandidate({ rootDir, source, ref, registryLabels, config });
|
|
499
|
+
if (audit.blocked) {
|
|
500
|
+
throw new Error(formatInstallAuditFailure(ref, audit));
|
|
501
|
+
}
|
|
502
|
+
return audit;
|
|
503
|
+
}
|
package/dist/setup.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import * as p from "@clack/prompts";
|
|
10
10
|
import { isHttpUrl } from "./common";
|
|
11
|
-
import { DEFAULT_CONFIG, getConfigPath,
|
|
11
|
+
import { DEFAULT_CONFIG, getConfigPath, loadUserConfig, saveConfig } from "./config";
|
|
12
12
|
import { closeDatabase, isVecAvailable, openDatabase } from "./db";
|
|
13
13
|
import { detectAgentPlatforms, detectOllama, detectOpenViking } from "./detect";
|
|
14
14
|
import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "./embedder";
|
|
@@ -568,7 +568,7 @@ async function stepAgentPlatforms(current) {
|
|
|
568
568
|
// ── Main Wizard ─────────────────────────────────────────────────────────────
|
|
569
569
|
export async function runSetupWizard() {
|
|
570
570
|
p.intro("akm setup");
|
|
571
|
-
const current =
|
|
571
|
+
const current = loadUserConfig();
|
|
572
572
|
const configPath = getConfigPath();
|
|
573
573
|
// Step 1: Stash directory
|
|
574
574
|
p.log.step("Step 1: Stash Directory");
|
package/dist/stash-add.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isHttpUrl, resolveStashDir } from "./common";
|
|
4
|
-
import { loadConfig, saveConfig } from "./config";
|
|
4
|
+
import { loadConfig, loadUserConfig, saveConfig } from "./config";
|
|
5
5
|
import { UsageError } from "./errors";
|
|
6
6
|
import { akmIndex } from "./indexer";
|
|
7
7
|
import { upsertLockEntry } from "./lockfile";
|
|
@@ -36,7 +36,7 @@ export async function akmAdd(input) {
|
|
|
36
36
|
async function addLocalStashSource(ref, sourcePath, stashDir) {
|
|
37
37
|
const stashRoot = detectStashRoot(sourcePath);
|
|
38
38
|
const resolvedPath = path.resolve(stashRoot);
|
|
39
|
-
const config =
|
|
39
|
+
const config = loadUserConfig();
|
|
40
40
|
// Check for duplicates in stashes[]
|
|
41
41
|
const stashes = [...(config.stashes ?? [])];
|
|
42
42
|
const existing = stashes.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
|
|
@@ -75,7 +75,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir) {
|
|
|
75
75
|
}
|
|
76
76
|
async function addWebsiteStashSource(ref, stashDir, name, options) {
|
|
77
77
|
const normalizedUrl = validateWebsiteInputUrl(ref);
|
|
78
|
-
const config =
|
|
78
|
+
const config = loadUserConfig();
|
|
79
79
|
const stashes = [...(config.stashes ?? [])];
|
|
80
80
|
let entry = stashes.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
|
|
81
81
|
if (!entry) {
|
|
@@ -167,6 +167,7 @@ async function addRegistryKit(ref, stashDir) {
|
|
|
167
167
|
cacheDir: installed.cacheDir,
|
|
168
168
|
extractedDir: installed.extractedDir,
|
|
169
169
|
installedAt: installed.installedAt,
|
|
170
|
+
audit: installed.audit,
|
|
170
171
|
},
|
|
171
172
|
config: {
|
|
172
173
|
stashCount: config.stashes?.length ?? 0,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { loadConfig, saveConfig } from "./config";
|
|
2
|
+
import { loadConfig, loadUserConfig, saveConfig } from "./config";
|
|
3
3
|
import { UsageError } from "./errors";
|
|
4
4
|
import { resolveStashSources } from "./search-source";
|
|
5
5
|
// ── Operations ──────────────────────────────────────────────────────────────
|
|
@@ -12,7 +12,7 @@ import { resolveStashSources } from "./search-source";
|
|
|
12
12
|
*/
|
|
13
13
|
export function addStash(opts) {
|
|
14
14
|
const { target, name, providerType, options: providerOptions } = opts;
|
|
15
|
-
const config =
|
|
15
|
+
const config = loadUserConfig();
|
|
16
16
|
const stashes = [...(config.stashes ?? [])];
|
|
17
17
|
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
18
18
|
let entry;
|
|
@@ -49,7 +49,7 @@ export function addStash(opts) {
|
|
|
49
49
|
* Match priority: URL > path > name (most specific first).
|
|
50
50
|
*/
|
|
51
51
|
export function removeStash(target) {
|
|
52
|
-
const config =
|
|
52
|
+
const config = loadUserConfig();
|
|
53
53
|
const stashes = [...(config.stashes ?? [])];
|
|
54
54
|
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
55
55
|
const resolvedPath = !isUrl ? path.resolve(target) : undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Kit Manager) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
|
|
6
6
|
"keywords": [
|