akm-cli 0.6.0-rc1 → 0.6.0

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +9 -9
  3. package/dist/cli.js +199 -114
  4. package/dist/{completions.js → commands/completions.js} +1 -1
  5. package/dist/{config-cli.js → commands/config-cli.js} +109 -11
  6. package/dist/{curate.js → commands/curate.js} +8 -3
  7. package/dist/{info.js → commands/info.js} +15 -9
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +4 -7
  10. package/dist/{installed-stashes.js → commands/installed-stashes.js} +77 -31
  11. package/dist/{migration-help.js → commands/migration-help.js} +2 -2
  12. package/dist/{registry-search.js → commands/registry-search.js} +8 -6
  13. package/dist/{remember.js → commands/remember.js} +55 -49
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +69 -3
  16. package/dist/{stash-show.js → commands/show.js} +104 -84
  17. package/dist/{stash-add.js → commands/source-add.js} +42 -32
  18. package/dist/{stash-clone.js → commands/source-clone.js} +12 -10
  19. package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
  20. package/dist/{vault.js → commands/vault.js} +43 -0
  21. package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
  22. package/dist/{asset-registry.js → core/asset-registry.js} +1 -1
  23. package/dist/{asset-spec.js → core/asset-spec.js} +1 -1
  24. package/dist/{config.js → core/config.js} +133 -56
  25. package/dist/core/errors.js +90 -0
  26. package/dist/{frontmatter.js → core/frontmatter.js} +5 -3
  27. package/dist/core/write-source.js +280 -0
  28. package/dist/{db-search.js → indexer/db-search.js} +25 -19
  29. package/dist/{db.js → indexer/db.js} +79 -47
  30. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  31. package/dist/{indexer.js → indexer/indexer.js} +132 -33
  32. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  33. package/dist/{matchers.js → indexer/matchers.js} +3 -6
  34. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  35. package/dist/{search-source.js → indexer/search-source.js} +52 -41
  36. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  37. package/dist/{walker.js → indexer/walker.js} +1 -1
  38. package/dist/{lockfile.js → integrations/lockfile.js} +1 -1
  39. package/dist/{llm-client.js → llm/client.js} +1 -1
  40. package/dist/{embedders → llm/embedders}/local.js +2 -2
  41. package/dist/{embedders → llm/embedders}/remote.js +1 -1
  42. package/dist/{embedders → llm/embedders}/types.js +1 -1
  43. package/dist/{metadata-enhance.js → llm/metadata-enhance.js} +2 -2
  44. package/dist/{cli-hints.js → output/cli-hints.js} +3 -0
  45. package/dist/{output-context.js → output/context.js} +21 -3
  46. package/dist/{renderers.js → output/renderers.js} +9 -65
  47. package/dist/{output-shapes.js → output/shapes.js} +18 -4
  48. package/dist/{output-text.js → output/text.js} +2 -2
  49. package/dist/{registry-build-index.js → registry/build-index.js} +16 -7
  50. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  51. package/dist/registry/factory.js +33 -0
  52. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  53. package/dist/{providers → registry/providers}/index.js +1 -1
  54. package/dist/{providers → registry/providers}/skills-sh.js +59 -3
  55. package/dist/{providers → registry/providers}/static-index.js +80 -12
  56. package/dist/registry/providers/types.js +25 -0
  57. package/dist/{registry-resolve.js → registry/resolve.js} +3 -3
  58. package/dist/{detect.js → setup/detect.js} +0 -27
  59. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  60. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  61. package/dist/{setup.js → setup/setup.js} +16 -56
  62. package/dist/{stash-include.js → sources/include.js} +1 -1
  63. package/dist/sources/provider-factory.js +36 -0
  64. package/dist/sources/provider.js +21 -0
  65. package/dist/sources/providers/filesystem.js +35 -0
  66. package/dist/{stash-providers → sources/providers}/git.js +53 -64
  67. package/dist/{stash-providers → sources/providers}/index.js +3 -4
  68. package/dist/sources/providers/install-types.js +14 -0
  69. package/dist/{stash-providers → sources/providers}/npm.js +42 -41
  70. package/dist/{stash-providers → sources/providers}/provider-utils.js +3 -3
  71. package/dist/{stash-providers → sources/providers}/sync-from-ref.js +2 -2
  72. package/dist/{stash-providers → sources/providers}/tar-utils.js +11 -8
  73. package/dist/{stash-providers → sources/providers}/website.js +29 -65
  74. package/dist/{stash-resolve.js → sources/resolve.js} +8 -7
  75. package/dist/{wiki.js → wiki/wiki.js} +34 -18
  76. package/dist/{workflow-authoring.js → workflows/authoring.js} +37 -14
  77. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  78. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  79. package/dist/workflows/document-cache.js +20 -0
  80. package/dist/workflows/parser.js +379 -0
  81. package/dist/workflows/renderer.js +78 -0
  82. package/dist/{workflow-runs.js → workflows/runs.js} +72 -28
  83. package/dist/workflows/schema.js +11 -0
  84. package/dist/workflows/validator.js +48 -0
  85. package/docs/migration/release-notes/0.6.0.md +91 -23
  86. package/package.json +1 -1
  87. package/dist/errors.js +0 -45
  88. package/dist/llm.js +0 -16
  89. package/dist/registry-factory.js +0 -19
  90. package/dist/ripgrep.js +0 -2
  91. package/dist/stash-provider-factory.js +0 -35
  92. package/dist/stash-provider.js +0 -3
  93. package/dist/stash-providers/filesystem.js +0 -71
  94. package/dist/stash-providers/openviking.js +0 -348
  95. package/dist/stash-types.js +0 -1
  96. package/dist/workflow-markdown.js +0 -260
  97. /package/dist/{common.js → core/common.js} +0 -0
  98. /package/dist/{markdown.js → core/markdown.js} +0 -0
  99. /package/dist/{paths.js → core/paths.js} +0 -0
  100. /package/dist/{warn.js → core/warn.js} +0 -0
  101. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  102. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  103. /package/dist/{github.js → integrations/github.js} +0 -0
  104. /package/dist/{embedder.js → llm/embedder.js} +0 -0
  105. /package/dist/{embedders → llm/embedders}/cache.js +0 -0
  106. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  107. /package/dist/{setup-steps.js → setup/steps.js} +0 -0
  108. /package/dist/{registry-types.js → sources/types.js} +0 -0
