akm-cli 0.5.0 → 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 (118) hide show
  1. package/CHANGELOG.md +53 -5
  2. package/README.md +9 -9
  3. package/dist/cli.js +379 -1448
  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/commands/curate.js +263 -0
  7. package/dist/{info.js → commands/info.js} +17 -11
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +14 -2
  10. package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
  11. package/dist/commands/migration-help.js +141 -0
  12. package/dist/{registry-search.js → commands/registry-search.js} +68 -9
  13. package/dist/commands/remember.js +178 -0
  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} +106 -81
  17. package/dist/{stash-add.js → commands/source-add.js} +133 -67
  18. package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
  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} +30 -6
  23. package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
  24. package/dist/{common.js → core/common.js} +147 -50
  25. package/dist/{config.js → core/config.js} +288 -29
  26. package/dist/core/errors.js +90 -0
  27. package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
  28. package/dist/{paths.js → core/paths.js} +4 -4
  29. package/dist/core/write-source.js +280 -0
  30. package/dist/{local-search.js → indexer/db-search.js} +49 -32
  31. package/dist/{db.js → indexer/db.js} +210 -81
  32. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  33. package/dist/{indexer.js → indexer/indexer.js} +153 -30
  34. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  35. package/dist/{matchers.js → indexer/matchers.js} +4 -7
  36. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  37. package/dist/{search-source.js → indexer/search-source.js} +97 -55
  38. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  39. package/dist/{walker.js → indexer/walker.js} +1 -1
  40. package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
  41. package/dist/{llm.js → llm/client.js} +12 -48
  42. package/dist/llm/embedder.js +127 -0
  43. package/dist/llm/embedders/cache.js +47 -0
  44. package/dist/llm/embedders/local.js +152 -0
  45. package/dist/llm/embedders/remote.js +121 -0
  46. package/dist/llm/embedders/types.js +39 -0
  47. package/dist/llm/metadata-enhance.js +53 -0
  48. package/dist/output/cli-hints.js +301 -0
  49. package/dist/output/context.js +95 -0
  50. package/dist/{renderers.js → output/renderers.js} +57 -61
  51. package/dist/output/shapes.js +212 -0
  52. package/dist/output/text.js +520 -0
  53. package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
  54. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  55. package/dist/registry/factory.js +33 -0
  56. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  57. package/dist/registry/providers/index.js +11 -0
  58. package/dist/{providers → registry/providers}/skills-sh.js +60 -4
  59. package/dist/{providers → registry/providers}/static-index.js +126 -56
  60. package/dist/registry/providers/types.js +25 -0
  61. package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
  62. package/dist/{detect.js → setup/detect.js} +0 -27
  63. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  64. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  65. package/dist/{setup.js → setup/setup.js} +162 -129
  66. package/dist/setup/steps.js +45 -0
  67. package/dist/{kit-include.js → sources/include.js} +1 -1
  68. package/dist/sources/provider-factory.js +36 -0
  69. package/dist/sources/provider.js +21 -0
  70. package/dist/sources/providers/filesystem.js +35 -0
  71. package/dist/{stash-providers → sources/providers}/git.js +218 -28
  72. package/dist/{stash-providers → sources/providers}/index.js +4 -4
  73. package/dist/sources/providers/install-types.js +14 -0
  74. package/dist/sources/providers/npm.js +160 -0
  75. package/dist/sources/providers/provider-utils.js +173 -0
  76. package/dist/sources/providers/sync-from-ref.js +45 -0
  77. package/dist/sources/providers/tar-utils.js +154 -0
  78. package/dist/{stash-providers → sources/providers}/website.js +60 -20
  79. package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
  80. package/dist/{wiki.js → wiki/wiki.js} +18 -17
  81. package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
  82. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  83. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  84. package/dist/workflows/document-cache.js +20 -0
  85. package/dist/workflows/parser.js +379 -0
  86. package/dist/workflows/renderer.js +78 -0
  87. package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
  88. package/dist/workflows/schema.js +11 -0
  89. package/dist/workflows/validator.js +48 -0
  90. package/docs/README.md +30 -0
  91. package/docs/migration/release-notes/0.0.13.md +4 -0
  92. package/docs/migration/release-notes/0.1.0.md +6 -0
  93. package/docs/migration/release-notes/0.2.0.md +6 -0
  94. package/docs/migration/release-notes/0.3.0.md +5 -0
  95. package/docs/migration/release-notes/0.5.0.md +6 -0
  96. package/docs/migration/release-notes/0.6.0.md +75 -0
  97. package/docs/migration/release-notes/README.md +21 -0
  98. package/package.json +3 -2
  99. package/dist/embedder.js +0 -351
  100. package/dist/errors.js +0 -34
  101. package/dist/migration-help.js +0 -110
  102. package/dist/registry-factory.js +0 -19
  103. package/dist/registry-install.js +0 -532
  104. package/dist/ripgrep.js +0 -2
  105. package/dist/stash-provider-factory.js +0 -35
  106. package/dist/stash-provider.js +0 -1
  107. package/dist/stash-providers/filesystem.js +0 -41
  108. package/dist/stash-providers/openviking.js +0 -348
  109. package/dist/stash-providers/provider-utils.js +0 -11
  110. package/dist/stash-types.js +0 -1
  111. package/dist/workflow-markdown.js +0 -251
  112. /package/dist/{markdown.js → core/markdown.js} +0 -0
  113. /package/dist/{warn.js → core/warn.js} +0 -0
  114. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  115. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  116. /package/dist/{github.js → integrations/github.js} +0 -0
  117. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  118. /package/dist/{registry-types.js → sources/types.js} +0 -0
