akm-cli 0.6.0-rc1 → 0.6.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.
Files changed (108) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +9 -9
  3. package/dist/cli.js +181 -111
  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} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +103 -78
  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} +79 -31
  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} +70 -47
  30. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  31. package/dist/{indexer.js → indexer/indexer.js} +123 -31
  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} +1 -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} +1 -1
  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} +12 -11
  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 +69 -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,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
@@ -30,7 +30,7 @@ export function makeAssetRef(type, name, origin) {
30
30
  export function parseAssetRef(ref) {
31
31
  const trimmed = ref.trim();
32
32
  if (!trimmed)
33
- throw new UsageError("Empty ref.");
33
+ throw new UsageError("Empty ref.", "MISSING_REQUIRED_ARGUMENT");
34
34
  let origin;
35
35
  let body = trimmed;
36
36
  const boundary = trimmed.indexOf("//");
@@ -38,16 +38,16 @@ export function parseAssetRef(ref) {
38
38
  origin = trimmed.slice(0, boundary);
39
39
  body = trimmed.slice(boundary + 2);
40
40
  if (!origin)
41
- throw new UsageError("Empty origin in ref.");
41
+ throw new UsageError("Empty origin in ref.", "MISSING_REQUIRED_ARGUMENT");
42
42
  }
43
43
  const colon = body.indexOf(":");
44
44
  if (colon <= 0) {
45
- throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name`);
45
+ throw new UsageError(`Invalid ref "${trimmed}". Expected [origin//]type:name, e.g. skill:deploy or knowledge:guide.md`, "MISSING_REQUIRED_ARGUMENT");
46
46
  }
47
47
  const rawType = body.slice(0, colon);
48
48
  const rawName = body.slice(colon + 1);
49
49
  if (!isAssetType(rawType)) {
50
- throw new UsageError(`Invalid asset type: "${rawType}".`);
50
+ throw new UsageError(`Invalid asset type: "${rawType}".`, "MISSING_REQUIRED_ARGUMENT");
51
51
  }
52
52
  validateName(rawName);
53
53
  const name = normalizeName(rawName);
@@ -10,7 +10,7 @@
10
10
  * `db-search.ts` import from, eliminating the import-order dependency
11
11
  * entirely.
12
12
  */
13
- import { buildWorkflowAction } from "./renderers";
13
+ import { buildWorkflowAction } from "../output/renderers";
14
14
  /** Map asset types to their primary renderer names. */
15
15
  export const TYPE_TO_RENDERER = {
16
16
  script: "script-source",
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
+ import { buildWorkflowAction } from "../output/renderers";
2
3
  import { registerActionBuilder, registerTypeRenderer } from "./asset-registry";
3
4
  import { toPosix } from "./common";
4
- import { buildWorkflowAction } from "./renderers";
5
5
  const markdownSpec = {
6
6
  isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
7
7
  toCanonicalName: (typeRoot, filePath) => {