@@ -1,31 +1,47 @@
1
+ /**
2
+ * `akm show` — entry point.
3
+ *
4
+ * Spec §6.2:
5
+ *
6
+ * show(ref) → indexer.lookup(ref) → readFile(entry.filePath)
7
+ *
8
+ * The richer presentation logic (matchers, renderers, wiki-root handling,
9
+ * edit-hints, summary-detail truncation) lives below in this file. The flow:
10
+ *
11
+ * 1. Special-case wiki-root refs (`wiki:<name>` with no page path).
12
+ * 2. Ask `indexer.lookup(ref)` for the row in the FTS index.
13
+ * 3. Fall back to the on-disk type-dir resolver only when the index has
14
+ * no matching row — covers the "indexed yet?" gap when the user has
15
+ * just added a file and not run `akm index`.
16
+ * 4. Render the file via the matcher/renderer pipeline.
17
+ *
18
+ * Step (2) is the v1 spec change: reading is the indexer's job. Step (3) is a
19
+ * pragmatic safety net (NOT remote provider fallback, which the spec
20
+ * forbids — "Show: Local FTS5 index only. No remote provider fallback.").
21
+ */
1
22
  import fs from "node:fs";
2
23
  import path from "node:path";
3
- import { loadConfig } from "./config";
4
- import { closeDatabase, openDatabase } from "./db";
5
- import { NotFoundError, UsageError } from "./errors";
6
- import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "./file-context";
7
- import { parseFrontmatter, toStringOrUndefined } from "./frontmatter";
8
- import { loadStashFile } from "./metadata";
9
- import { resolveSourcesForOrigin } from "./origin-resolve";
10
- import { buildEditHint, findSourceForPath, isEditable, resolveStashSources } from "./search-source";
11
- import { resolveStashProviders } from "./stash-provider-factory";
12
- import { parseAssetRef } from "./stash-ref";
13
- import { resolveAssetPath } from "./stash-resolve";
14
- import { insertUsageEvent } from "./usage-events";
15
- // Eagerly import stash providers to trigger self-registration
16
- import "./stash-providers/index";
24
+ import { parseAssetRef } from "../core/asset-ref";
25
+ import { loadConfig } from "../core/config";
26
+ import { NotFoundError, UsageError } from "../core/errors";
27
+ import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
28
+ import { closeDatabase, findEntryIdByRef, openDatabase } from "../indexer/db";
29
+ import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
30
+ import { lookup } from "../indexer/indexer";
31
+ import { loadStashFile } from "../indexer/metadata";
32
+ import { buildEditHint, findSourceForPath, isEditable, resolveSourceEntries } from "../indexer/search-source";
33
+ import { insertUsageEvent } from "../indexer/usage-events";
34
+ import { resolveSourcesForOrigin } from "../registry/origin-resolve";
35
+ // Eagerly import source providers to trigger self-registration.
36
+ import "../sources/providers/index";
37
+ import { resolveAssetPath } from "../sources/resolve";
17
38
  /**
18
39
  * Show a wiki root (no page path) — returns the same payload as
19
40
  * `akm wiki show <name>`.
20
- *
21
- * Called when `parseAssetRef` yields `type === "wiki"` and the name has no
22
- * `/`, e.g. `wiki:research`.
23
41
  */
