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,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { getAssetTypes } from "./asset-spec";
4
+ import { getAssetTypes } from "../core/asset-spec";
5
5
  // ── Known flag values ────────────────────────────────────────────────────────
6
6
  const FLAG_VALUES = {
7
7
  "--format": ["json", "text", "yaml", "jsonl"],
@@ -1,9 +1,18 @@
1
- import { DEFAULT_CONFIG, } from "./config";
2
- import { UsageError } from "./errors";
1
+ import { DEFAULT_CONFIG, } from "../core/config";
2
+ import { UsageError } from "../core/errors";
3
+ // ── Merge helpers for LLM/embedding subkey set ───────────────────────────────
4
+ function mergeLlmLike(base, patch) {
5
+ return { endpoint: "", model: "", ...(base ?? {}), ...patch };
6
+ }
7
+ function mergeLlmLikeEmbedding(base, patch) {
8
+ return { endpoint: "", model: "", ...(base ?? {}), ...patch };
9
+ }
3
10
  export function parseConfigValue(key, value) {
4
11
  switch (key) {
5
12
  case "stashDir":
6
13
  return { stashDir: requireNonEmptyString(value, key) };
14
+ case "defaultWriteTarget":
15
+ return { defaultWriteTarget: requireNonEmptyString(value, key) };
7
16
  case "semanticSearchMode":
8
17
  // Accept legacy boolean-style strings from CLI
9
18
  if (value === "true")
@@ -16,12 +25,26 @@ export function parseConfigValue(key, value) {
16
25
  return { semanticSearchMode: value };
17
26
  case "embedding":
18
27
  return { embedding: parseEmbeddingConnectionValue(value) };
28
+ case "embedding.endpoint":
29
+ return { embedding: mergeLlmLikeEmbedding(undefined, { endpoint: requireNonEmptyString(value, key) }) };
30
+ case "embedding.model":
31
+ return { embedding: mergeLlmLikeEmbedding(undefined, { model: requireNonEmptyString(value, key) }) };
32
+ case "embedding.apiKey":
33
+ return { embedding: mergeLlmLikeEmbedding(undefined, { apiKey: requireNonEmptyString(value, key) }) };
19
34
  case "llm":
20
35
  return { llm: parseLlmConnectionValue(value) };
36
+ case "llm.endpoint":
37
+ return { llm: mergeLlmLike(undefined, { endpoint: requireNonEmptyString(value, key) }) };
38
+ case "llm.model":
39
+ return { llm: mergeLlmLike(undefined, { model: requireNonEmptyString(value, key) }) };
40
+ case "llm.apiKey":
41
+ return { llm: mergeLlmLike(undefined, { apiKey: requireNonEmptyString(value, key) }) };
21
42
  case "registries":
22
43
  return { registries: parseRegistriesValue(value) };
44
+ case "sources":
23
45
  case "stashes":
24
- return { stashes: parseStashesValue(value) };
46
+ // "stashes" is kept as an alias for backwards-compat; both write to `sources`.
47
+ return { sources: parseStashesValue(value) };
25
48
  case "output.format":
26
49
  return { output: { format: parseOutputFormat(value) } };
27
50
  case "output.detail":
@@ -46,16 +69,32 @@ export function getConfigValue(config, key) {
46
69
  switch (key) {
47
70
  case "stashDir":
48
71
  return config.stashDir ?? null;
72
+ case "defaultWriteTarget":
73
+ return config.defaultWriteTarget ?? null;
49
74
  case "semanticSearchMode":
50
75
  return config.semanticSearchMode;
51
76
  case "embedding":
52
77
  return config.embedding ?? null;
78
+ case "embedding.endpoint":
79
+ return config.embedding?.endpoint ?? null;
80
+ case "embedding.model":
81
+ return config.embedding?.model ?? null;
82
+ case "embedding.apiKey":
83
+ return config.embedding?.apiKey ?? null;
53
84
  case "llm":
54
85
  return config.llm ?? null;
86
+ case "llm.endpoint":
87
+ return config.llm?.endpoint ?? null;
88
+ case "llm.model":
89
+ return config.llm?.model ?? null;
90
+ case "llm.apiKey":
91
+ return config.llm?.apiKey ?? null;
55
92
  case "registries":
56
93
  return config.registries ?? DEFAULT_CONFIG.registries ?? [];
94
+ case "sources":
57
95
  case "stashes":
58
- return config.stashes ?? [];
96
+ // "stashes" is an alias for "sources" for backwards-compat.
97
+ return config.sources ?? config.stashes ?? [];
59
98
  case "output.format":
60
99
  return config.output?.format ?? null;
61
100
  case "output.detail":
@@ -85,6 +124,7 @@ export function setConfigValue(config, key, rawValue) {
85
124
  case "embedding":
86
125
  case "llm":
87
126
  case "registries":
127
+ case "sources":
88
128
  case "stashes":
89
129
  case "output.format":
90
130
  case "output.detail":
@@ -95,6 +135,38 @@ export function setConfigValue(config, key, rawValue) {
95
135
  case "security.installAudit.registryWhitelist":
96
136
  case "security.installAudit.allowedFindings":
97
137
  return mergeConfigValue(config, parseConfigValue(key, rawValue));
138
+ // Subkey setters use deep-merge so sibling fields are preserved
139
+ case "embedding.endpoint":
140
+ return {
141
+ ...config,
142
+ embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: requireNonEmptyString(rawValue, key) }),
143
+ };
144
+ case "embedding.model":
145
+ return {
146
+ ...config,
147
+ embedding: mergeLlmLikeEmbedding(config.embedding, { model: requireNonEmptyString(rawValue, key) }),
148
+ };
149
+ case "embedding.apiKey":
150
+ return {
151
+ ...config,
152
+ embedding: mergeLlmLikeEmbedding(config.embedding, { apiKey: requireNonEmptyString(rawValue, key) }),
153
+ };
154
+ case "llm.endpoint":
155
+ return { ...config, llm: mergeLlmLike(config.llm, { endpoint: requireNonEmptyString(rawValue, key) }) };
156
+ case "llm.model":
157
+ return { ...config, llm: mergeLlmLike(config.llm, { model: requireNonEmptyString(rawValue, key) }) };
158
+ case "llm.apiKey":
159
+ return { ...config, llm: mergeLlmLike(config.llm, { apiKey: requireNonEmptyString(rawValue, key) }) };
160
+ case "defaultWriteTarget": {
161
+ const name = requireNonEmptyString(rawValue, key);
162
+ const knownNames = (config.sources ?? config.stashes ?? [])
163
+ .map((s) => s.name)
164
+ .filter((n) => typeof n === "string");
165
+ if (knownNames.length > 0 && !knownNames.includes(name)) {
166
+ throw new UsageError(`Unknown source name "${name}" for defaultWriteTarget; configured source names: ${knownNames.map((n) => `"${n}"`).join(", ")}`);
167
+ }
168
+ return { ...config, defaultWriteTarget: name };
169
+ }
98
170
  default:
99
171
  throw new UsageError(`Unknown config key: ${key}`);
100
172
  }
@@ -103,14 +175,38 @@ export function unsetConfigValue(config, key) {
103
175
  switch (key) {
104
176
  case "stashDir":
105
177
  return { ...config, stashDir: undefined };
178
+ case "defaultWriteTarget":
179
+ return { ...config, defaultWriteTarget: undefined };
106
180
  case "embedding":
107
181
  return { ...config, embedding: undefined };
182
+ case "embedding.endpoint":
183
+ return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: "" }) };
184
+ case "embedding.model":
185
+ return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { model: "" }) };
186
+ case "embedding.apiKey": {
187
+ if (!config.embedding)
188
+ return config;
189
+ const { apiKey: _a, ...rest } = config.embedding;
190
+ return { ...config, embedding: rest };
191
+ }
108
192
  case "llm":
109
193
  return { ...config, llm: undefined };
194
+ case "llm.endpoint":
195
+ return { ...config, llm: mergeLlmLike(config.llm, { endpoint: "" }) };
196
+ case "llm.model":
197
+ return { ...config, llm: mergeLlmLike(config.llm, { model: "" }) };
198
+ case "llm.apiKey": {
199
+ if (!config.llm)
200
+ return config;
201
+ const { apiKey: _b, ...restLlm } = config.llm;
202
+ return { ...config, llm: restLlm };
203
+ }
110
204
  case "registries":
111
205
  return { ...config, registries: undefined };
206
+ case "sources":
112
207
  case "stashes":
113
- return { ...config, stashes: undefined };
208
+ // "stashes" is kept as an alias for backwards-compat; both clear `sources`.
209
+ return { ...config, sources: undefined, stashes: undefined };
114
210
  case "output.format":
115
211
  return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
116
212
  case "output.detail":
@@ -155,8 +251,10 @@ export function listConfig(config) {
155
251
  output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
156
252
  stashDir: config.stashDir ?? null,
157
253
  installed: config.installed ?? [],
158
- stashes: config.stashes ?? [],
254
+ sources: config.sources ?? config.stashes ?? [],
159
255
  };
256
+ if (config.defaultWriteTarget)
257
+ result.defaultWriteTarget = config.defaultWriteTarget;
160
258
  if (config.embedding)
161
259
  result.embedding = config.embedding;
162
260
  if (config.llm)
@@ -349,7 +447,7 @@ function parseJsonObject(value, key, example) {
349
447
  }
350
448
  catch {
351
449
  throw new UsageError(`Invalid value for ${key}: expected JSON object with endpoint and model` +
352
- ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`);
450
+ ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`, "INVALID_JSON_CONFIG_VALUE");
353
451
  }
354
452
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
355
453
  throw new UsageError(`Invalid value for ${key}: expected a JSON object`);
@@ -388,18 +486,18 @@ function parseStashesValue(value) {
388
486
  parsed = JSON.parse(value);
389
487
  }
390
488
  catch {
391
- throw new UsageError(`Invalid value for stashes: expected JSON array of {type, path?, url?, name?, enabled?, options?} objects`);
489
+ throw new UsageError(`Invalid value for sources: expected JSON array of {type, path?, url?, name?, enabled?, options?} objects`);
392
490
  }
393
491
  if (!Array.isArray(parsed)) {
394
- throw new UsageError(`Invalid value for stashes: expected a JSON array`);
492
+ throw new UsageError(`Invalid value for sources: expected a JSON array`);
395
493
  }
396
494
  return parsed.map((entry, i) => {
397
495
  if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
398
- throw new UsageError(`Invalid value for stashes[${i}]: expected an object with a "type" field`);
496
+ throw new UsageError(`Invalid value for sources[${i}]: expected an object with a "type" field`);
399
497
  }
400
498
  const obj = entry;
401
499
  if (typeof obj.type !== "string" || !obj.type) {
402
- throw new UsageError(`Invalid value for stashes[${i}]: "type" is required`);
500
+ throw new UsageError(`Invalid value for sources[${i}]: "type" is required`);
403
501
  }
404
502
  const result = { type: obj.type };
405
503
  if (typeof obj.path === "string" && obj.path)
@@ -10,9 +10,10 @@
10
10
  * pure helpers (`curateSearchResults`, `orderCuratedTypes`,
11
11
  * `deriveCurateFallbackQueries`) by importing them directly.
12
12
  */
13
- import { truncateDescription } from "./output-shapes";
14
- import { akmSearch, parseSearchSource } from "./stash-search";
15
- import { akmShowUnified } from "./stash-show";
13
+ import { UsageError } from "../core/errors";
14
+ import { truncateDescription } from "../output/shapes";
15
+ import { akmSearch, parseSearchSource } from "./search";
16
+ import { akmShowUnified } from "./show";
16
17
  const CURATE_FALLBACK_FILTER_WORDS = new Set([
17
18
  "a",
18
19
  "an",
@@ -39,6 +40,10 @@ const DEFAULT_CURATE_LIMIT = 4;
39
40
  * `options.searchResponse` is not supplied.
40
41
  */
41
42
  export async function akmCurate(options) {
43
+ const trimmedQuery = options.query.trim();
44
+ if (!trimmedQuery) {
45
+ throw new UsageError('A curation query is required. Usage: akm curate "<task or prompt>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT");
46
+ }
42
47
  const limit = options.limit && options.limit > 0 ? options.limit : DEFAULT_CURATE_LIMIT;
43
48
  const source = options.source ?? parseSearchSource("stash");
44
49
  const searchResponse = options.searchResponse ??
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs";
2
- import { getAssetTypes } from "./asset-spec";
3
- import { loadConfig } from "./config";
4
- import { closeDatabase, getEntryCount, getMeta, isVecAvailable, openDatabase } from "./db";
5
- import { getDbPath } from "./paths";
6
- import { getEffectiveSemanticStatus, readSemanticStatus } from "./semantic-status";
7
- import { pkgVersion } from "./version";
2
+ import { getAssetTypes } from "../core/asset-spec";
3
+ import { loadConfig } from "../core/config";
4
+ import { getDbPath } from "../core/paths";
5
+ import { closeDatabase, getEntryCount, getMeta, isVecAvailable, openDatabase } from "../indexer/db";
6
+ import { getEffectiveSemanticStatus, readSemanticStatus } from "../indexer/semantic-status";
7
+ import { pkgVersion } from "../version";
8
8
  /**
9
9
  * Assemble system info describing the current capabilities, configuration,
10
10
  * and index state. Used by `akm info`.
@@ -29,8 +29,14 @@ export function assembleInfo(options) {
29
29
  ...(r.provider ? { provider: r.provider } : {}),
30
30
  ...(r.enabled !== undefined ? { enabled: r.enabled } : {}),
31
31
  }));
32
- // Stash providers
33
- const stashProviders = (config.stashes ?? []).map((s) => ({
32
+ // Stash providers — prefer `sources[]`; fall back to `stashDir` when the
33
+ // user has not yet migrated to the sources[] config shape so that info
34
+ // always reflects at least one provider when a stash is configured.
35
+ const configuredSources = config.sources ?? config.stashes ?? [];
36
+ const stashesList = configuredSources.length === 0 && config.stashDir
37
+ ? [{ type: "filesystem", path: config.stashDir, name: "primary" }]
38
+ : configuredSources;
39
+ const sourceProviders = stashesList.map((s) => ({
34
40
  type: s.type,
35
41
  ...(s.name ? { name: s.name } : {}),
36
42
  ...(s.path ? { path: s.path } : {}),
@@ -51,7 +57,7 @@ export function assembleInfo(options) {
51
57
  ...(semanticRuntime?.message ? { message: semanticRuntime.message } : {}),
52
58
  },
53
59
  registries,
54
- stashProviders,
60
+ sourceProviders,
55
61
  indexStats,
56
62
  };
57
63
  }
@@ -7,10 +7,10 @@
7
7
  import { spawnSync } from "node:child_process";
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
- import { TYPE_DIRS } from "./asset-spec";
11
- import { getConfigPath, loadUserConfig, saveConfig } from "./config";
12
- import { getBinDir, getDefaultStashDir } from "./paths";
13
- import { ensureRg } from "./ripgrep-install";
10
+ import { TYPE_DIRS } from "../core/asset-spec";
11
+ import { getConfigPath, loadUserConfig, saveConfig } from "../core/config";
12
+ import { getBinDir, getDefaultStashDir } from "../core/paths";
13
+ import { ensureRg } from "../setup/ripgrep-install";
14
14
  export async function akmInit(options) {
15
15
  const stashDir = options?.dir ? path.resolve(options.dir) : getDefaultStashDir();
16
16
  let created = false;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { filterNonEmptyStrings } from "./common";
3
+ import { filterNonEmptyStrings, toPosix } from "../core/common";
4
4
  const DEFAULT_INSTALL_AUDIT_CONFIG = {
5
5
  enabled: true,
6
6
  blockOnCritical: true,
@@ -363,12 +363,9 @@ function matchesAllowedFinding(finding, ref, allowedFindings) {
363
363
  function normalizeWaiverPath(value) {
364
364
  if (!value)
365
365
  return value;
366
- // Strip a leading `./`, collapse duplicate slashes via path.normalize, and
367
- // POSIX-ify so Windows path separators don't trigger spurious mismatches.
368
- const normalized = path
369
- .normalize(value)
370
- .replace(/\\/g, "/")
371
- .replace(/^\.\/+/, "");
366
+ // Strip a leading `./` and POSIX-ify after path.normalize so Windows path
367
+ // separators don't trigger spurious mismatches.
368
+ const normalized = toPosix(path.normalize(value)).replace(/^\.\/+/, "");
372
369
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
373
370
  }
374
371
  function addUrlLabels(labels, rawUrl) {
@@ -6,36 +6,38 @@
6
6
  */
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
- import { resolveStashDir } from "./common";
10
- import { loadConfig } from "./config";
11
- import { NotFoundError, UsageError } from "./errors";
12
- import { akmIndex } from "./indexer";
9
+ import { resolveStashDir } from "../core/common";
10
+ import { loadConfig } from "../core/config";
11
+ import { NotFoundError, UsageError } from "../core/errors";
12
+ import { akmIndex } from "../indexer/indexer";
13
+ import { removeLockEntry, upsertLockEntry } from "../integrations/lockfile";
14
+ import { parseRegistryRef } from "../registry/resolve";
15
+ import { syncFromRef } from "../sources/providers/sync-from-ref";
16
+ import { ensureWebsiteMirror } from "../sources/providers/website";
13
17
  import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
14
- import { removeLockEntry, upsertLockEntry } from "./lockfile";
15
- import { parseRegistryRef } from "./registry-resolve";
16
- import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./stash-add";
17
- import { syncFromRef } from "./stash-providers/sync-from-ref";
18
- import { removeStash } from "./stash-source-manage";
18
+ import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add";
19
+ import { removeStash } from "./source-manage";
19
20
  export async function akmListSources(input) {
20
21
  const stashDir = input?.stashDir ?? resolveStashDir();
21
22
  const config = loadConfig();
22
23
  const kindFilter = input?.kind;
23
24
  const sources = [];
24
- // Stash entries local or remote sources
25
- for (const stash of config.stashes ?? []) {
26
- const isRemote = stash.url != null;
27
- const kind = isRemote ? "remote" : "local";
25
+ // Stash entries each entry exposes its provider type as kind (spec §2.1).
26
+ // Writable defaults: true for filesystem, false for git/npm/website (CLAUDE.md "Writes").
27
+ for (const stash of config.sources ?? config.stashes ?? []) {
28
+ const kind = stash.type ?? "filesystem";
28
29
  if (kindFilter && !kindFilter.includes(kind))
29
30
  continue;
31
+ const isFilesystem = kind === "filesystem";
32
+ const writableDefault = isFilesystem;
30
33
  const name = stash.name ?? stash.path ?? stash.url ?? "unknown";
31
34
  sources.push({
32
35
  name,
33
36
  kind,
34
37
  wiki: stash.wikiName,
35
38
  path: stash.path,
36
- provider: isRemote ? stash.type : undefined,
37
- updatable: false,
38
- writable: stash.writable === true,
39
+ provider: stash.url != null ? stash.type : undefined,
40
+ writable: stash.writable !== undefined ? stash.writable : writableDefault,
39
41
  status: { exists: stash.path ? directoryExists(stash.path) : true },
40
42
  });
41
43
  }
@@ -51,7 +53,6 @@ export async function akmListSources(input) {
51
53
  path: entry.stashRoot,
52
54
  ref: entry.ref,
53
55
  version: entry.resolvedVersion,
54
- updatable: true,
55
56
  writable: entry.writable === true,
56
57
  status: { exists: directoryExists(entry.stashRoot) },
57
58
  });
@@ -91,7 +92,7 @@ export async function akmRemove(input) {
91
92
  stashRoot: entry.stashRoot,
92
93
  },
93
94
  config: {
94
- stashCount: updatedConfig.stashes?.length ?? 0,
95
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
95
96
  installedKitCount: updatedConfig.installed?.length ?? 0,
96
97
  },
97
98
  index: {
@@ -105,7 +106,7 @@ export async function akmRemove(input) {
105
106
  // Fall through to stashes[] (local/remote sources)
106
107
  const stashResult = removeStash(target);
107
108
  if (!stashResult.removed || !stashResult.entry) {
108
- throw new NotFoundError(`No matching source for target: ${target}`);
109
+ throw new NotFoundError(`No matching source for target: ${target}`, "SOURCE_NOT_FOUND");
109
110
  }
110
111
  const removedEntry = stashResult.entry;
111
112
  const index = await akmIndex({ stashDir });
@@ -122,7 +123,7 @@ export async function akmRemove(input) {
122
123
  stashRoot: removedEntry.path ?? "",
123
124
  },
124
125
  config: {
125
- stashCount: updatedConfig.stashes?.length ?? 0,
126
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
126
127
  installedKitCount: updatedConfig.installed?.length ?? 0,
127
128
  },
128
129
  index: {
@@ -138,9 +139,51 @@ export async function akmUpdate(input) {
138
139
  const target = input?.target?.trim();
139
140
  const all = input?.all === true;
140
141
  const force = input?.force === true;
141
- const installedEntries = loadConfig().installed ?? [];
142
+ const config = loadConfig();
143
+ const installedEntries = config.installed ?? [];
144
+ // Check if the target refers to a website source — those are syncable via
145
+ // ensureWebsiteMirror and are stored in sources[] not installed[].
146
+ if (target && !all) {
147
+ const stashes = config.sources ?? config.stashes ?? [];
148
+ const isUrl = target.startsWith("http://") || target.startsWith("https://");
149
+ const resolvedPath = !isUrl ? path.resolve(target) : undefined;
150
+ const websiteMatch = stashes.find((s) => {
151
+ if (s.type !== "website")
152
+ return false;
153
+ if (isUrl && s.url === target)
154
+ return true;
155
+ if (s.name === target)
156
+ return true;
157
+ if (resolvedPath && s.path && path.resolve(s.path) === resolvedPath)
158
+ return true;
159
+ return false;
160
+ });
161
+ if (websiteMatch) {
162
+ // TODO: full incremental re-crawl with delta tracking (#19)
163
+ await ensureWebsiteMirror(websiteMatch, { requireStashDir: true, force: true });
164
+ const index = await akmIndex({ stashDir });
165
+ const updatedConfig = loadConfig();
166
+ return {
167
+ schemaVersion: 1,
168
+ stashDir,
169
+ target,
170
+ all,
171
+ processed: [],
172
+ config: {
173
+ sourceCount: (updatedConfig.sources ?? updatedConfig.stashes ?? []).length,
174
+ installedKitCount: updatedConfig.installed?.length ?? 0,
175
+ },
176
+ index: {
177
+ mode: index.mode,
178
+ totalEntries: index.totalEntries,
179
+ directoriesScanned: index.directoriesScanned,
180
+ directoriesSkipped: index.directoriesSkipped,
181
+ },
182
+ };
183
+ }
184
+ }
142
185
  const selectedEntries = selectTargets(installedEntries, target, all);
143
- const auditConfig = loadConfig();
186
+ const auditConfig = config;
144
187
  const processed = [];
145
188
  for (const entry of selectedEntries) {
146
189
  if (force && shouldCleanupCache(entry)) {
@@ -210,7 +253,7 @@ export async function akmUpdate(input) {
210
253
  });
211
254
  }
212
255
  const index = await akmIndex({ stashDir });
213
- const config = loadConfig();
256
+ const finalConfig = loadConfig();
214
257
  return {
215
258
  schemaVersion: 1,
216
259
  stashDir,
@@ -218,8 +261,8 @@ export async function akmUpdate(input) {
218
261
  all,
219
262
  processed,
220
263
  config: {
221
- stashCount: config.stashes?.length ?? 0,
222
- installedKitCount: config.installed?.length ?? 0,
264
+ sourceCount: (finalConfig.sources ?? finalConfig.stashes ?? []).length,
265
+ installedKitCount: finalConfig.installed?.length ?? 0,
223
266
  },
224
267
  index: {
225
268
  mode: index.mode,
@@ -231,19 +274,19 @@ export async function akmUpdate(input) {
231
274
  }
232
275
  function selectTargets(installed, target, all) {
233
276
  if (all && target) {
234
- throw new UsageError("Specify either <target> or --all, not both.");
277
+ throw new UsageError("Specify either <target> or --all, not both.", "MISSING_OR_AMBIGUOUS_TARGET");
235
278
  }
236
279
  if (all)
237
280
  return installed;
238
281
  if (!target) {
239
- throw new UsageError("Either <target> or --all is required.");
282
+ throw new UsageError("Either <target> or --all is required.", "MISSING_OR_AMBIGUOUS_TARGET");
240
283
  }
241
284
  const found = tryResolveInstalledTarget(installed, target);
242
285
  if (found)
243
286
  return [found];
244
287
  // Check if target matches a stash source and give a helpful message
245
288
  const config = loadConfig();
246
- const stashes = config.stashes ?? [];
289
+ const stashes = config.sources ?? config.stashes ?? [];
247
290
  const isUrl = target.startsWith("http://") || target.startsWith("https://");
248
291
  const resolvedPath = !isUrl ? path.resolve(target) : undefined;
249
292
  const stashMatch = stashes.find((s) => {
@@ -256,10 +299,13 @@ function selectTargets(installed, target, all) {
256
299
  return false;
257
300
  });
258
301
  if (stashMatch) {
259
- if (stashMatch.url) {
260
- throw new UsageError(`"${target}" is a remote provider it queries live data and has nothing to update.`);
302
+ if (stashMatch.type === "website") {
303
+ // Website sources should be handled before reaching selectTargets.
304
+ // This path should not be reached; surface a clear message if it is.
305
+ throw new UsageError(`"${target}" is a website source — website caching not yet implemented for --all. ` +
306
+ `Run \`akm update ${target}\` to re-mirror this source individually.`, "TARGET_NOT_UPDATABLE");
261
307
  }
262
- throw new UsageError(`"${target}" is a local directory — it reflects your files in place. To refresh the search index, run: akm index`);
308
+ throw new UsageError(`"${target}" is a local directory — it reflects your files in place. To refresh the search index, run: akm index`, "TARGET_NOT_UPDATABLE");
263
309
  }
264
310
  throw new NotFoundError(`No matching source for target: ${target}`);
265
311
  }
@@ -10,11 +10,11 @@ const MIGRATION_DOC_URL = "https://github.com/itlackey/akm/blob/main/docs/migrat
10
10
  * `files[]` array in `package.json`.
11
11
  */
12
12
  function releaseNotesDir() {
13
- return path.resolve(import.meta.dir, "../docs/migration/release-notes");
13
+ return path.resolve(import.meta.dir, "../../docs/migration/release-notes");
14
14
  }
15
15
  function loadChangelog() {
16
16
  try {
17
- const changelogPath = path.resolve(import.meta.dir, "../CHANGELOG.md");
17
+ const changelogPath = path.resolve(import.meta.dir, "../../CHANGELOG.md");
18
18
  if (fs.existsSync(changelogPath)) {
19
19
  return fs.readFileSync(changelogPath, "utf8");
20
20
  }
@@ -1,8 +1,8 @@
1
- import { toErrorMessage } from "./common";
2
- import { DEFAULT_CONFIG, loadConfig } from "./config";
3
- import { resolveProviderFactory } from "./registry-factory";
1
+ import { toErrorMessage } from "../core/common";
2
+ import { DEFAULT_CONFIG, loadConfig } from "../core/config";
3
+ import { resolveProviderFactory } from "../registry/factory";
4
4
  // ── Eagerly import providers to trigger self-registration ───────────────────
5
- import "./providers/index";
5
+ import "../registry/providers/index";
6
6
  // ── Public API ──────────────────────────────────────────────────────────────
7
7
  export async function searchRegistry(query, options) {
8
8
  const trimmed = query.trim();
@@ -24,7 +24,8 @@ export async function searchRegistry(query, options) {
24
24
  // Merge results grouped by provider
25
25
  const allHits = [];
26
26
  const allAssetHits = [];
27
- for (const result of results) {
27
+ for (let i = 0; i < results.length; i++) {
28
+ const result = results[i];
28
29
  if (result.status === "rejected") {
29
30
  warnings.push(toErrorMessage(result.reason));
30
31
  continue;
@@ -32,6 +33,7 @@ export async function searchRegistry(query, options) {
32
33
  const value = result.value;
33
34
  if (!value)
34
35
  continue;
36
+ const registryLabel = entries[i].name ? `"${entries[i].name}"` : entries[i].url;
35
37
  let dropped = 0;
36
38
  for (const hit of value.hits) {
37
39
  if (isCompleteHit(hit)) {
@@ -52,7 +54,7 @@ export async function searchRegistry(query, options) {
52
54
  }
53
55
  }
54
56
  if (dropped > 0) {
55
- warnings.push(`Registry returned ${dropped} incomplete hit(s); dropped from response.`);
57
+ warnings.push(`Registry ${registryLabel} returned ${dropped} incomplete hit(s); dropped from response.`);
56
58
  }
57
59
  if (value.warnings)
58
60
  warnings.push(...value.warnings);