@@ -1,21 +1,23 @@
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";
7
- import { upsertLockEntry } from "./lockfile";
8
- import { detectStashRoot, installRegistryRef, upsertInstalledRegistryEntry } from "./registry-install";
9
- import { parseRegistryRef } from "./registry-resolve";
10
- import { ensureWebsiteMirror, validateWebsiteInputUrl } from "./stash-providers/website";
11
- import { warn } from "./warn";
12
- import { ensureWikiNameAvailable, validateWikiName } from "./wiki";
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";
14
+ import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
13
15
  const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
14
16
  export async function akmAdd(input) {
15
17
  const ref = input.ref.trim();
16
18
  if (!ref)
17
19
  throw new UsageError("Install ref or local directory is required. " +
18
- "Examples: `akm add @scope/kit`, `akm add github:owner/repo`, `akm add ./local/path`");
20
+ "Examples: `akm add @scope/stash`, `akm add github:owner/repo`, `akm add ./local/path`");
19
21
  // Validate and resolve wiki name when --type wiki is used
20
22
  let wikiName;
21
23
  if (input.overrideType) {
@@ -30,7 +32,7 @@ export async function akmAdd(input) {
30
32
  }
31
33
  const stashDir = resolveStashDir();
32
34
  if (shouldAddAsWebsiteUrl(ref)) {
33
- return addWebsiteStashSource(ref, stashDir, input.name ?? wikiName, input.options, wikiName);
35
+ return addWebsiteSource(ref, stashDir, input.name ?? wikiName, input.options, wikiName);
34
36
  }
35
37
  // Detect local directory refs and route them to stashes[] instead of installed[]
36
38
  try {
@@ -39,13 +41,13 @@ export async function akmAdd(input) {
39
41
  if (input.trustThisInstall) {
40
42
  warn("--trust has no effect on local directory sources; the install audit is not run for local paths.");
41
43
  }
42
- return addLocalStashSource(ref, parsed.sourcePath, stashDir, wikiName);
44
+ return addLocalSource(ref, parsed.sourcePath, stashDir, wikiName, input.name);
43
45
  }
44
46
  }
45
47
  catch {
46
48
  // Not a local ref — fall through to registry install
47
49
  }
48
- return addRegistryKit(ref, stashDir, input.trustThisInstall, input.writable, wikiName);
50
+ return addRegistryStash(ref, stashDir, input.trustThisInstall, input.writable, wikiName);
49
51
  }
50
52
  export async function registerWikiSource(input) {
51
53
  const stashDir = resolveStashDir();
@@ -65,29 +67,39 @@ export async function registerWikiSource(input) {
65
67
  * Add a local directory as a filesystem stash source.
66
68
  * Creates a stashes[] entry instead of an installed[] entry.
67
69
  */
68
- async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
70
+ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName) {
69
71
  const stashRoot = detectStashRoot(sourcePath);
70
72
  const resolvedPath = path.resolve(stashRoot);
71
73
  const config = loadUserConfig();
72
- // Check for duplicates in stashes[]
73
- const stashes = [...(config.stashes ?? [])];
74
- 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);
75
79
  let persistedEntry;
76
80
  if (!existing) {
77
81
  persistedEntry = {
78
82
  type: "filesystem",
79
83
  path: resolvedPath,
80
- name: wikiName ?? toReadableId(resolvedPath),
84
+ name: derivedName,
81
85
  ...(wikiName ? { wikiName } : {}),
82
86
  };
83
- stashes.push(persistedEntry);
84
- saveConfig({ ...config, stashes });
87
+ sources.push(persistedEntry);
88
+ saveConfig({ ...config, sources, stashes: undefined });
85
89
  }
86
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
+ }
87
97
  if (wikiName && existing.wikiName !== wikiName) {
88
98
  existing.wikiName = wikiName;
89
- saveConfig({ ...config, stashes });
99
+ changed = true;
90
100
  }
