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/dist/renderers.js CHANGED
@@ -9,10 +9,14 @@
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
11
  import { hasErrnoCode } from "./common";
12
+ import { UsageError } from "./errors";
12
13
  import { registerRenderer } from "./file-context";
13
14
  import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
14
15
  import { extractFrontmatterOnly, extractLineRange, extractSection, formatToc, parseMarkdownToc } from "./markdown";
15
16
  import { extractDescriptionFromComments, loadStashFile } from "./metadata";
17
+ import { makeAssetRef } from "./stash-ref";
18
+ import { listKeys as listVaultKeys } from "./vault";
19
+ import { parseWorkflowMarkdown, WorkflowValidationError } from "./workflow-markdown";
16
20
  // ── Interpreter auto-detection map ───────────────────────────────────────────
17
21
  const INTERPRETER_MAP = {
18
22
  ".sh": "bash",
@@ -149,6 +153,12 @@ function deriveName(ctx) {
149
153
  const ext = path.extname(ctx.relPath);
150
154
  return ext ? ctx.relPath.slice(0, -ext.length) : ctx.relPath;
151
155
  }
156
+ function shellQuote(value) {
157
+ return `'${value.replace(/'/g, `'\\''`)}'`;
158
+ }
159
+ export function buildWorkflowAction(ref) {
160
+ return `Resume the active run or start a new run with \`akm workflow next ${shellQuote(ref)}\`.`;
161
+ }
152
162
  /**
153
163
  * Load the matching StashEntry for a file path from the directory's .stash.json.
154
164
  */
@@ -309,6 +319,85 @@ const knowledgeMdRenderer = {
309
319
  }
310
320
  },
311
321
  };
322
+ // ── 4b. wiki-md ──────────────────────────────────────────────────────────────
323
+ const WIKI_PAGE_ACTION = "Wiki page — read below. Use 'toc' to scan, 'section <heading>' for depth.";
324
+ const wikiMdRenderer = {
325
+ name: "wiki-md",
326
+ buildShowResponse(ctx) {
327
+ const name = deriveName(ctx);
328
+ const v = ctx.matchResult.meta?.view ?? { mode: "full" };
329
+ const content = ctx.content();
330
+ switch (v.mode) {
331
+ case "toc": {
332
+ const toc = parseMarkdownToc(content);
333
+ return {
334
+ type: "wiki",
335
+ name,
336
+ path: ctx.absPath,
337
+ action: WIKI_PAGE_ACTION,
338
+ content: formatToc(toc),
339
+ };
340
+ }
341
+ case "frontmatter": {
342
+ const fm = extractFrontmatterOnly(content);
343
+ return {
344
+ type: "wiki",
345
+ name,
346
+ path: ctx.absPath,
347
+ action: WIKI_PAGE_ACTION,
348
+ content: fm ?? "(no frontmatter)",
349
+ };
350
+ }
351
+ case "section": {
352
+ const section = extractSection(content, v.heading);
353
+ if (!section) {
354
+ return {
355
+ type: "wiki",
356
+ name,
357
+ path: ctx.absPath,
358
+ action: WIKI_PAGE_ACTION,
359
+ content: `Section "${v.heading}" not found in ${name}. Try \`akm show wiki:${name} toc\` to discover available headings.`,
360
+ };
361
+ }
362
+ return {
363
+ type: "wiki",
364
+ name,
365
+ path: ctx.absPath,
366
+ action: WIKI_PAGE_ACTION,
367
+ content: section.content,
368
+ };
369
+ }
370
+ case "lines": {
371
+ return {
372
+ type: "wiki",
373
+ name,
374
+ path: ctx.absPath,
375
+ action: WIKI_PAGE_ACTION,
376
+ content: extractLineRange(content, v.start, v.end),
377
+ };
378
+ }
379
+ default: {
380
+ return {
381
+ type: "wiki",
382
+ name,
383
+ path: ctx.absPath,
384
+ action: WIKI_PAGE_ACTION,
385
+ content,
386
+ };
387
+ }
388
+ }
389
+ },
390
+ extractMetadata(entry, ctx) {
391
+ try {
392
+ const toc = parseMarkdownToc(ctx.content());
393
+ if (toc.headings.length > 0)
394
+ entry.toc = toc.headings;
395
+ }
396
+ catch {
397
+ // Non-fatal: skip TOC if file can't be read
398
+ }
399
+ },
400
+ };
312
401
  // ── 5. memory-md ─────────────────────────────────────────────────────────────
