akm-cli 0.4.0 → 0.5.0-rc1
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/asset-registry.js +7 -0
- package/dist/asset-spec.js +35 -0
- package/dist/cli.js +1383 -25
- package/dist/completions.js +2 -2
- package/dist/config-cli.js +41 -0
- package/dist/config.js +62 -0
- package/dist/embedder.js +26 -4
- package/dist/file-context.js +2 -1
- package/dist/github.js +20 -1
- package/dist/indexer.js +55 -5
- package/dist/init.js +11 -0
- package/dist/install-audit.js +53 -8
- package/dist/installed-kits.js +2 -0
- package/dist/llm.js +64 -23
- package/dist/matchers.js +56 -4
- package/dist/metadata.js +68 -4
- package/dist/paths.js +3 -0
- package/dist/registry-install.js +36 -7
- package/dist/registry-resolve.js +25 -0
- package/dist/renderers.js +182 -2
- package/dist/search-fields.js +4 -0
- package/dist/search-source.js +12 -8
- package/dist/setup.js +158 -33
- package/dist/stash-add.js +84 -11
- package/dist/stash-providers/git.js +182 -44
- package/dist/stash-show.js +56 -1
- package/dist/stash-source-manage.js +14 -4
- package/dist/templates/wiki-templates.js +100 -0
- package/dist/vault.js +290 -0
- package/dist/wiki.js +886 -0
- package/dist/workflow-authoring.js +131 -0
- package/dist/workflow-cli.js +44 -0
- package/dist/workflow-db.js +55 -0
- package/dist/workflow-markdown.js +251 -0
- package/dist/workflow-runs.js +364 -0
- package/package.json +2 -1
- package/LICENSE +0 -374
package/dist/llm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchWithTimeout } from "./common";
|
|
2
|
-
async function chatCompletion(config, messages) {
|
|
2
|
+
export async function chatCompletion(config, messages, options) {
|
|
3
3
|
const headers = { "Content-Type": "application/json" };
|
|
4
4
|
if (config.apiKey) {
|
|
5
5
|
headers.Authorization = `Bearer ${config.apiKey}`;
|
|
@@ -10,8 +10,8 @@ async function chatCompletion(config, messages) {
|
|
|
10
10
|
body: JSON.stringify({
|
|
11
11
|
model: config.model,
|
|
12
12
|
messages,
|
|
13
|
-
temperature: config.temperature ?? 0.3,
|
|
14
|
-
max_tokens: config.maxTokens ?? 512,
|
|
13
|
+
temperature: options?.temperature ?? config.temperature ?? 0.3,
|
|
14
|
+
max_tokens: options?.maxTokens ?? config.maxTokens ?? 512,
|
|
15
15
|
}),
|
|
16
16
|
});
|
|
17
17
|
if (!response.ok) {
|
|
@@ -21,6 +21,23 @@ async function chatCompletion(config, messages) {
|
|
|
21
21
|
const json = (await response.json());
|
|
22
22
|
return json.choices?.[0]?.message?.content?.trim() ?? "";
|
|
23
23
|
}
|
|
24
|
+
/** Strip leading/trailing markdown code fences from an LLM response. */
|
|
25
|
+
function stripJsonFences(raw) {
|
|
26
|
+
return raw
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/^```(?:json)?\s*\n?/i, "")
|
|
29
|
+
.replace(/\n?```\s*$/i, "")
|
|
30
|
+
.trim();
|
|
31
|
+
}
|
|
32
|
+
/** Parse a possibly-fenced JSON response. Returns undefined if invalid. */
|
|
33
|
+
export function parseJsonResponse(raw) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(stripJsonFences(raw));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
24
41
|
// ── Metadata Enhancement ────────────────────────────────────────────────────
|
|
25
42
|
const SYSTEM_PROMPT = `You are a metadata generator for a developer asset registry. Given a script/skill/command/agent entry, generate improved metadata. Respond with ONLY valid JSON, no markdown fencing.`;
|
|
26
43
|
/**
|
|
@@ -50,28 +67,22 @@ Return ONLY the JSON object, no explanation.`;
|
|
|
50
67
|
{ role: "system", content: SYSTEM_PROMPT },
|
|
51
68
|
{ role: "user", content: userPrompt },
|
|
52
69
|
]);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const cleaned = raw.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "");
|
|
56
|
-
const parsed = JSON.parse(cleaned);
|
|
57
|
-
const result = {};
|
|
58
|
-
if (typeof parsed.description === "string" && parsed.description) {
|
|
59
|
-
result.description = parsed.description;
|
|
60
|
-
}
|
|
61
|
-
if (Array.isArray(parsed.searchHints)) {
|
|
62
|
-
result.searchHints = parsed.searchHints
|
|
63
|
-
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
64
|
-
.slice(0, 8);
|
|
65
|
-
}
|
|
66
|
-
if (Array.isArray(parsed.tags)) {
|
|
67
|
-
result.tags = parsed.tags.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 10);
|
|
68
|
-
}
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// LLM returned unparseable output, return empty
|
|
70
|
+
const parsed = parseJsonResponse(raw);
|
|
71
|
+
if (!parsed)
|
|
73
72
|
return {};
|
|
73
|
+
const result = {};
|
|
74
|
+
if (typeof parsed.description === "string" && parsed.description) {
|
|
75
|
+
result.description = parsed.description;
|
|
74
76
|
}
|
|
77
|
+
if (Array.isArray(parsed.searchHints)) {
|
|
78
|
+
result.searchHints = parsed.searchHints
|
|
79
|
+
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
80
|
+
.slice(0, 8);
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(parsed.tags)) {
|
|
83
|
+
result.tags = parsed.tags.filter((s) => typeof s === "string" && s.trim().length > 0).slice(0, 10);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
75
86
|
}
|
|
76
87
|
/**
|
|
77
88
|
* Check if the LLM endpoint is reachable.
|
|
@@ -85,3 +96,33 @@ export async function isLlmAvailable(config) {
|
|
|
85
96
|
return false;
|
|
86
97
|
}
|
|
87
98
|
}
|
|
99
|
+
// ── Capability probe ────────────────────────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Ask the model to emit a strict JSON object so we know whether the knowledge
|
|
102
|
+
* wiki ingest/lint flows can rely on structured output. Failure is non-fatal —
|
|
103
|
+
* the caller can fall back to assist-only mode.
|
|
104
|
+
*/
|
|
105
|
+
export async function probeLlmCapabilities(config) {
|
|
106
|
+
try {
|
|
107
|
+
const raw = await chatCompletion(config, [
|
|
108
|
+
{
|
|
109
|
+
role: "system",
|
|
110
|
+
content: "You return only valid JSON. No prose, no markdown fences.",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
role: "user",
|
|
114
|
+
content: 'Return exactly this JSON object and nothing else: {"ok": true, "ingest": true, "lint": true}',
|
|
115
|
+
},
|
|
116
|
+
], { maxTokens: 64, temperature: 0 });
|
|
117
|
+
if (!raw)
|
|
118
|
+
return { reachable: false, structuredOutput: false, error: "empty response" };
|
|
119
|
+
const parsed = parseJsonResponse(raw);
|
|
120
|
+
return {
|
|
121
|
+
reachable: true,
|
|
122
|
+
structuredOutput: Boolean(parsed && parsed.ok === true),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
return { reachable: false, structuredOutput: false, error: err instanceof Error ? err.message : String(err) };
|
|
127
|
+
}
|
|
128
|
+
}
|
package/dist/matchers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in asset matchers for the akm file classification system.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Five matchers are registered at module load time, each at a different
|
|
5
5
|
* specificity level. Extension and content determine type; directories are
|
|
6
6
|
* optional specificity boosts, not requirements.
|
|
7
7
|
*
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* and body content for agent/command signals; falls back to "knowledge"
|
|
16
16
|
* at specificity 5 when no signals are found. Command signals (`agent`
|
|
17
17
|
* frontmatter, `$ARGUMENTS`/`$1`-`$3` placeholders) return 18.
|
|
18
|
+
* - `wikiMatcher` (20) -- classifies any `.md` under `wikis/<name>/…` as
|
|
19
|
+
* `wiki`. Registered last so the later-wins tiebreaker beats agent at 20.
|
|
18
20
|
*/
|
|
19
21
|
import { SCRIPT_EXTENSIONS } from "./asset-spec";
|
|
20
22
|
import { registerMatcher } from "./file-context";
|
|
@@ -32,7 +34,9 @@ import { registerMatcher } from "./file-context";
|
|
|
32
34
|
export function extensionMatcher(ctx) {
|
|
33
35
|
// SKILL.md is a skill regardless of location — high specificity beats
|
|
34
36
|
// smartMdMatcher's knowledge fallback and all directory-based matchers.
|
|
35
|
-
|
|
37
|
+
// Exception: files under wikis/<name>/… are always wiki pages; the wiki
|
|
38
|
+
// directory is an authoritative signal that outranks the filename.
|
|
39
|
+
if (ctx.fileName === "SKILL.md" && !ctx.ancestorDirs.includes("wikis")) {
|
|
36
40
|
return { type: "skill", specificity: 25, renderer: "skill-md" };
|
|
37
41
|
}
|
|
38
42
|
// Known script extensions (excluding .md, handled by smartMdMatcher)
|
|
@@ -68,9 +72,15 @@ export function directoryMatcher(ctx) {
|
|
|
68
72
|
if (dir === "knowledge" && ext === ".md") {
|
|
69
73
|
return { type: "knowledge", specificity: 10, renderer: "knowledge-md" };
|
|
70
74
|
}
|
|
75
|
+
if (dir === "workflows" && ext === ".md") {
|
|
76
|
+
return { type: "workflow", specificity: 10, renderer: "workflow-md" };
|
|
77
|
+
}
|
|
71
78
|
if (dir === "memories" && ext === ".md") {
|
|
72
79
|
return { type: "memory", specificity: 10, renderer: "memory-md" };
|
|
73
80
|
}
|
|
81
|
+
if (dir === "vaults" && (ctx.fileName === ".env" || ctx.fileName.endsWith(".env"))) {
|
|
82
|
+
return { type: "vault", specificity: 10, renderer: "vault-env" };
|
|
83
|
+
}
|
|
74
84
|
}
|
|
75
85
|
return null;
|
|
76
86
|
}
|
|
@@ -98,9 +108,15 @@ export function parentDirHintMatcher(ctx) {
|
|
|
98
108
|
if (parentDir === "knowledge" && ext === ".md") {
|
|
99
109
|
return { type: "knowledge", specificity: 15, renderer: "knowledge-md" };
|
|
100
110
|
}
|
|
111
|
+
if (parentDir === "workflows" && ext === ".md") {
|
|
112
|
+
return { type: "workflow", specificity: 15, renderer: "workflow-md" };
|
|
113
|
+
}
|
|
101
114
|
if (parentDir === "memories" && ext === ".md") {
|
|
102
115
|
return { type: "memory", specificity: 15, renderer: "memory-md" };
|
|
103
116
|
}
|
|
117
|
+
if (parentDir === "vaults" && (fileName === ".env" || fileName.endsWith(".env"))) {
|
|
118
|
+
return { type: "vault", specificity: 15, renderer: "vault-env" };
|
|
119
|
+
}
|
|
104
120
|
return null;
|
|
105
121
|
}
|
|
106
122
|
// ── smartMdMatcher (specificity: 20 / 18 / 8 / 5) ──────────────────────────
|
|
@@ -123,6 +139,14 @@ const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
|
|
|
123
139
|
export function smartMdMatcher(ctx) {
|
|
124
140
|
if (ctx.ext !== ".md")
|
|
125
141
|
return null;
|
|
142
|
+
const body = ctx.content();
|
|
143
|
+
const hasWorkflowSignals = /^#\s+Workflow:\s+/m.test(body) &&
|
|
144
|
+
/^##\s+Step:\s+/m.test(body) &&
|
|
145
|
+
/^Step ID:\s+/m.test(body) &&
|
|
146
|
+
/^###\s+Instructions\s*$/m.test(body);
|
|
147
|
+
if (hasWorkflowSignals) {
|
|
148
|
+
return { type: "workflow", specificity: 19, renderer: "workflow-md" };
|
|
149
|
+
}
|
|
126
150
|
const fm = ctx.frontmatter();
|
|
127
151
|
if (fm) {
|
|
128
152
|
// Agent-exclusive indicators: toolPolicy or tools
|
|
@@ -138,7 +162,6 @@ export function smartMdMatcher(ctx) {
|
|
|
138
162
|
}
|
|
139
163
|
// Command signal: body contains $ARGUMENTS or $1/$2/$3 placeholders.
|
|
140
164
|
// These are definitively command template patterns (OpenCode convention).
|
|
141
|
-
const body = ctx.content();
|
|
142
165
|
if (COMMAND_PLACEHOLDER_RE.test(body)) {
|
|
143
166
|
return { type: "command", specificity: 18, renderer: "command-md" };
|
|
144
167
|
}
|
|
@@ -154,9 +177,38 @@ export function smartMdMatcher(ctx) {
|
|
|
154
177
|
// Weak fallback: any .md file is assumed to be knowledge
|
|
155
178
|
return { type: "knowledge", specificity: 5, renderer: "knowledge-md" };
|
|
156
179
|
}
|
|
180
|
+
// ── wikiMatcher (specificity: 20) ──────────────────────────────────────────
|
|
181
|
+
/**
|
|
182
|
+
* Classify any `.md` file that lives under `wikis/<name>/…` as `wiki`.
|
|
183
|
+
*
|
|
184
|
+
* Registered AFTER `smartMdMatcher` so the registered-later-wins tiebreaker
|
|
185
|
+
* puts wiki ahead of agent at specificity 20. That means a wiki page with
|
|
186
|
+
* agent-style frontmatter (e.g. `tools:`) still classifies as a wiki page,
|
|
187
|
+
* not an agent. That's intentional — the directory is the authoritative
|
|
188
|
+
* signal: files under `wikis/` are wiki content.
|
|
189
|
+
*
|
|
190
|
+
* Requires at least one path segment after `wikis/` (the wiki name) — a
|
|
191
|
+
* stray `.md` at the bare `wikis/` root is not a wiki page.
|
|
192
|
+
*/
|
|
193
|
+
export function wikiMatcher(ctx) {
|
|
194
|
+
if (ctx.ext !== ".md")
|
|
195
|
+
return null;
|
|
196
|
+
const idx = ctx.ancestorDirs.indexOf("wikis");
|
|
197
|
+
if (idx < 0)
|
|
198
|
+
return null;
|
|
199
|
+
if (idx + 1 >= ctx.ancestorDirs.length)
|
|
200
|
+
return null;
|
|
201
|
+
return { type: "wiki", specificity: 20, renderer: "wiki-md" };
|
|
202
|
+
}
|
|
157
203
|
// ── Registration ────────────────────────────────────────────────────────────
|
|
158
204
|
/** All built-in matchers in registration order (later wins ties). */
|
|
159
|
-
const builtinMatchers = [
|
|
205
|
+
const builtinMatchers = [
|
|
206
|
+
extensionMatcher,
|
|
207
|
+
directoryMatcher,
|
|
208
|
+
parentDirHintMatcher,
|
|
209
|
+
smartMdMatcher,
|
|
210
|
+
wikiMatcher,
|
|
211
|
+
];
|
|
160
212
|
/**
|
|
161
213
|
* Register all built-in matchers with the file-context registry.
|
|
162
214
|
* Called once from the CLI entry point (or ensureBuiltinsRegistered).
|
package/dist/metadata.js
CHANGED
|
@@ -133,6 +133,30 @@ export function validateStashEntry(entry) {
|
|
|
133
133
|
result.cwd = e.cwd.trim();
|
|
134
134
|
if (typeof e.fileSize === "number" && Number.isFinite(e.fileSize) && e.fileSize >= 0)
|
|
135
135
|
result.fileSize = e.fileSize;
|
|
136
|
+
if (e.wikiRole === "schema" ||
|
|
137
|
+
e.wikiRole === "index" ||
|
|
138
|
+
e.wikiRole === "log" ||
|
|
139
|
+
e.wikiRole === "raw" ||
|
|
140
|
+
e.wikiRole === "page") {
|
|
141
|
+
result.wikiRole = e.wikiRole;
|
|
142
|
+
}
|
|
143
|
+
if (typeof e.pageKind === "string" && e.pageKind.trim().length > 0) {
|
|
144
|
+
result.pageKind = e.pageKind.trim();
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(e.xrefs)) {
|
|
147
|
+
const filtered = e.xrefs
|
|
148
|
+
.filter((x) => typeof x === "string" && x.trim().length > 0)
|
|
149
|
+
.map((x) => x.trim());
|
|
150
|
+
if (filtered.length > 0)
|
|
151
|
+
result.xrefs = filtered;
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(e.sources)) {
|
|
154
|
+
const filtered = e.sources
|
|
155
|
+
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
156
|
+
.map((s) => s.trim());
|
|
157
|
+
if (filtered.length > 0)
|
|
158
|
+
result.sources = filtered;
|
|
159
|
+
}
|
|
136
160
|
if (Array.isArray(e.parameters)) {
|
|
137
161
|
const validated = e.parameters
|
|
138
162
|
.filter((p) => {
|
|
@@ -196,6 +220,36 @@ export function extractCommandParameters(template) {
|
|
|
196
220
|
}
|
|
197
221
|
return params.length > 0 ? params : undefined;
|
|
198
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Extract wiki frontmatter fields (wikiRole, pageKind, xrefs, sources) from a parsed
|
|
225
|
+
* frontmatter block and apply them to the entry. Tolerates missing or malformed values.
|
|
226
|
+
*/
|
|
227
|
+
export function applyWikiFrontmatter(entry, fmData) {
|
|
228
|
+
const role = fmData.wikiRole;
|
|
229
|
+
if (role === "schema" || role === "index" || role === "log" || role === "raw" || role === "page") {
|
|
230
|
+
entry.wikiRole = role;
|
|
231
|
+
}
|
|
232
|
+
const pageKind = fmData.pageKind;
|
|
233
|
+
if (typeof pageKind === "string" && pageKind.trim().length > 0) {
|
|
234
|
+
entry.pageKind = pageKind.trim();
|
|
235
|
+
}
|
|
236
|
+
const xrefs = fmData.xrefs;
|
|
237
|
+
if (Array.isArray(xrefs)) {
|
|
238
|
+
const filtered = xrefs
|
|
239
|
+
.filter((x) => typeof x === "string" && x.trim().length > 0)
|
|
240
|
+
.map((x) => x.trim());
|
|
241
|
+
if (filtered.length > 0)
|
|
242
|
+
entry.xrefs = filtered;
|
|
243
|
+
}
|
|
244
|
+
const sources = fmData.sources;
|
|
245
|
+
if (Array.isArray(sources)) {
|
|
246
|
+
const filtered = sources
|
|
247
|
+
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
248
|
+
.map((s) => s.trim());
|
|
249
|
+
if (filtered.length > 0)
|
|
250
|
+
entry.sources = filtered;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
199
253
|
/**
|
|
200
254
|
* Extract `@param` JSDoc tags from a script file's leading comment block.
|
|
201
255
|
*
|
|
@@ -316,6 +370,8 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
|
|
|
316
370
|
const fmParams = extractFrontmatterParameters(parsed.data);
|
|
317
371
|
if (fmParams)
|
|
318
372
|
entry.parameters = fmParams;
|
|
373
|
+
// Pass wiki-pattern frontmatter through onto the entry
|
|
374
|
+
applyWikiFrontmatter(entry, parsed.data);
|
|
319
375
|
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
320
376
|
if (entry.type === "command") {
|
|
321
377
|
const cmdParams = extractCommandParameters(parsed.content);
|
|
@@ -324,8 +380,11 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
|
|
|
324
380
|
}
|
|
325
381
|
}
|
|
326
382
|
}
|
|
327
|
-
// Extract @param from script files
|
|
328
|
-
|
|
383
|
+
// Extract @param from script files.
|
|
384
|
+
// Vault files (.env) are deliberately excluded — their contents are secrets
|
|
385
|
+
// and must never be parsed for @param or any other metadata that could
|
|
386
|
+
// embed a value into the entry.
|
|
387
|
+
if (ext !== ".md" && assetType !== "vault") {
|
|
329
388
|
const scriptParams = extractScriptParameters(file);
|
|
330
389
|
if (scriptParams)
|
|
331
390
|
entry.parameters = scriptParams;
|
|
@@ -418,6 +477,8 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
418
477
|
const fmParams = extractFrontmatterParameters(parsed.data);
|
|
419
478
|
if (fmParams)
|
|
420
479
|
entry.parameters = fmParams;
|
|
480
|
+
// Pass wiki-pattern frontmatter through onto the entry
|
|
481
|
+
applyWikiFrontmatter(entry, parsed.data);
|
|
421
482
|
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
422
483
|
if (entry.type === "command") {
|
|
423
484
|
const cmdParams = extractCommandParameters(parsed.content);
|
|
@@ -426,8 +487,11 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
426
487
|
}
|
|
427
488
|
}
|
|
428
489
|
}
|
|
429
|
-
// Extract @param from script files
|
|
430
|
-
|
|
490
|
+
// Extract @param from script files.
|
|
491
|
+
// Vault files (.env) are deliberately excluded — their contents are secrets
|
|
492
|
+
// and must never be parsed for @param or any other metadata that could
|
|
493
|
+
// embed a value into the entry.
|
|
494
|
+
if (ext !== ".md" && assetType !== "vault") {
|
|
431
495
|
const scriptParams = extractScriptParameters(file, ctx.content());
|
|
432
496
|
if (scriptParams)
|
|
433
497
|
entry.parameters = scriptParams;
|
package/dist/paths.js
CHANGED
|
@@ -68,6 +68,9 @@ export function getCacheDir() {
|
|
|
68
68
|
export function getDbPath() {
|
|
69
69
|
return path.join(getCacheDir(), "index.db");
|
|
70
70
|
}
|
|
71
|
+
export function getWorkflowDbPath() {
|
|
72
|
+
return path.join(getCacheDir(), "workflow.db");
|
|
73
|
+
}
|
|
71
74
|
export function getSemanticStatusPath() {
|
|
72
75
|
return path.join(getCacheDir(), "semantic-status.json");
|
|
73
76
|
}
|
package/dist/registry-install.js
CHANGED
|
@@ -20,6 +20,9 @@ export async function installRegistryRef(ref, options) {
|
|
|
20
20
|
if (parsed.source === "git") {
|
|
21
21
|
return installGitRegistryRef(parsed, config, options);
|
|
22
22
|
}
|
|
23
|
+
if (parsed.source === "github") {
|
|
24
|
+
return installGithubRegistryRef(parsed, config, options);
|
|
25
|
+
}
|
|
23
26
|
const resolved = await resolveRegistryArtifact(parsed);
|
|
24
27
|
const registryLabels = deriveRegistryLabels({
|
|
25
28
|
source: resolved.source,
|
|
@@ -38,7 +41,7 @@ export async function installRegistryRef(ref, options) {
|
|
|
38
41
|
const cachedStashRoot = detectStashRoot(extractedDir);
|
|
39
42
|
if (cachedStashRoot) {
|
|
40
43
|
const integrity = fs.existsSync(archivePath) ? await computeFileHash(archivePath) : undefined;
|
|
41
|
-
const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
|
|
44
|
+
const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
|
|
42
45
|
return {
|
|
43
46
|
id: resolved.id,
|
|
44
47
|
source: resolved.source,
|
|
@@ -51,6 +54,7 @@ export async function installRegistryRef(ref, options) {
|
|
|
51
54
|
extractedDir,
|
|
52
55
|
stashRoot: cachedStashRoot,
|
|
53
56
|
integrity,
|
|
57
|
+
writable: options?.writable,
|
|
54
58
|
audit,
|
|
55
59
|
};
|
|
56
60
|
}
|
|
@@ -70,7 +74,7 @@ export async function installRegistryRef(ref, options) {
|
|
|
70
74
|
verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
|
|
71
75
|
integrity = await computeFileHash(archivePath);
|
|
72
76
|
extractTarGzSecure(archivePath, extractedDir);
|
|
73
|
-
audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
|
|
77
|
+
audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
|
|
74
78
|
provisionalKitRoot = detectStashRoot(extractedDir);
|
|
75
79
|
installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
76
80
|
stashRoot = detectStashRoot(installRoot);
|
|
@@ -98,9 +102,24 @@ export async function installRegistryRef(ref, options) {
|
|
|
98
102
|
extractedDir,
|
|
99
103
|
stashRoot,
|
|
100
104
|
integrity,
|
|
105
|
+
writable: options?.writable,
|
|
101
106
|
audit,
|
|
102
107
|
};
|
|
103
108
|
}
|
|
109
|
+
async function installGithubRegistryRef(parsed, config, options) {
|
|
110
|
+
const gitParsed = {
|
|
111
|
+
source: "git",
|
|
112
|
+
ref: parsed.ref,
|
|
113
|
+
id: parsed.id,
|
|
114
|
+
url: `https://github.com/${parsed.owner}/${parsed.repo}.git`,
|
|
115
|
+
requestedRef: parsed.requestedRef,
|
|
116
|
+
};
|
|
117
|
+
const installed = await installGitRegistryRef(gitParsed, config, options);
|
|
118
|
+
return {
|
|
119
|
+
...installed,
|
|
120
|
+
source: "github",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
104
123
|
async function installLocalRegistryRef(parsed, config, options) {
|
|
105
124
|
const resolved = await resolveRegistryArtifact(parsed);
|
|
106
125
|
const installedAt = (options?.now ?? new Date()).toISOString();
|
|
@@ -109,7 +128,7 @@ async function installLocalRegistryRef(parsed, config, options) {
|
|
|
109
128
|
ref: resolved.ref,
|
|
110
129
|
artifactUrl: resolved.artifactUrl,
|
|
111
130
|
});
|
|
112
|
-
const audit = runInstallAuditOrThrow(parsed.sourcePath, resolved.source, resolved.ref, registryLabels, config);
|
|
131
|
+
const audit = runInstallAuditOrThrow(parsed.sourcePath, resolved.source, resolved.ref, registryLabels, config, options);
|
|
113
132
|
// For local directories, detect the stash root within the source path.
|
|
114
133
|
// If no nested stash is found, the source path itself is used.
|
|
115
134
|
const stashRoot = detectStashRoot(parsed.sourcePath);
|
|
@@ -124,6 +143,7 @@ async function installLocalRegistryRef(parsed, config, options) {
|
|
|
124
143
|
cacheDir: parsed.sourcePath,
|
|
125
144
|
extractedDir: parsed.sourcePath,
|
|
126
145
|
stashRoot,
|
|
146
|
+
writable: options?.writable,
|
|
127
147
|
audit,
|
|
128
148
|
};
|
|
129
149
|
}
|
|
@@ -148,7 +168,7 @@ async function installGitRegistryRef(parsed, config, options) {
|
|
|
148
168
|
const installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
149
169
|
const stashRoot = detectStashRoot(installRoot);
|
|
150
170
|
if (stashRoot) {
|
|
151
|
-
const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
|
|
171
|
+
const audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
|
|
152
172
|
return {
|
|
153
173
|
id: resolved.id,
|
|
154
174
|
source: resolved.source,
|
|
@@ -160,6 +180,7 @@ async function installGitRegistryRef(parsed, config, options) {
|
|
|
160
180
|
cacheDir,
|
|
161
181
|
extractedDir,
|
|
162
182
|
stashRoot,
|
|
183
|
+
writable: options?.writable,
|
|
163
184
|
audit,
|
|
164
185
|
};
|
|
165
186
|
}
|
|
@@ -193,7 +214,7 @@ async function installGitRegistryRef(parsed, config, options) {
|
|
|
193
214
|
copyDirectoryContents(cloneDir, extractedDir);
|
|
194
215
|
// Clean up the clone dir
|
|
195
216
|
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
196
|
-
audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config);
|
|
217
|
+
audit = runInstallAuditOrThrow(extractedDir, resolved.source, resolved.ref, registryLabels, config, options);
|
|
197
218
|
provisionalKitRoot = detectStashRoot(extractedDir);
|
|
198
219
|
installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
199
220
|
stashRoot = detectStashRoot(installRoot);
|
|
@@ -220,6 +241,7 @@ async function installGitRegistryRef(parsed, config, options) {
|
|
|
220
241
|
cacheDir,
|
|
221
242
|
extractedDir,
|
|
222
243
|
stashRoot,
|
|
244
|
+
writable: options?.writable,
|
|
223
245
|
audit,
|
|
224
246
|
};
|
|
225
247
|
}
|
|
@@ -494,8 +516,15 @@ async function computeFileHash(filePath) {
|
|
|
494
516
|
const hash = createHash("sha256").update(data).digest("hex");
|
|
495
517
|
return `sha256:${hash}`;
|
|
496
518
|
}
|
|
497
|
-
function runInstallAuditOrThrow(rootDir, source, ref, registryLabels, config) {
|
|
498
|
-
const audit = auditInstallCandidate({
|
|
519
|
+
function runInstallAuditOrThrow(rootDir, source, ref, registryLabels, config, options) {
|
|
520
|
+
const audit = auditInstallCandidate({
|
|
521
|
+
rootDir,
|
|
522
|
+
source,
|
|
523
|
+
ref,
|
|
524
|
+
registryLabels,
|
|
525
|
+
config,
|
|
526
|
+
trustThisInstall: options?.trustThisInstall,
|
|
527
|
+
});
|
|
499
528
|
if (audit.blocked) {
|
|
500
529
|
throw new Error(formatInstallAuditFailure(ref, audit));
|
|
501
530
|
}
|
package/dist/registry-resolve.js
CHANGED
|
@@ -273,6 +273,20 @@ async function resolveNpmArtifact(parsed) {
|
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
275
|
async function resolveGithubArtifact(parsed) {
|
|
276
|
+
const gitUrl = `https://github.com/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}.git`;
|
|
277
|
+
// Prefer git-backed installs so private GitHub repos work with the user's
|
|
278
|
+
// normal git credential helper rather than requiring API-specific auth.
|
|
279
|
+
const gitResolvedRevision = resolveGitRevisionFromRemote(gitUrl, parsed.requestedRef);
|
|
280
|
+
if (gitResolvedRevision) {
|
|
281
|
+
return {
|
|
282
|
+
id: parsed.id,
|
|
283
|
+
source: parsed.source,
|
|
284
|
+
ref: parsed.ref,
|
|
285
|
+
artifactUrl: gitUrl,
|
|
286
|
+
resolvedVersion: parsed.requestedRef,
|
|
287
|
+
resolvedRevision: gitResolvedRevision,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
276
290
|
const headers = githubHeaders();
|
|
277
291
|
if (parsed.requestedRef) {
|
|
278
292
|
const commit = await tryFetchJson(`${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}/commits/${encodeURIComponent(parsed.requestedRef)}`, headers);
|
|
@@ -315,6 +329,17 @@ async function resolveGithubArtifact(parsed) {
|
|
|
315
329
|
resolvedRevision: asString(commit?.sha) ?? defaultBranch,
|
|
316
330
|
};
|
|
317
331
|
}
|
|
332
|
+
function resolveGitRevisionFromRemote(url, requestedRef) {
|
|
333
|
+
validateGitUrl(url);
|
|
334
|
+
const ref = requestedRef ?? "HEAD";
|
|
335
|
+
if (requestedRef)
|
|
336
|
+
validateGitRef(requestedRef);
|
|
337
|
+
const result = spawnSync("git", ["ls-remote", url, ref], { encoding: "utf8", timeout: 30_000 });
|
|
338
|
+
if (result.status !== 0)
|
|
339
|
+
return undefined;
|
|
340
|
+
const firstLine = result.stdout.trim().split(/\r?\n/)[0];
|
|
341
|
+
return firstLine?.split(/\s/)[0] || undefined;
|
|
342
|
+
}
|
|
318
343
|
async function resolveGitArtifact(parsed) {
|
|
319
344
|
validateGitUrl(parsed.url);
|
|
320
345
|
const ref = parsed.requestedRef ?? "HEAD";
|