24
42
  async function showWikiRoot(stashDir, wikiName) {
25
- const { showWiki } = await import("./wiki.js");
43
+ const { showWiki } = await import("../wiki/wiki.js");
26
44
  const result = showWiki(stashDir, wikiName);
27
- // Shape the WikiShowResult into a ShowResponse-compatible object.
28
- // The payload mirrors what `akm wiki show <name>` returns.
29
45
  return {
30
46
  type: "wiki",
31
47
  name: result.ref,
@@ -40,7 +56,7 @@ async function showWikiRoot(stashDir, wikiName) {
40
56
  };
41
57
  }
42
58
  async function showWikiRootForSource(stashDir, source, wikiName) {
43
- const { showWikiAtPath } = await import("./wiki.js");
59
+ const { showWikiAtPath } = await import("../wiki/wiki.js");
44
60
  if (source.wikiName === wikiName) {
45
61
  const result = showWikiAtPath(wikiName, source.path);
46
62
  return {
@@ -78,7 +94,9 @@ function resolveRegisteredWikiAssetPath(wikiRoot, wikiName, assetName) {
78
94
  return realTarget;
79
95
  }
80
96
  /**
81
- * Unified show: tries local FTS5 index first, then remote providers.
97
+ * Unified show: queries the local FTS5 index, then falls back to on-disk
98
+ * type-dir resolution if the index has no row. Spec §6.2; no remote provider
99
+ * fallback.
82
100
  *
83
101
  * When `detail` is `"summary"`, the response omits content/template/prompt and
84
102
  * returns only compact metadata (name, type, description, tags, parameters).
@@ -92,7 +110,7 @@ export async function akmShowUnified(input) {
92
110
  {
93
111
  const parsed = parseAssetRef(ref);
94
112
  if (parsed.type === "wiki" && !parsed.name.includes("/")) {
95
- const allSources = resolveStashSources();
113
+ const allSources = resolveSourceEntries();
96
114
  const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
97
115
  let lastError;
98
116
  for (const source of searchSources) {
@@ -109,56 +127,19 @@ export async function akmShowUnified(input) {
109
127
  new NotFoundError(`Wiki not found: ${parsed.name}. Run \`akm wiki create ${parsed.name}\` to create it.`));
110
128
  }
111
129
  }
112
- // 1. Try local filesystem first (FTS5 index lookup)
113
- let localError;
114
- try {
115
- const result = await showLocal(input);
116
- logShowEvent(ref);
117
- return result;
118
- }
119
- catch (err) {
120
- // Only fall through to remote providers on NotFoundError
121
- if (!(err instanceof NotFoundError))
122
- throw err;
123
- localError = err;
124
- }
125
- // 2. Try remote providers (e.g. OpenViking)
126
- const config = loadConfig();
127
- const providers = resolveStashProviders(config).filter((p) => p.type !== "filesystem" && p.canShow(ref));
128
- for (const provider of providers) {
129
- try {
130
- const response = await provider.show(ref, input.view);
131
- logShowEvent(ref);
132
- if (input.detail === "summary") {
133
- return buildSummaryResponse(response);
134
- }
135
- return response;
136
- }
137
- catch (err) {
138
- if (!(err instanceof NotFoundError))
139
- throw err;
140
- }
141
- }
142
- // Nothing found anywhere — rethrow the original local error with its specific message
143
- throw localError;
130
+ // Try local filesystem (FTS5 index lookup, then on-disk fallback)
131
+ const result = await showLocal(input);
132
+ logShowEvent(ref);
133
+ return result;
144
134
  }
145
- /**
146
- * Fire-and-forget: log a show event to the usage_events table.
147
- * Never blocks the caller; errors are silently ignored.
148
- */
149
135
  function logShowEvent(ref, existingDb) {
150
136
  try {
151
137
  const db = existingDb ?? openDatabase();
152
138
  try {
153
- const parsed = parseAssetRef(ref);
154
- const safeName = parsed.name.replace(/%/g, "\\%").replace(/_/g, "\\_");
155
- const row = db
156
- .prepare("SELECT id FROM entries WHERE entry_key LIKE ? ESCAPE '\\' AND entry_type = ? LIMIT 1")
157
- .get(`%:${parsed.type}:${safeName}`, parsed.type);
158
139
  insertUsageEvent(db, {
159
140
  event_type: "show",
160
141
  entry_ref: ref,
161
- entry_id: row?.id,
142
+ entry_id: findEntryIdByRef(db, ref),
162
143
  });
163
144
  }
164
145
  finally {
@@ -170,14 +151,48 @@ function logShowEvent(ref, existingDb) {
170
151
  /* fire-and-forget */
171
152
  }
172
153
  }
154
+ /**
155
+ * Resolve an asset path to a file via:
156
+ * 1. `indexer.lookup(ref)` — the spec's primary path (§6.2).
157
+ * 2. On-disk type-dir traversal — fallback for files not yet indexed.
158
+ *
159
+ * Returns `undefined` if neither path finds a match.
160
+ */
161
+ async function resolvePathViaIndexThenDisk(parsed, searchSourceDirs) {
162
+ // Step 1: indexer
163
+ try {
164
+ const entry = await lookup(parsed);
165
+ if (entry) {
166
+ return { assetPath: entry.filePath };
167
+ }
168
+ }
169
+ catch (err) {
170
+ // Index unavailable (e.g. DB doesn't exist yet) — fall back to disk walk.
171
+ if (!(err instanceof NotFoundError)) {
172
+ // continue to disk fallback
173
+ }
174
+ }
175
+ // Step 2: on-disk type-dir traversal
176
+ let lastError;
177
+ for (const dir of searchSourceDirs) {
178
+ try {
179
+ const assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
180
+ return { assetPath, lastError };
181
+ }
182
+ catch (err) {
183
+ lastError = err instanceof Error ? err : new Error(String(err));
184
+ }
185
+ }
186
+ return lastError ? { assetPath: "", lastError } : undefined;
187
+ }
173
188
  /** @internal Use akmShowUnified() for all external callers. */
174
189
  export async function showLocal(input) {
175
190
  const parsed = parseAssetRef(input.ref);
176
191
  const displayType = parsed.type;
177
192
  const config = loadConfig();
178
- const allSources = resolveStashSources(input.stashDir);
193
+ const allSources = resolveSourceEntries(input.stashDir);
179
194
  const searchSources = resolveSourcesForOrigin(parsed.origin, allSources);
180
- const allStashDirs = searchSources.map((s) => s.path);
195
+ const allSourceDirs = searchSources.map((s) => s.path);
181
196
  let assetPath;
182
197
  const matchedSource = parsed.type === "wiki" ? searchSources.find((source) => parsed.name.startsWith(`${source.wikiName}/`)) : undefined;
183
198
  let lastError;
@@ -189,15 +204,13 @@ export async function showLocal(input) {
189
204
  lastError = err instanceof Error ? err : new Error(String(err));
190
205
  }
191
206
  }
192
- for (const dir of allStashDirs) {
193
- if (assetPath)
194
- break;
195
- try {
196
- assetPath = await resolveAssetPath(dir, parsed.type, parsed.name);
197
- break;
207
+ if (!assetPath) {
208
+ const resolved = await resolvePathViaIndexThenDisk(parsed, allSourceDirs);
209
+ if (resolved?.assetPath) {
210
+ assetPath = resolved.assetPath;
198
211
  }
199
- catch (err) {
200
- lastError = err instanceof Error ? err : new Error(String(err));
212
+ else if (resolved?.lastError) {
213
+ lastError = resolved.lastError;
201
214
  }
202
215
  }
203
216
  if (!assetPath && parsed.origin && searchSources.length === 0) {
@@ -211,7 +224,7 @@ export async function showLocal(input) {
211
224
  "Check the name with `akm search` or verify the asset exists in your stash."));
212
225
  }
213
226
  const source = matchedSource ?? findSourceForPath(assetPath, allSources);
214
- const sourceStashDir = source?.path ?? allStashDirs[0];
227
+ const sourceStashDir = source?.path ?? allSourceDirs[0];
215
228
  if (!sourceStashDir) {
216
229
  throw new UsageError(`Could not determine stash root for asset: ${displayType}:${parsed.name}. ` +
217
230
  "Run `akm init` to create the stash directory, or check `akm stash list` for configured paths.");
@@ -229,7 +242,7 @@ export async function showLocal(input) {
229
242
  if (!renderer) {
230
243
  throw new UsageError(`Renderer "${match.renderer}" not found for asset: ${displayType}:${parsed.name}`);
231
244
  }
232
- const renderCtx = buildRenderContext(fileCtx, match, allStashDirs, source?.registryId);
245
+ const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs, source?.registryId);
233
246
  const response = renderer.buildShowResponse(renderCtx);
234
247
  const editable = isEditable(assetPath, config);
235
248
  const fullResponse = {
@@ -243,6 +256,20 @@ export async function showLocal(input) {
243
256
  }
244
257
  return fullResponse;
245
258
  }
259
+ /**
260
+ * Minimal `show`: ref → indexer lookup → file contents. Used by callers that
261
+ * just need the raw file (e.g. clone, write-source) and don't want the full
262
+ * renderer graph. Spec §6.2's literal flow.
263
+ */
264
+ export async function showByRef(ref) {
265
+ const parsed = parseAssetRef(ref);
266
+ const entry = await lookup(parsed);
267
+ if (!entry) {
268
+ throw new NotFoundError(`Asset not found for ref: ${parsed.type}:${parsed.name}`);
269
+ }
270
+ const body = await fs.promises.readFile(entry.filePath, "utf8");
271
+ return { filePath: entry.filePath, body };
272
+ }
246
273
  /**
247
274
  * Build a compact summary response from a full ShowResponse.
248
275
  *
@@ -250,24 +277,17 @@ export async function showLocal(input) {
250
277
  * type, name, path, description, tags, parameters, action.
251
278
  * Enriches description and tags from frontmatter or .stash.json when available.
252
279
  *
253
- * Enrichment via frontmatter and .stash.json is only performed when `assetPath`
254
- * is supplied (local assets). Remote provider responses (e.g. OpenViking) rely
255
- * on the provider having already populated description and tags.
256
- *
257
280
  * The resulting JSON should be under 200 tokens.
258
281
  */
259
282
  function buildSummaryResponse(full, assetPath) {
260
- // Try to enrich metadata from .stash.json if description or tags are missing
261
283
  let description = full.description;
262
284
  let tags = full.tags;
263
285
  if (assetPath) {
264
- // Try frontmatter extraction from content fields
265
286
  const textContent = full.content ?? full.template ?? full.prompt;
266
287
  if (textContent && !description) {
267
288
  const parsed = parseFrontmatter(textContent);
268
289
  description = toStringOrUndefined(parsed.data.description);
269
290
  }
270
- // Try .stash.json for richer metadata (tags especially)
271
291
  const dir = path.dirname(assetPath);
272
292
  const stashFile = loadStashFile(dir);
273
293
  if (stashFile) {
@@ -1,17 +1,17 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { isHttpUrl, resolveStashDir } from "./common";
4
- import { loadConfig, loadUserConfig, saveConfig } from "./config";
5
- import { UsageError } from "./errors";
6
- import { akmIndex } from "./indexer";
3
+ import { isHttpUrl, resolveStashDir } from "../core/common";
4
+ import { loadConfig, loadUserConfig, saveConfig } from "../core/config";
5
+ import { UsageError } from "../core/errors";
6
+ import { warn } from "../core/warn";
7
+ import { akmIndex } from "../indexer/indexer";
8
+ import { upsertLockEntry } from "../integrations/lockfile";
9
+ import { parseRegistryRef } from "../registry/resolve";
10
+ import { detectStashRoot } from "../sources/providers/provider-utils";
11
+ import { syncFromRef } from "../sources/providers/sync-from-ref";
12
+ import { ensureWebsiteMirror, validateWebsiteInputUrl } from "../sources/providers/website";
13
+ import { ensureWikiNameAvailable, validateWikiName } from "../wiki/wiki";
7
14
  import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
8
- import { upsertLockEntry } from "./lockfile";
9
- import { parseRegistryRef } from "./registry-resolve";
10
- import { detectStashRoot } from "./stash-providers/provider-utils";
11
- import { syncFromRef } from "./stash-providers/sync-from-ref";
12
- import { ensureWebsiteMirror, validateWebsiteInputUrl } from "./stash-providers/website";
13
- import { warn } from "./warn";
14
- import { ensureWikiNameAvailable, validateWikiName } from "./wiki";
15
15
  const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
16
16
  export async function akmAdd(input) {
17
17
  const ref = input.ref.trim();
@@ -32,7 +32,7 @@ export async function akmAdd(input) {
32
32
  }
33
33
  const stashDir = resolveStashDir();
34
34
  if (shouldAddAsWebsiteUrl(ref)) {
35
- return addWebsiteStashSource(ref, stashDir, input.name ?? wikiName, input.options, wikiName);
35
+ return addWebsiteSource(ref, stashDir, input.name ?? wikiName, input.options, wikiName);
36
36
  }
37
37
  // Detect local directory refs and route them to stashes[] instead of installed[]
38
38
  try {
@@ -41,7 +41,7 @@ export async function akmAdd(input) {
41
41
  if (input.trustThisInstall) {
42
42
  warn("--trust has no effect on local directory sources; the install audit is not run for local paths.");
43
43
  }
44
- return addLocalStashSource(ref, parsed.sourcePath, stashDir, wikiName);
44
+ return addLocalSource(ref, parsed.sourcePath, stashDir, wikiName, input.name);
45
45
  }
46
46
  }
47
47
  catch {
@@ -67,29 +67,39 @@ export async function registerWikiSource(input) {
67
67
  * Add a local directory as a filesystem stash source.
68
68
  * Creates a stashes[] entry instead of an installed[] entry.
69
69
  */
70
- async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
70
+ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName) {
71
71
  const stashRoot = detectStashRoot(sourcePath);
72
72
  const resolvedPath = path.resolve(stashRoot);
73
73
  const config = loadUserConfig();
74
- // Check for duplicates in stashes[]
75
- const stashes = [...(config.stashes ?? [])];
76
- const existing = stashes.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
74
+ // Derive the canonical name: explicit --name wins, then wiki name, then readable path.
75
+ const derivedName = explicitName ?? wikiName ?? toReadableId(resolvedPath);
76
+ // Check for duplicates in sources[]
77
+ const sources = [...(config.sources ?? config.stashes ?? [])];
78
+ const existing = sources.find((s) => s.type === "filesystem" && s.path && path.resolve(s.path) === resolvedPath);
77
79
  let persistedEntry;
78
80
  if (!existing) {
79
81
  persistedEntry = {
80
82
  type: "filesystem",
81
83
  path: resolvedPath,
82
- name: wikiName ?? toReadableId(resolvedPath),
84
+ name: derivedName,
83
85
  ...(wikiName ? { wikiName } : {}),
84
86
  };
85
- stashes.push(persistedEntry);
86
- saveConfig({ ...config, stashes });
87
+ sources.push(persistedEntry);
88
+ saveConfig({ ...config, sources, stashes: undefined });
87
89
  }
88
90
  else {
91
+ let changed = false;
92
+ // If --name was explicitly supplied, update the persisted name.
93
+ if (explicitName && existing.name !== explicitName) {
94
+ existing.name = explicitName;
95
+ changed = true;
96
+ }
89
97
  if (wikiName && existing.wikiName !== wikiName) {
90
98
  existing.wikiName = wikiName;
91
- saveConfig({ ...config, stashes });
99
+ changed = true;
92
100
  }
101
+ if (changed)
102
+ saveConfig({ ...config, sources, stashes: undefined });
93
103
  persistedEntry = existing;
94
104
  }
95
105
  const index = await akmIndex({ stashDir });
@@ -98,7 +108,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
98
108
  schemaVersion: 1,
99
109
  stashDir,
100
110
  ref: wikiName ?? ref,
101
- stashSource: {
111
+ sourceAdded: {
102
112
  type: "filesystem",
103
113
  path: resolvedPath,
104
114
  name: persistedEntry.name ?? toReadableId(resolvedPath),
@@ -106,7 +116,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
106
116
  ...(persistedEntry.wikiName ? { wiki: persistedEntry.wikiName } : {}),
107
117
  },
108
118
  config: {
109
- stashCount: updatedConfig.stashes?.length ?? 0,
119
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
110
120
  installedKitCount: updatedConfig.installed?.length ?? 0,
111
121
  },
112
122
  index: {
@@ -118,11 +128,11 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
118
128
  },
119
129
  };
120
130
  }
121
- async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
131
+ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
122
132
  const normalizedUrl = validateWebsiteInputUrl(ref);
123
133
  const config = loadUserConfig();
124
- const stashes = [...(config.stashes ?? [])];
125
- let entry = stashes.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
134
+ const sources = [...(config.sources ?? config.stashes ?? [])];
135
+ let entry = sources.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
126
136
  if (!entry) {
127
137
  entry = {
128
138
  type: "website",
@@ -131,8 +141,8 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
131
141
  ...(options && Object.keys(options).length > 0 ? { options } : {}),
132
142
  ...(wikiName ? { wikiName } : {}),
133
143
  };
134
- stashes.push(entry);
135
- saveConfig({ ...config, stashes });
144
+ sources.push(entry);
145
+ saveConfig({ ...config, sources, stashes: undefined });
136
146
  }
137
147
  else {
138
148
  let changed = false;
@@ -145,7 +155,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
145
155
  changed = true;
146
156
  }
147
157
  if (changed)
148
- saveConfig({ ...config, stashes });
158
+ saveConfig({ ...config, sources, stashes: undefined });
149
159
  }
150
160
  const cachePaths = await ensureWebsiteMirror(entry, { requireStashDir: true });
151
161
  const index = await akmIndex({ stashDir });
@@ -154,7 +164,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
154
164
  schemaVersion: 1,
155
165
  stashDir,
156
166
  ref: wikiName ?? ref,
157
- stashSource: {
167
+ sourceAdded: {
158
168
  type: "website",
159
169
  url: normalizedUrl,
160
170
  name: entry.name,
@@ -162,7 +172,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
162
172
  ...(entry.wikiName ? { wiki: entry.wikiName } : {}),
163
173
  },
164
174
  config: {
165
- stashCount: updatedConfig.stashes?.length ?? 0,
175
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
166
176
  installedKitCount: updatedConfig.installed?.length ?? 0,
167
177
  },
168
178
  index: {
@@ -254,7 +264,7 @@ async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiN
254
264
  audit,
255
265
  },
256
266
  config: {
257
- stashCount: updatedConfig.stashes?.length ?? 0,
267
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
258
268
  installedKitCount: updatedConfig.installed?.length ?? 0,
259
269
  },
260
270
  index: {
@@ -1,18 +1,18 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { TYPE_DIRS } from "./asset-spec";
4
- import { UsageError } from "./errors";
5
- import { isRemoteOrigin, resolveSourcesForOrigin } from "./origin-resolve";
6
- import { findSourceForPath, getPrimarySource, resolveStashSources } from "./search-source";
7
- import { syncFromRef } from "./stash-providers/sync-from-ref";
8
- import { makeAssetRef, parseAssetRef } from "./stash-ref";
9
- import { resolveAssetPath } from "./stash-resolve";
3
+ import { makeAssetRef, parseAssetRef } from "../core/asset-ref";
4
+ import { TYPE_DIRS } from "../core/asset-spec";
5
+ import { NotFoundError, UsageError } from "../core/errors";
6
+ import { findSourceForPath, getPrimarySource, resolveSourceEntries } from "../indexer/search-source";
7
+ import { isRemoteOrigin, resolveSourcesForOrigin } from "../registry/origin-resolve";
8
+ import { syncFromRef } from "../sources/providers/sync-from-ref";
9
+ import { resolveAssetPath } from "../sources/resolve";
10
10
  export async function akmClone(options) {
11
11
  const parsed = parseAssetRef(options.sourceRef);
12
12
  // When --dest is provided, the working stash is optional
13
13
  let allSources;
14
14
  try {
15
- allSources = resolveStashSources();
15
+ allSources = resolveSourceEntries();
16
16
  }
17
17
  catch (err) {
18
18
  if (options.dest) {
@@ -56,8 +56,10 @@ export async function akmClone(options) {
56
56
  }
57
57
  }
58
58
  if (!sourcePath) {
59
- const context = remoteFetched ? ` (remote package fetched but asset not found inside it)` : "";
60
- throw lastError ?? new Error(`Source asset not found for ref: ${options.sourceRef}${context}`);
59
+ if (remoteFetched) {
60
+ throw new NotFoundError(`Source asset not found for ref: ${options.sourceRef} (remote package fetched but asset not found inside it)`, "ASSET_NOT_FOUND", "The remote package was fetched but doesn't contain the requested asset. Check the asset name and type.");
61
+ }
62
+ throw lastError ?? new NotFoundError(`Source asset not found for ref: ${options.sourceRef}`, "ASSET_NOT_FOUND");
61
63
  }
62
64
  const sourceSource = findSourceForPath(sourcePath, allSources);
63
65
  const destName = options.newName ?? parsed.name;
@@ -1,19 +1,19 @@
1
1
  import path from "node:path";
2
- import { loadConfig, loadUserConfig, saveConfig } from "./config";
3
- import { UsageError } from "./errors";
4
- import { resolveStashSources } from "./search-source";
2
+ import { loadConfig, loadUserConfig, saveConfig } from "../core/config";
3
+ import { UsageError } from "../core/errors";
4
+ import { resolveSourceEntries } from "../indexer/search-source";
5
5
  // ── Operations ──────────────────────────────────────────────────────────────
6
6
  /**
7
7
  * Add a stash source (filesystem path or remote provider URL) to config.
8
8
  *
9
9
  * Filesystem paths are auto-detected when `target` does not start with
10
10
  * `http://` or `https://`. URL sources require a `providerType` option
11
- * (e.g. "openviking").
11
+ * (e.g. "website", "git").
12
12
  */
13
13
  export function addStash(opts) {
14
14
  const { target, name, providerType, options: providerOptions, writable } = opts;
15
15
  const config = loadUserConfig();
16
- const stashes = [...(config.stashes ?? [])];
16
+ const sources = [...(config.sources ?? config.stashes ?? [])];
17
17
  const isRemoteUrl = target.startsWith("http://") ||
18
18
  target.startsWith("https://") ||
19
19
  target.startsWith("git@") ||
@@ -22,11 +22,11 @@ export function addStash(opts) {
22
22
  let entry;
23
23
  if (isRemoteUrl) {
24
24
  if (!providerType) {
25
- throw new UsageError("--provider is required for URL sources (e.g. --provider openviking)");
25
+ throw new UsageError("--provider is required for URL sources (e.g. --provider git --provider website)");
26
26
  }
27
27
  // Deduplicate by URL
28
- if (stashes.some((s) => s.url === target)) {
29
- return { stashes, added: false, message: "Source URL already configured" };
28
+ if (sources.some((s) => s.url === target)) {
29
+ return { sources, added: false, message: "Source URL already configured" };
30
30
  }
31
31
  entry = { type: providerType, url: target };
32
32
  if (name)
@@ -39,16 +39,16 @@ export function addStash(opts) {
39
39
  else {
40
40
  // Filesystem path
41
41
  const resolvedPath = path.resolve(target);
42
- if (stashes.some((s) => s.path && path.resolve(s.path) === resolvedPath)) {
43
- return { stashes, added: false, message: "Source path already configured" };
42
+ if (sources.some((s) => s.path && path.resolve(s.path) === resolvedPath)) {
43
+ return { sources, added: false, message: "Source path already configured" };
44
44
  }
45
45
  entry = { type: "filesystem", path: resolvedPath };
46
46
  if (name)
47
47
  entry.name = name;
48
48
  }
49
- stashes.push(entry);
50
- saveConfig({ ...config, stashes });
51
- return { stashes, added: true, entry };
49
+ sources.push(entry);
50
+ saveConfig({ ...config, sources, stashes: undefined });
51
+ return { sources, added: true, entry };
52
52
  }
53
53
  /**
54
54
  * Remove a stash source by URL, path, or name.
@@ -56,7 +56,7 @@ export function addStash(opts) {
56
56
  */
57
57
  export function removeStash(target) {
58
58
  const config = loadUserConfig();
59
- const stashes = [...(config.stashes ?? [])];
59
+ const sources = [...(config.sources ?? config.stashes ?? [])];
60
60
  const isUrl = target.startsWith("http://") ||
61
61
  target.startsWith("https://") ||
62
62
  target.startsWith("git@") ||
@@ -66,27 +66,27 @@ export function removeStash(target) {
66
66
  // Try URL match first, then path, then name (most specific → least specific)
67
67
  let idx = -1;
68
68
  if (isUrl) {
69
- idx = stashes.findIndex((s) => s.url === target);
69
+ idx = sources.findIndex((s) => s.url === target);
70
70
  }
71
71
  if (idx === -1 && resolvedPath) {
72
- idx = stashes.findIndex((s) => s.path && path.resolve(s.path) === resolvedPath);
72
+ idx = sources.findIndex((s) => s.path && path.resolve(s.path) === resolvedPath);
73
73
  }
74
74
  if (idx === -1) {
75
- idx = stashes.findIndex((s) => s.name === target);
75
+ idx = sources.findIndex((s) => s.name === target);
76
76
  }
77
77
  if (idx === -1) {
78
- return { stashes, removed: false, message: "No matching source found" };
78
+ return { sources, removed: false, message: "No matching source found" };
79
79
  }
80
- const removed = stashes.splice(idx, 1)[0];
81
- saveConfig({ ...config, stashes });
82
- return { stashes, removed: true, entry: removed };
80
+ const removed = sources.splice(idx, 1)[0];
81
+ saveConfig({ ...config, sources, stashes: undefined });
82
+ return { sources, removed: true, entry: removed };
83
83
  }
84
84
  /**
85
85
  * List all stash sources (local filesystem + configured stashes).
86
86
  */
87
87
  export function listStashes() {
88
88
  const config = loadConfig();
89
- const localSources = resolveStashSources();
90
- const stashes = config.stashes ?? [];
91
- return { localSources, stashes };
89
+ const localSources = resolveSourceEntries();
90
+ const sources = config.sources ?? config.stashes ?? [];
91
+ return { localSources, sources };
92
92
  }
@@ -66,6 +66,49 @@ export function listKeys(vaultPath) {
66
66
  const text = fs.readFileSync(vaultPath, "utf8");
67
67
  return { keys: scanKeys(text), comments: scanComments(text) };
68
68
  }
69
+ /**
70
+ * Return structured `entries` pairing each key with the nearest preceding
71
+ * comment line (if any). This replaces the parallel `keys[]` + `comments[]`
72
+ * shape used internally by `listKeys` with a single merged array, which is
73
+ * easier for callers to consume (QA #35).
74
+ *
75
+ * Values are never included — the same privacy guarantee as `listKeys`.
76
+ */
77
+ export function listEntries(vaultPath) {
78
+ if (!fs.existsSync(vaultPath))
79
+ return [];
80
+ const text = fs.readFileSync(vaultPath, "utf8");
81
+ const lines = text.split(/\r?\n/);
82
+ const seen = new Set();
83
+ const entries = [];
84
+ let pendingComment;
85
+ for (const line of lines) {
86
+ const trimmed = line.trimStart();
87
+ if (trimmed.startsWith("#")) {
88
+ // Capture the most recent comment before a key
89
+ pendingComment = trimmed.slice(1).trimStart() || undefined;
90
+ continue;
91
+ }
92
+ const m = line.match(ASSIGN_RE);
93
+ if (m) {
94
+ const key = m[1];
95
+ if (!seen.has(key)) {
96
+ seen.add(key);
97
+ const entry = { key };
98
+ if (pendingComment)
99
+ entry.comment = pendingComment;
100
+ entries.push(entry);
101
+ }
102
+ pendingComment = undefined;
103
+ }
104
+ else {
105
+ // Any non-comment, non-assignment line (including blank lines)
106
+ // breaks "nearest preceding comment line" association.
107
+ pendingComment = undefined;
108
+ }
109
+ }
110
+ return entries;
111
+ }
69
112
  /**
70
113
  * Read all KEY=value pairs from a vault file. Intended for programmatic
71
114
  * callers that need to inject values into a process environment. Callers