313
402
  const memoryMdRenderer = {
314
403
  name: "memory-md",
@@ -323,7 +412,58 @@ const memoryMdRenderer = {
323
412
  };
324
413
  },
325
414
  };
326
- // ── 6. script-source ─────────────────────────────────────────────────────────
415
+ // ── 6. workflow-md ───────────────────────────────────────────────────────────
416
+ const workflowMdRenderer = {
417
+ name: "workflow-md",
418
+ buildShowResponse(ctx) {
419
+ const name = deriveName(ctx);
420
+ const workflow = parseWorkflowForRendering(ctx.content());
421
+ const ref = makeAssetRef("workflow", name, ctx.origin);
422
+ return {
423
+ type: "workflow",
424
+ name,
425
+ path: ctx.absPath,
426
+ action: buildWorkflowAction(ref),
427
+ description: workflow.description,
428
+ workflowTitle: workflow.title,
429
+ parameters: workflow.parameters?.map((parameter) => parameter.name),
430
+ workflowParameters: workflow.parameters,
431
+ steps: workflow.steps,
432
+ };
433
+ },
434
+ extractMetadata(entry, ctx) {
435
+ const workflow = parseWorkflowForRendering(ctx.content());
436
+ const hints = new Set(entry.searchHints ?? []);
437
+ hints.add(workflow.title);
438
+ for (const step of workflow.steps) {
439
+ hints.add(step.title);
440
+ hints.add(step.id);
441
+ hints.add(step.instructions);
442
+ for (const criterion of step.completionCriteria ?? []) {
443
+ hints.add(criterion);
444
+ }
445
+ }
446
+ entry.searchHints = Array.from(hints).filter(Boolean);
447
+ if (workflow.parameters?.length) {
448
+ entry.parameters = workflow.parameters.map((parameter) => ({
449
+ name: parameter.name,
450
+ ...(parameter.description ? { description: parameter.description } : {}),
451
+ }));
452
+ }
453
+ },
454
+ };
455
+ function parseWorkflowForRendering(content) {
456
+ try {
457
+ return parseWorkflowMarkdown(content);
458
+ }
459
+ catch (error) {
460
+ if (error instanceof WorkflowValidationError) {
461
+ throw new UsageError(error.message);
462
+ }
463
+ throw error;
464
+ }
465
+ }
466
+ // ── 7. script-source ─────────────────────────────────────────────────────────
327
467
  const scriptSourceRenderer = {
328
468
  name: "script-source",
329
469
  buildShowResponse(ctx) {
@@ -379,6 +519,43 @@ const scriptSourceRenderer = {
379
519
  }
380
520
  },
381
521
  };
522
+ // ── 8. vault-env ─────────────────────────────────────────────────────────────
523
+ /**
524
+ * Vault renderer. Returns ONLY key names and start-of-line comments — never
525
+ * values. Deliberately omits content/template/prompt so vault values cannot
526
+ * leak through `akm show`.
527
+ */
528
+ const vaultEnvRenderer = {
529
+ name: "vault-env",
530
+ buildShowResponse(ctx) {
531
+ const name = deriveName(ctx);
532
+ const { keys, comments } = listVaultKeys(ctx.absPath);
533
+ return {
534
+ type: "vault",
535
+ name,
536
+ path: ctx.absPath,
537
+ action: 'Vault — keys + comments only. Use `eval "$(akm vault load <ref>)"` to load values into the current shell. Values stay on disk and are never written to akm\'s stdout.',
538
+ description: comments.length > 0 ? comments.join("\n") : undefined,
539
+ keys,
540
+ comments,
541
+ };
542
+ },
543
+ extractMetadata(entry, ctx) {
544
+ // Re-derive from the file directly to guarantee no value ever transits
545
+ // through any other code path. Caller already short-circuits in
546
+ // generateMetadata{,Flat}, but this is defense in depth.
547
+ const { keys, comments } = listVaultKeys(ctx.absPath);
548
+ if (comments.length > 0 && !entry.description) {
549
+ entry.description = comments.join(" ").slice(0, 500);
550
+ entry.source = "comments";
551
+ entry.confidence = 0.7;
552
+ }
553
+ if (keys.length > 0) {
554
+ entry.searchHints = keys;
555
+ }
556
+ entry.tags = Array.from(new Set([...(entry.tags ?? []), "vault", "secrets"]));
557
+ },
558
+ };
382
559
  // ── Registration ─────────────────────────────────────────────────────────────
