akm-cli 0.4.1 → 0.5.0-rc2
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/CHANGELOG.md +232 -0
- package/dist/asset-registry.js +7 -0
- package/dist/asset-spec.js +35 -0
- package/dist/cli.js +1153 -32
- package/dist/completions.js +2 -2
- package/dist/config-cli.js +41 -0
- package/dist/config.js +62 -0
- package/dist/file-context.js +2 -1
- package/dist/github.js +20 -1
- package/dist/indexer.js +60 -6
- 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/local-search.js +3 -1
- package/dist/matchers.js +56 -4
- package/dist/metadata.js +102 -4
- package/dist/migration-help.js +110 -0
- 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/self-update.js +86 -10
- 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 +3 -1
package/dist/local-search.js
CHANGED
|
@@ -14,7 +14,7 @@ import { deriveCanonicalAssetNameFromStashRoot } from "./asset-spec";
|
|
|
14
14
|
import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getUtilityScoresByIds, openDatabase, searchFts, searchVec, } from "./db";
|
|
15
15
|
import { getRenderer } from "./file-context";
|
|
16
16
|
import { buildSearchText } from "./indexer";
|
|
17
|
-
import { generateMetadataFlat, loadStashFile } from "./metadata";
|
|
17
|
+
import { generateMetadataFlat, loadStashFile, shouldIndexStashFile } from "./metadata";
|
|
18
18
|
import { getDbPath } from "./paths";
|
|
19
19
|
import { buildEditHint, findSourceForPath, isEditable } from "./search-source";
|
|
20
20
|
import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status";
|
|
@@ -619,6 +619,8 @@ async function indexAssets(stashDir, type) {
|
|
|
619
619
|
fileBasenameMap.get(entry.name.split("/").pop() ?? "") ??
|
|
620
620
|
(files[0] || dirPath);
|
|
621
621
|
}
|
|
622
|
+
if (!shouldIndexStashFile(stashDir, entryPath))
|
|
623
|
+
continue;
|
|
622
624
|
assets.push({ entry, path: entryPath });
|
|
623
625
|
}
|
|
624
626
|
}
|
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,68 @@ 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
|
+
}
|
|
253
|
+
const WIKI_INFRA_FILES = new Set(["schema.md", "index.md", "log.md"]);
|
|
254
|
+
/**
|
|
255
|
+
* Apply wiki-specific index exclusions while leaving all other stash files
|
|
256
|
+
* untouched.
|
|
257
|
+
*
|
|
258
|
+
* - In a normal stash, excludes `wikis/<name>/raw/**` and wiki-root
|
|
259
|
+
* `schema.md`, `index.md`, `log.md`.
|
|
260
|
+
* - In a wiki-root stash source (`wikiName`), excludes `raw/**` and those same
|
|
261
|
+
* root-level infrastructure files.
|
|
262
|
+
*/
|
|
263
|
+
export function shouldIndexStashFile(stashRoot, file, options) {
|
|
264
|
+
const relPath = path.relative(stashRoot, file);
|
|
265
|
+
if (!relPath || relPath.startsWith("..") || path.isAbsolute(relPath))
|
|
266
|
+
return true;
|
|
267
|
+
const segments = relPath.split(/[\\/]+/).filter(Boolean);
|
|
268
|
+
if (segments.length === 0)
|
|
269
|
+
return true;
|
|
270
|
+
if (options?.treatStashRootAsWikiRoot) {
|
|
271
|
+
if (segments[0] === "raw")
|
|
272
|
+
return false;
|
|
273
|
+
return !(segments.length === 1 && WIKI_INFRA_FILES.has(segments[0]));
|
|
274
|
+
}
|
|
275
|
+
const wikisIdx = segments.indexOf("wikis");
|
|
276
|
+
if (wikisIdx < 0 || wikisIdx + 1 >= segments.length)
|
|
277
|
+
return true;
|
|
278
|
+
const wikiRelativeSegments = segments.slice(wikisIdx + 2);
|
|
279
|
+
if (wikiRelativeSegments.length === 0)
|
|
280
|
+
return true;
|
|
281
|
+
if (wikiRelativeSegments[0] === "raw")
|
|
282
|
+
return false;
|
|
283
|
+
return !(wikiRelativeSegments.length === 1 && WIKI_INFRA_FILES.has(wikiRelativeSegments[0]));
|
|
284
|
+
}
|
|
199
285
|
/**
|
|
200
286
|
* Extract `@param` JSDoc tags from a script file's leading comment block.
|
|
201
287
|
*
|
|
@@ -316,6 +402,8 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
|
|
|
316
402
|
const fmParams = extractFrontmatterParameters(parsed.data);
|
|
317
403
|
if (fmParams)
|
|
318
404
|
entry.parameters = fmParams;
|
|
405
|
+
// Pass wiki-pattern frontmatter through onto the entry
|
|
406
|
+
applyWikiFrontmatter(entry, parsed.data);
|
|
319
407
|
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
320
408
|
if (entry.type === "command") {
|
|
321
409
|
const cmdParams = extractCommandParameters(parsed.content);
|
|
@@ -324,8 +412,11 @@ export async function generateMetadata(dirPath, assetType, files, typeRoot = dir
|
|
|
324
412
|
}
|
|
325
413
|
}
|
|
326
414
|
}
|
|
327
|
-
// Extract @param from script files
|
|
328
|
-
|
|
415
|
+
// Extract @param from script files.
|
|
416
|
+
// Vault files (.env) are deliberately excluded — their contents are secrets
|
|
417
|
+
// and must never be parsed for @param or any other metadata that could
|
|
418
|
+
// embed a value into the entry.
|
|
419
|
+
if (ext !== ".md" && assetType !== "vault") {
|
|
329
420
|
const scriptParams = extractScriptParameters(file);
|
|
330
421
|
if (scriptParams)
|
|
331
422
|
entry.parameters = scriptParams;
|
|
@@ -369,6 +460,8 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
369
460
|
const entries = [];
|
|
370
461
|
const pkgMetaCache = new Map();
|
|
371
462
|
for (const file of files) {
|
|
463
|
+
if (!shouldIndexStashFile(stashRoot, file))
|
|
464
|
+
continue;
|
|
372
465
|
const ctx = buildFileContext(stashRoot, file);
|
|
373
466
|
const match = await runMatchers(ctx);
|
|
374
467
|
if (!match)
|
|
@@ -418,6 +511,8 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
418
511
|
const fmParams = extractFrontmatterParameters(parsed.data);
|
|
419
512
|
if (fmParams)
|
|
420
513
|
entry.parameters = fmParams;
|
|
514
|
+
// Pass wiki-pattern frontmatter through onto the entry
|
|
515
|
+
applyWikiFrontmatter(entry, parsed.data);
|
|
421
516
|
// Extract parameters from template placeholders ($1, $ARGUMENTS, {{named}})
|
|
422
517
|
if (entry.type === "command") {
|
|
423
518
|
const cmdParams = extractCommandParameters(parsed.content);
|
|
@@ -426,8 +521,11 @@ export async function generateMetadataFlat(stashRoot, files) {
|
|
|
426
521
|
}
|
|
427
522
|
}
|
|
428
523
|
}
|
|
429
|
-
// Extract @param from script files
|
|
430
|
-
|
|
524
|
+
// Extract @param from script files.
|
|
525
|
+
// Vault files (.env) are deliberately excluded — their contents are secrets
|
|
526
|
+
// and must never be parsed for @param or any other metadata that could
|
|
527
|
+
// embed a value into the entry.
|
|
528
|
+
if (ext !== ".md" && assetType !== "vault") {
|
|
431
529
|
const scriptParams = extractScriptParameters(file, ctx.content());
|
|
432
530
|
if (scriptParams)
|
|
433
531
|
entry.parameters = scriptParams;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";
|
|
4
|
+
const EMBEDDED_MIGRATION_GUIDES = {
|
|
5
|
+
"0.5.0": `Migration notes for akm v0.5.0
|
|
6
|
+
|
|
7
|
+
- New top-level surfaces: \`akm wiki …\`, \`akm workflow …\`, \`akm vault …\`, and \`akm save\`.
|
|
8
|
+
- If you tried the unreleased single-wiki LLM prototype, move to the new \`akm wiki …\` workflow.
|
|
9
|
+
- Removed from the prototype surface: \`akm lint\`, \`akm import --llm\`, \`akm import --dry-run\`, \`knowledge.pageKinds\`, and the old ingest/lint LLM prompts.
|
|
10
|
+
- Existing raw wiki-like content should be moved into \`wikis/<name>/raw/\` and then managed with the new wiki commands.
|
|
11
|
+
`,
|
|
12
|
+
"0.3.0": `Migration notes for akm v0.3.0
|
|
13
|
+
|
|
14
|
+
- The old \`stash\` and \`kit\` command groups were folded into the top-level CLI.
|
|
15
|
+
- Use \`akm add\`, \`akm list\`, and \`akm remove\` instead of the older split command surfaces.
|
|
16
|
+
- Documentation and examples from older releases should be updated to the unified source model.
|
|
17
|
+
`,
|
|
18
|
+
"0.2.0": `Migration notes for akm v0.2.0
|
|
19
|
+
|
|
20
|
+
- Asset refs are user-facing \`type:name\` values; do not rely on URI-style refs.
|
|
21
|
+
- The old fixed asset-type union was replaced by an extensible asset type system.
|
|
22
|
+
- \`tool\` assets were removed; use \`script\` assets instead.
|
|
23
|
+
- Config and docs should treat remote provider scores and local scores as part of one shared search pipeline.
|
|
24
|
+
`,
|
|
25
|
+
"0.1.0": `Migration notes for akm v0.1.0
|
|
26
|
+
|
|
27
|
+
- The package and project were rebranded from Agent-i-Kit to akm.
|
|
28
|
+
- Update package references from \`agent-i-kit\` to \`akm-cli\`.
|
|
29
|
+
- Update config, registry, plugin, path, and environment-variable references from \`agent-i-kit\` / \`AGENT_I_KIT_*\` to \`akm\` / \`AKM_*\`.
|
|
30
|
+
- The \`tool\` asset type and \`submit\` command were removed.
|
|
31
|
+
`,
|
|
32
|
+
"0.0.13": `Migration notes for akm v0.0.13
|
|
33
|
+
|
|
34
|
+
- Initial public release.
|
|
35
|
+
- No migration steps are required for earlier akm versions.
|
|
36
|
+
`,
|
|
37
|
+
};
|
|
38
|
+
function loadChangelog() {
|
|
39
|
+
try {
|
|
40
|
+
const changelogPath = path.resolve(import.meta.dir, "../CHANGELOG.md");
|
|
41
|
+
if (fs.existsSync(changelogPath)) {
|
|
42
|
+
return fs.readFileSync(changelogPath, "utf8");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// fall through to embedded notes
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function escapeRegexString(value) {
|
|
51
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
}
|
|
53
|
+
function normalizeRequestedVersion(input) {
|
|
54
|
+
const value = input.trim();
|
|
55
|
+
if (!value)
|
|
56
|
+
return value;
|
|
57
|
+
if (value.toLowerCase() === "latest")
|
|
58
|
+
return "latest";
|
|
59
|
+
const withoutV = value.replace(/^v/i, "");
|
|
60
|
+
return withoutV;
|
|
61
|
+
}
|
|
62
|
+
function versionCandidates(requested) {
|
|
63
|
+
if (requested === "latest")
|
|
64
|
+
return ["latest"];
|
|
65
|
+
const exact = requested;
|
|
66
|
+
const stable = requested.replace(/[-+].*$/, "");
|
|
67
|
+
return stable === exact ? [exact] : [exact, stable];
|
|
68
|
+
}
|
|
69
|
+
function resolveLatestVersion(changelog) {
|
|
70
|
+
for (const match of changelog.matchAll(/^## \[([^\]]+)\]/gm)) {
|
|
71
|
+
const version = match[1];
|
|
72
|
+
if (version !== "Unreleased")
|
|
73
|
+
return version;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function extractChangelogSection(changelog, version) {
|
|
78
|
+
const pattern = new RegExp(`^## \\[${escapeRegexString(version)}\\][^\\n]*\\n([\\s\\S]*?)(?=^## \\[|\\Z)`, "m");
|
|
79
|
+
const match = changelog.match(pattern);
|
|
80
|
+
if (!match)
|
|
81
|
+
return undefined;
|
|
82
|
+
return `## [${version}]\n${match[1].trim()}\n`;
|
|
83
|
+
}
|
|
84
|
+
function fallbackGuide(version) {
|
|
85
|
+
const embedded = EMBEDDED_MIGRATION_GUIDES[version];
|
|
86
|
+
if (embedded)
|
|
87
|
+
return `${embedded.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
88
|
+
return `No dedicated migration note is bundled for akm v${version}.\n\nSee the full changelog: ${CHANGELOG_URL}\n`;
|
|
89
|
+
}
|
|
90
|
+
export function renderMigrationHelp(versionInput, changelogText = loadChangelog()) {
|
|
91
|
+
const requested = normalizeRequestedVersion(versionInput);
|
|
92
|
+
if (!requested) {
|
|
93
|
+
return `Version is required.\n\nUsage: akm help migrate <version>\n`;
|
|
94
|
+
}
|
|
95
|
+
const resolvedLatest = changelogText ? resolveLatestVersion(changelogText) : undefined;
|
|
96
|
+
const candidates = requested === "latest" && resolvedLatest ? [resolvedLatest] : versionCandidates(requested);
|
|
97
|
+
if (changelogText) {
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
const section = extractChangelogSection(changelogText, candidate);
|
|
100
|
+
if (section) {
|
|
101
|
+
const embedded = EMBEDDED_MIGRATION_GUIDES[candidate];
|
|
102
|
+
if (!embedded)
|
|
103
|
+
return `${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
104
|
+
return `${embedded.trim()}\n\nRelease notes\n-------------\n${section.trim()}\n\nFull changelog: ${CHANGELOG_URL}\n`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const fallbackVersion = candidates.find((candidate) => candidate !== "latest") ?? requested;
|
|
109
|
+
return fallbackGuide(fallbackVersion);
|
|
110
|
+
}
|
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";
|