101
+ if (changed)
102
+ saveConfig({ ...config, sources, stashes: undefined });
91
103
  persistedEntry = existing;
92
104
  }
93
105
  const index = await akmIndex({ stashDir });
@@ -96,7 +108,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
96
108
  schemaVersion: 1,
97
109
  stashDir,
98
110
  ref: wikiName ?? ref,
99
- stashSource: {
111
+ sourceAdded: {
100
112
  type: "filesystem",
101
113
  path: resolvedPath,
102
114
  name: persistedEntry.name ?? toReadableId(resolvedPath),
@@ -104,7 +116,7 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
104
116
  ...(persistedEntry.wikiName ? { wiki: persistedEntry.wikiName } : {}),
105
117
  },
106
118
  config: {
107
- stashCount: updatedConfig.stashes?.length ?? 0,
119
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
108
120
  installedKitCount: updatedConfig.installed?.length ?? 0,
109
121
  },
110
122
  index: {
@@ -116,11 +128,11 @@ async function addLocalStashSource(ref, sourcePath, stashDir, wikiName) {
116
128
  },
117
129
  };
118
130
  }
119
- async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
131
+ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
120
132
  const normalizedUrl = validateWebsiteInputUrl(ref);
121
133
  const config = loadUserConfig();
122
- const stashes = [...(config.stashes ?? [])];
123
- 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);
124
136
  if (!entry) {
125
137
  entry = {
126
138
  type: "website",
@@ -129,8 +141,8 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
129
141
  ...(options && Object.keys(options).length > 0 ? { options } : {}),
130
142
  ...(wikiName ? { wikiName } : {}),
131
143
  };
132
- stashes.push(entry);
133
- saveConfig({ ...config, stashes });
144
+ sources.push(entry);
145
+ saveConfig({ ...config, sources, stashes: undefined });
134
146
  }
135
147
  else {
136
148
  let changed = false;
@@ -143,7 +155,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
143
155
  changed = true;
144
156
  }
145
157
  if (changed)
146
- saveConfig({ ...config, stashes });
158
+ saveConfig({ ...config, sources, stashes: undefined });
147
159
  }
148
160
  const cachePaths = await ensureWebsiteMirror(entry, { requireStashDir: true });
149
161
  const index = await akmIndex({ stashDir });
@@ -152,7 +164,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
152
164
  schemaVersion: 1,
153
165
  stashDir,
154
166
  ref: wikiName ?? ref,