383
560
  /** All built-in renderers. */
384
561
  const builtinRenderers = [
@@ -386,8 +563,11 @@ const builtinRenderers = [
386
563
  commandMdRenderer,
387
564
  agentMdRenderer,
388
565
  knowledgeMdRenderer,
566
+ wikiMdRenderer,
389
567
  memoryMdRenderer,
568
+ workflowMdRenderer,
390
569
  scriptSourceRenderer,
570
+ vaultEnvRenderer,
391
571
  ];
392
572
  /**
393
573
  * Register all built-in renderers with the file-context registry.
@@ -399,4 +579,4 @@ export function registerBuiltinRenderers() {
399
579
  }
400
580
  }
401
581
  // ── Named exports for testing ────────────────────────────────────────────────
402
- export { agentMdRenderer, commandMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, skillMdRenderer, };
582
+ export { agentMdRenderer, commandMdRenderer, INTERPRETER_MAP, knowledgeMdRenderer, memoryMdRenderer, SETUP_SIGNALS, scriptSourceRenderer, skillMdRenderer, vaultEnvRenderer, wikiMdRenderer, workflowMdRenderer, };
@@ -41,6 +41,10 @@ export function buildSearchFields(entry) {
41
41
  if (entry.intent.output)
42
42
  hintParts.push(entry.intent.output);
43
43
  }
44
+ if (entry.xrefs)
45
+ hintParts.push(entry.xrefs.join(" "));
46
+ if (entry.pageKind)
47
+ hintParts.push(entry.pageKind);
44
48
  const hints = hintParts.join(" ").toLowerCase();
45
49
  const contentParts = [];
46
50
  if (entry.toc) {
@@ -20,7 +20,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
20
20
  const config = existingConfig ?? loadConfig();
21
21
  const sources = [{ path: stashDir }];
22
22
  const seen = new Set([path.resolve(stashDir)]);
23
- const addSource = (dir, registryId) => {
23
+ const addSource = (dir, registryId, wikiName) => {
24
24
  const resolved = path.resolve(dir);
25
25
  if (seen.has(resolved))
26
26
  return;
@@ -29,17 +29,21 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
29
29
  warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
30
30
  }
31
31
  if (isValidDirectory(dir)) {
32
- sources.push({ path: resolved, ...(registryId ? { registryId } : {}) });
32
+ sources.push({
33
+ path: resolved,
34
+ ...(registryId ? { registryId } : {}),
35
+ ...(wikiName ? { wikiName } : {}),
36
+ });
33
37
  }
34
38
  };
35
39
  // Filesystem entries from stashes[]
36
40
  for (const entry of config.stashes ?? []) {
37
41
  if (entry.type === "filesystem" && entry.path && entry.enabled !== false) {
38
- addSource(entry.path, entry.name);
42
+ addSource(entry.path, entry.name, entry.wikiName);
39
43
  }
40
44
  }
41
45
  // Git stash entries: resolve cache directory so the indexer can walk it.
42
- // "context-hub", "github", and "git" provider types are handled.
46
+ // "git" provider type (and its legacy aliases "context-hub", "github") are handled.
43
47
  for (const entry of config.stashes ?? []) {
44
48
  if (GIT_STASH_TYPES.has(entry.type) && entry.url && entry.enabled !== false) {
45
49
  try {
@@ -48,7 +52,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
48
52
  // The content/ subdirectory inside the extracted repo is the actual
49
53
  // stash root containing DOC.md / SKILL.md files that the walker indexes.
50
54
  const contentDir = path.join(cachePaths.repoDir, "content");
51
- addSource(contentDir, entry.name);
55
+ addSource(contentDir, entry.name, entry.wikiName);
52
56
  }
53
57
  catch (err) {
54
58
  warn(`Warning: failed to resolve git stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
@@ -61,7 +65,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
61
65
  if (entry.type === "website" && entry.url && entry.enabled !== false) {
62
66
  try {
63
67
  const cachePaths = getWebsiteCachePaths(entry.url);
64
- addSource(cachePaths.stashDir, entry.name ?? entry.url);
68
+ addSource(cachePaths.stashDir, entry.name ?? entry.url, entry.wikiName);
65
69
  }
66
70
  catch (err) {
67
71
  warn(`Warning: failed to resolve website stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
@@ -70,7 +74,7 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
70
74
  }
71
75
  // Installed kits (registry and local)
72
76
  for (const entry of config.installed ?? []) {
73
- addSource(entry.stashRoot, entry.id);
77
+ addSource(entry.stashRoot, entry.id, entry.wikiName);
74
78
  }
75
79
  return sources;
76
80
  }
@@ -180,7 +184,7 @@ export async function ensureStashCaches(config) {
180
184
  try {
181
185
  const repo = parseGitRepoUrl(entry.url);
182
186
  const cachePaths = getCachePaths(repo.canonicalUrl);
183
- await ensureGitMirror(repo, cachePaths, { requireRepoDir: true });
187
+ await ensureGitMirror(repo, cachePaths, { requireRepoDir: true, writable: entry.writable === true });
184
188
  }
185
189
  catch (err) {
186
190
  warn(`Warning: failed to refresh git mirror for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
@@ -1,9 +1,14 @@
1
+ import * as childProcess from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { fetchWithRetry, IS_WINDOWS } from "./common";
5
6
  import { githubHeaders } from "./github";
6
7
  const REPO = "itlackey/akm";
8
+ const DEFAULT_PACKAGE_NAME = "akm-cli";
9
+ const NODE_MODULES_SEGMENT = "/node_modules/";
10
+ const BUN_GLOBAL_INSTALL_PATTERN = /(^|\/)\.bun\/(?:[^/]+\/)+node_modules\//;
11
+ const PNPM_GLOBAL_INSTALL_PATTERN = /(^|\/)(?:pnpm\/global|\.pnpm-global)(?:\/\d+)?\/node_modules\//;
7
12
  /** Read live runtime signals. */
8
13
  export function getInstallSignals() {
9
14
  return {
@@ -15,8 +20,14 @@ export function getInstallSignals() {
15
20
  // AKM_VERSION ambient type is declared in globals.d.ts
16
21
  export function detectInstallMethod(signals) {
17
22
  const s = signals ?? getInstallSignals();
18
- // npm/bun global install: import.meta.dir contains node_modules
19
- if (s.importMetaDir?.includes("node_modules")) {
23
+ const normalizedImportMetaDir = normalizePathSeparators(s.importMetaDir);
24
+ if (normalizedImportMetaDir.includes(NODE_MODULES_SEGMENT)) {
25
+ if (BUN_GLOBAL_INSTALL_PATTERN.test(normalizedImportMetaDir)) {
26
+ return "bun";
27
+ }
28
+ if (PNPM_GLOBAL_INSTALL_PATTERN.test(normalizedImportMetaDir)) {
29
+ return "pnpm";
30
+ }
20
31
  return "npm";
21
32
  }
22
33
  // Bun-compiled binaries: Bun.main points to a virtual /$bunfs/ path,
@@ -69,34 +80,51 @@ export async function checkForUpdate(currentVersion) {
69
80
  export async function performUpgrade(check, opts) {
70
81
  const { currentVersion, latestVersion, installMethod } = check;
71
82
  const force = opts?.force === true;
72
- if (installMethod === "npm") {
83
+ // All install methods can short-circuit here unless the user explicitly forces an upgrade.
84
+ if (!check.updateAvailable && !force) {
73
85
  return {
74
86
  currentVersion,
75
87
  newVersion: latestVersion,
76
88
  upgraded: false,
77
89
  installMethod,
78
- message: `akm installed via npm. Run: bun install -g akm-cli@latest`,
90
+ message: `akm v${currentVersion} is already the latest version`,
79
91
  };
80
92
  }
81
- if (installMethod === "unknown") {
93
+ const packageManagerCommand = getPackageManagerUpgradeCommand(installMethod);
94
+ if (packageManagerCommand) {
95
+ if (!latestVersion) {
96
+ throw new Error("Unable to determine latest version from GitHub releases. Check https://github.com/itlackey/akm/releases");
97
+ }
98
+ const result = childProcess.spawnSync(packageManagerCommand.command, packageManagerCommand.args, {
99
+ encoding: "utf8",
100
+ env: process.env,
101
+ stdio: "pipe",
102
+ });
103
+ if (result.error) {
104
+ throw new Error(`Failed to run '${packageManagerCommand.displayCommand}': ${result.error.message}`);
105
+ }
106
+ if (result.status !== 0) {
107
+ const details = (result.stderr ?? "").trim() || (result.stdout ?? "").trim() || `exit code ${result.status}`;
108
+ throw new Error(`Failed to upgrade akm via ${installMethod}: ${details}\nRun manually: ${packageManagerCommand.displayCommand}`);
109
+ }
82
110
  return {
83
111
  currentVersion,
84
112
  newVersion: latestVersion,
85
- upgraded: false,
113
+ upgraded: true,
86
114
  installMethod,
87
- message: `Unable to detect install method. Upgrade manually from https://github.com/${REPO}/releases`,
115
+ message: `akm upgraded via ${installMethod}`,
88
116
  };
89
117
  }
90
- // Binary install
91
- if (!check.updateAvailable && !force) {
118
+ if (installMethod === "unknown") {
92
119
  return {
93
120
  currentVersion,
94
121
  newVersion: latestVersion,
95
122
  upgraded: false,
96
123
  installMethod,
97
- message: `akm v${currentVersion} is already the latest version`,
124
+ message: `Unable to detect install method. Upgrade manually from https://github.com/${REPO}/releases`,
98
125
  };
99
126
  }
127
+ // Binary install
100
128
  if (!latestVersion) {
101
129
  throw new Error("Unable to determine latest version from GitHub releases. Check https://github.com/itlackey/akm/releases");
102
130
  }
@@ -270,3 +298,51 @@ function parseChecksumForFile(checksumsText, filename) {
270
298
  }
271
299
  return undefined;
272
300
  }
301
+ function normalizePathSeparators(value) {
302
+ return (value ?? "").replaceAll("\\", "/");
303
+ }
304
+ function getInstalledPackageName() {
305
+ try {
306
+ const pkgPath = path.resolve(import.meta.dir ?? __dirname, "../package.json");
307
+ if (fs.existsSync(pkgPath)) {
308
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
309
+ if (typeof pkg.name === "string" && pkg.name.trim()) {
310
+ return pkg.name.trim();
311
+ }
312
+ }
313
+ }
314
+ catch {
315
+ // Swallow and fall back to default package name.
316
+ }
317
+ return DEFAULT_PACKAGE_NAME;
318
+ }
319
+ function resolveNodePackageManagerCommand(name) {
320
+ const extension = IS_WINDOWS ? ".cmd" : "";
321
+ const adjacent = path.join(path.dirname(process.execPath), `${name}${extension}`);
322
+ return fs.existsSync(adjacent) ? adjacent : name;
323
+ }
324
+ export function getPackageManagerUpgradeCommand(installMethod, packageName = getInstalledPackageName()) {
325
+ const pkgRef = `${packageName}@latest`;
326
+ if (installMethod === "bun") {
327
+ return {
328
+ command: "bun",
329
+ args: ["install", "-g", pkgRef],
330
+ displayCommand: `bun install -g ${pkgRef}`,
331
+ };
332
+ }
333
+ if (installMethod === "pnpm") {
334
+ return {
335
+ command: resolveNodePackageManagerCommand("pnpm"),
336
+ args: ["add", "-g", pkgRef],
337
+ displayCommand: `pnpm add -g ${pkgRef}`,
338
+ };
339
+ }
340
+ if (installMethod === "npm") {
341
+ return {
342
+ command: resolveNodePackageManagerCommand("npm"),
343
+ args: ["install", "-g", pkgRef],
344
+ displayCommand: `npm install -g ${pkgRef}`,
345
+ };
346
+ }
347
+ return undefined;
348
+ }