155
- stashSource: {
167
+ sourceAdded: {
156
168
  type: "website",
157
169
  url: normalizedUrl,
158
170
  name: entry.name,
@@ -160,7 +172,7 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
160
172
  ...(entry.wikiName ? { wiki: entry.wikiName } : {}),
161
173
  },
162
174
  config: {
163
- stashCount: updatedConfig.stashes?.length ?? 0,
175
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
164
176
  installedKitCount: updatedConfig.installed?.length ?? 0,
165
177
  },
166
178
  index: {
@@ -173,34 +185,59 @@ async function addWebsiteStashSource(ref, stashDir, name, options, wikiName) {
173
185
  };
174
186
  }
175
187
  /**
176
- * Install a kit from a registry (npm, github, git).
188
+ * Install a stash from a registry (npm, github, git) by dispatching to the
189
+ * matching syncable provider, then running the post-sync install audit and
190
+ * persisting the lock entry.
177
191
  */
178
- async function addRegistryKit(ref, stashDir, trustThisInstall, writable, wikiName) {
179
- const installed = await installRegistryRef(ref, { trustThisInstall, writable });
180
- const replaced = (loadConfig().installed ?? []).find((entry) => entry.id === installed.id);
181
- const config = upsertInstalledRegistryEntry({
182
- id: installed.id,
183
- source: installed.source,
184
- ref: installed.ref,
185
- artifactUrl: installed.artifactUrl,
186
- resolvedVersion: installed.resolvedVersion,
187
- resolvedRevision: installed.resolvedRevision,
188
- stashRoot: installed.stashRoot,
189
- cacheDir: installed.cacheDir,
190
- installedAt: installed.installedAt,
191
- writable: installed.writable,
192
+ async function addRegistryStash(ref, stashDir, trustThisInstall, writable, wikiName) {
193
+ // Pre-sync registry-policy enforcement uses just the parsed ref (no fetch needed),
194
+ // so we keep parity with the historical behavior where `enforceRegistryInstallPolicy`
195
+ // ran before `extractTarGzSecure` etc.
196
+ const config = loadConfig();
197
+ const synced = await syncFromRef(ref, { trustThisInstall, writable });
198
+ const registryLabels = deriveRegistryLabels({
199
+ source: synced.source,
200
+ ref: synced.ref,
201
+ artifactUrl: synced.artifactUrl,
202
+ });
203
+ enforceRegistryInstallPolicy(registryLabels, config, ref);
204
+ // Post-sync hook: install audit. Throws when blocked unless `--trust` is set
205
+ // (in which case the audit report still surfaces in the response).
206
+ const audit = auditInstallCandidate({
207
+ rootDir: synced.extractedDir,
208
+ source: synced.source,
209
+ ref: synced.ref,
210
+ registryLabels,
211
+ config,
212
+ trustThisInstall,
213
+ });
214
+ if (audit.blocked) {
215
+ throw new Error(formatInstallAuditFailure(synced.ref, audit));
216
+ }
217
+ const replaced = (loadConfig().installed ?? []).find((entry) => entry.id === synced.id);
218
+ const updatedConfig = upsertInstalledRegistryEntry({
219
+ id: synced.id,
220
+ source: synced.source,
221
+ ref: synced.ref,
222
+ artifactUrl: synced.artifactUrl,
223
+ resolvedVersion: synced.resolvedVersion,
224
+ resolvedRevision: synced.resolvedRevision,
225
+ stashRoot: synced.contentDir,
226
+ cacheDir: synced.cacheDir,
227
+ installedAt: synced.syncedAt,
228
+ writable: synced.writable,
192
229
  ...(wikiName ? { wikiName } : {}),
193
230
  });
194
231
  await upsertLockEntry({
195
- id: installed.id,
196
- source: installed.source,
197
- ref: installed.ref,
198
- resolvedVersion: installed.resolvedVersion,
199
- resolvedRevision: installed.resolvedRevision,
200
- integrity: installed.integrity,
232
+ id: synced.id,
233
+ source: synced.source,
234
+ ref: synced.ref,
235
+ resolvedVersion: synced.resolvedVersion,
236
+ resolvedRevision: synced.resolvedRevision,
237
+ integrity: synced.integrity,
201
238
  });
202
239
  // Clean up old cache directory on re-install
203
- if (replaced && replaced.cacheDir !== installed.cacheDir) {
240
+ if (replaced && replaced.cacheDir !== synced.cacheDir) {
204
241
  try {
205
242
  fs.rmSync(replaced.cacheDir, { recursive: true, force: true });
206
243
  }
@@ -214,21 +251,21 @@ async function addRegistryKit(ref, stashDir, trustThisInstall, writable, wikiNam
214
251
  stashDir,
215
252
  ref,
216
253
  installed: {
217
- id: installed.id,
218
- source: installed.source,
219
- ref: installed.ref,
220
- artifactUrl: installed.artifactUrl,
221
- resolvedVersion: installed.resolvedVersion,
222
- resolvedRevision: installed.resolvedRevision,
223
- stashRoot: installed.stashRoot,
224
- cacheDir: installed.cacheDir,
225
- extractedDir: installed.extractedDir,
226
- installedAt: installed.installedAt,
227
- audit: installed.audit,
254
+ id: synced.id,
255
+ source: synced.source,
256
+ ref: synced.ref,
257
+ artifactUrl: synced.artifactUrl,
258
+ resolvedVersion: synced.resolvedVersion,
259
+ resolvedRevision: synced.resolvedRevision,
260
+ stashRoot: synced.contentDir,
261
+ cacheDir: synced.cacheDir,
262
+ extractedDir: synced.extractedDir,
263
+ installedAt: synced.syncedAt,
264
+ audit,
228
265
  },
229
266
  config: {
230
- stashCount: config.stashes?.length ?? 0,
231
- installedKitCount: config.installed?.length ?? 0,
267
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
268
+ installedKitCount: updatedConfig.installed?.length ?? 0,
232
269
  },
233
270
  index: {
234
271
  mode: index.mode,
@@ -239,6 +276,35 @@ async function addRegistryKit(ref, stashDir, trustThisInstall, writable, wikiNam
239
276
  },
240
277
  };
241
278
  }
279
+ /** Persist or replace an installed stash entry in the user config. */
280
+ export function upsertInstalledRegistryEntry(entry) {
281
+ const current = loadUserConfig();
282
+ const currentInstalled = current.installed ?? [];
283
+ const withoutExisting = currentInstalled.filter((item) => item.id !== entry.id);
284
+ const nextInstalled = [...withoutExisting, normalizeInstalledEntry(entry)];
285
+ const nextConfig = { ...current, installed: nextInstalled };
286
+ saveConfig(nextConfig);
287
+ return nextConfig;
288
+ }
289
+ /** Remove an installed stash entry from the user config. */
290
+ export function removeInstalledRegistryEntry(id) {
291
+ const current = loadUserConfig();
292
+ const currentInstalled = current.installed ?? [];
293
+ const nextInstalled = currentInstalled.filter((item) => item.id !== id);
294
+ const nextConfig = {
295
+ ...current,
296
+ installed: nextInstalled.length > 0 ? nextInstalled : undefined,
297
+ };
298
+ saveConfig(nextConfig);
299
+ return nextConfig;
300
+ }
301
+ function normalizeInstalledEntry(entry) {
302
+ return {
303
+ ...entry,
304
+ stashRoot: path.resolve(entry.stashRoot),
305
+ cacheDir: path.resolve(entry.cacheDir),
306
+ };
307
+ }
242
308
  function toReadableId(resolvedPath) {
243
309
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
244
310
  if (home && resolvedPath.startsWith(home + path.sep)) {
@@ -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 { installRegistryRef } from "./registry-install";
7
- import { findSourceForPath, getPrimarySource, resolveStashSources } from "./search-source";
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) {
@@ -31,16 +31,16 @@ export async function akmClone(options) {
31
31
  // Remote fetch fallback: if no local source matched and origin looks remote, fetch it
32
32
  let remoteFetched;
33
33
  if (searchSources.length === 0 && parsed.origin && isRemoteOrigin(parsed.origin, allSources)) {
34
- const installResult = await installRegistryRef(parsed.origin);
34
+ const installResult = await syncFromRef(parsed.origin);
35
35
  const syntheticSource = {
36
- path: installResult.stashRoot,
36
+ path: installResult.contentDir,
37
37
  registryId: installResult.id,
38
38
  };
39
39
  searchSources = [syntheticSource];
40
40
  allSources = [...allSources, syntheticSource];
41
41
  remoteFetched = {
42
42
  origin: parsed.origin,
43
- stashRoot: installResult.stashRoot,
43
+ stashRoot: installResult.contentDir,
44
44
  cacheDir: installResult.cacheDir,
45
45
  };
46
46
  }
@@ -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);
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Central registry for asset type renderer and action builder maps.
3
3
  *
4
- * Previously these maps lived in `local-search.ts` and were wired into
5
- * `asset-spec.ts` via a fragile `_setAssetTypeHooks` deferred callback
6
- * pattern. If `local-search.ts` was imported after `registerAssetType()`
7
- * calls, hooks would be silently dropped.
4
+ * Previously these maps lived in `db-search.ts` (then `local-search.ts`) and
5
+ * were wired into `asset-spec.ts` via a fragile `_setAssetTypeHooks` deferred
6
+ * callback pattern. If the search module was imported after
7
+ * `registerAssetType()` calls, hooks would be silently dropped.
8
8
  *
9
9
  * This module is a simple singleton that both `asset-spec.ts` and
10
- * `local-search.ts` import from, eliminating the import-order dependency
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",
@@ -53,3 +53,27 @@ export function registerTypeRenderer(type, rendererName) {
53
53
  export function registerActionBuilder(type, builder) {
54
54
  ACTION_BUILDERS[type] = builder;
55
55
  }
56
+ export const defaultRendererRegistry = {
57
+ rendererNameFor(type) {
58
+ return TYPE_TO_RENDERER[type];
59
+ },
60
+ actionBuilderFor(type) {
61
+ return ACTION_BUILDERS[type];
62
+ },
63
+ };
64
+ /**
65
+ * Build a registry from explicit maps. Useful for tests that need to assert
66
+ * rendering behavior without touching the global singletons.
67
+ */
68
+ export function createRendererRegistry(maps) {
69
+ const renderers = maps.renderers ?? {};
70
+ const actionBuilders = maps.actionBuilders ?? {};
71
+ return {
72
+ rendererNameFor(type) {
73
+ return renderers[type];
74
+ },
75
+ actionBuilderFor(type) {
76
+ return actionBuilders[type];
77
+ },
78
+ };
79
+ }