akm-cli 0.5.0 → 0.6.0-rc1

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 (74) hide show
  1. package/CHANGELOG.md +32 -5
  2. package/dist/asset-registry.js +29 -5
  3. package/dist/asset-spec.js +12 -5
  4. package/dist/cli-hints.js +300 -0
  5. package/dist/cli.js +218 -1357
  6. package/dist/common.js +147 -50
  7. package/dist/config.js +224 -13
  8. package/dist/create-provider-registry.js +1 -1
  9. package/dist/curate.js +258 -0
  10. package/dist/{local-search.js → db-search.js} +30 -19
  11. package/dist/db.js +168 -62
  12. package/dist/embedder.js +49 -273
  13. package/dist/embedders/cache.js +47 -0
  14. package/dist/embedders/local.js +152 -0
  15. package/dist/embedders/remote.js +121 -0
  16. package/dist/embedders/types.js +39 -0
  17. package/dist/errors.js +14 -3
  18. package/dist/frontmatter.js +61 -7
  19. package/dist/indexer.js +38 -7
  20. package/dist/info.js +2 -2
  21. package/dist/install-audit.js +16 -1
  22. package/dist/{installed-kits.js → installed-stashes.js} +48 -22
  23. package/dist/llm-client.js +92 -0
  24. package/dist/llm.js +14 -126
  25. package/dist/lockfile.js +28 -1
  26. package/dist/matchers.js +1 -1
  27. package/dist/metadata-enhance.js +53 -0
  28. package/dist/migration-help.js +75 -44
  29. package/dist/output-context.js +77 -0
  30. package/dist/output-shapes.js +198 -0
  31. package/dist/output-text.js +520 -0
  32. package/dist/paths.js +4 -4
  33. package/dist/providers/index.js +11 -0
  34. package/dist/providers/skills-sh.js +1 -1
  35. package/dist/providers/static-index.js +47 -45
  36. package/dist/registry-build-index.js +36 -29
  37. package/dist/registry-factory.js +2 -2
  38. package/dist/registry-resolve.js +8 -4
  39. package/dist/registry-search.js +62 -5
  40. package/dist/remember.js +172 -0
  41. package/dist/renderers.js +52 -0
  42. package/dist/search-source.js +73 -42
  43. package/dist/setup-steps.js +45 -0
  44. package/dist/setup.js +149 -76
  45. package/dist/stash-add.js +94 -38
  46. package/dist/stash-clone.js +4 -4
  47. package/dist/stash-provider-factory.js +2 -2
  48. package/dist/stash-provider.js +3 -1
  49. package/dist/stash-providers/filesystem.js +31 -1
  50. package/dist/stash-providers/git.js +209 -8
  51. package/dist/stash-providers/index.js +1 -0
  52. package/dist/stash-providers/npm.js +159 -0
  53. package/dist/stash-providers/provider-utils.js +162 -0
  54. package/dist/stash-providers/sync-from-ref.js +45 -0
  55. package/dist/stash-providers/tar-utils.js +151 -0
  56. package/dist/stash-providers/website.js +80 -4
  57. package/dist/stash-resolve.js +5 -5
  58. package/dist/stash-search.js +4 -4
  59. package/dist/stash-show.js +3 -3
  60. package/dist/wiki.js +6 -6
  61. package/dist/workflow-authoring.js +12 -4
  62. package/dist/workflow-markdown.js +9 -0
  63. package/dist/workflow-runs.js +12 -2
  64. package/docs/README.md +30 -0
  65. package/docs/migration/release-notes/0.0.13.md +4 -0
  66. package/docs/migration/release-notes/0.1.0.md +6 -0
  67. package/docs/migration/release-notes/0.2.0.md +6 -0
  68. package/docs/migration/release-notes/0.3.0.md +5 -0
  69. package/docs/migration/release-notes/0.5.0.md +6 -0
  70. package/docs/migration/release-notes/0.6.0.md +29 -0
  71. package/docs/migration/release-notes/README.md +21 -0
  72. package/package.json +3 -2
  73. package/dist/registry-install.js +0 -532
  74. /package/dist/{kit-include.js → stash-include.js} +0 -0
package/dist/renderers.js CHANGED
@@ -411,6 +411,58 @@ const memoryMdRenderer = {
411
411
  content: ctx.content(),
412
412
  };
413
413
  },
414
+ extractMetadata(entry, ctx) {
415
+ try {
416
+ const parsed = parseFrontmatter(ctx.content());
417
+ const fm = parsed.data;
418
+ // Description from frontmatter
419
+ const desc = toStringOrUndefined(fm.description);
420
+ if (desc && !entry.description) {
421
+ entry.description = desc;
422
+ entry.source = "frontmatter";
423
+ entry.confidence = 0.9;
424
+ }
425
+ // Tags from frontmatter
426
+ if (Array.isArray(fm.tags) && fm.tags.length > 0) {
427
+ const fmTags = fm.tags.filter((t) => typeof t === "string" && t.trim().length > 0);
428
+ if (fmTags.length > 0) {
429
+ entry.tags = Array.from(new Set([...(entry.tags ?? []), ...fmTags]));
430
+ }
431
+ }
432
+ // Build searchHints from structured memory metadata fields
433
+ const hints = new Set(entry.searchHints ?? []);
434
+ const source = toStringOrUndefined(fm.source);
435
+ if (source)
436
+ hints.add(source);
437
+ // observed_at: prefer frontmatter value, fall back to file mtime
438
+ const fmObservedAt = toStringOrUndefined(fm.observed_at);
439
+ if (fmObservedAt) {
440
+ hints.add(`observed_at:${fmObservedAt}`);
441
+ }
442
+ else {
443
+ // mtime fallback: format as ISO date (YYYY-MM-DD)
444
+ try {
445
+ const mtime = ctx.stat().mtime;
446
+ const isoDate = mtime.toISOString().slice(0, 10);
447
+ hints.add(`observed_at:${isoDate}`);
448
+ }
449
+ catch {
450
+ // Non-fatal: skip mtime fallback on stat error
451
+ }
452
+ }
453
+ const expires = toStringOrUndefined(fm.expires);
454
+ if (expires)
455
+ hints.add(`expires:${expires}`);
456
+ if (fm.subjective === true)
457
+ hints.add("subjective");
458
+ if (hints.size > 0) {
459
+ entry.searchHints = Array.from(hints).filter(Boolean);
460
+ }
461
+ }
462
+ catch {
463
+ // Non-fatal: skip metadata extraction on error
464
+ }
465
+ },
414
466
  };
415
467
  // ── 6. workflow-md ───────────────────────────────────────────────────────────
416
468
  const workflowMdRenderer = {
@@ -5,15 +5,25 @@ import { loadConfig } from "./config";
5
5
  import { ensureGitMirror, getCachePaths, parseGitRepoUrl } from "./stash-providers/git";
6
6
  import { ensureWebsiteMirror, getCachePaths as getWebsiteCachePaths } from "./stash-providers/website";
7
7
  import { warn } from "./warn";
8
+ // Legacy "context-hub" / "github" type aliases are normalized to "git" at
9
+ // config-load time (see src/config.ts), so this set only contains the canonical
10
+ // type.
11
+ const GIT_STASH_TYPES = new Set(["git"]);
8
12
  // ── Resolution ──────────────────────────────────────────────────────────────
9
13
  /**
10
- * Build the ordered list of stash sources:
11
- * 1. Primary stash dir (user's own, destination for clone)
12
- * 2. Additional stashes (filesystem and remote providers)
13
- * 3. Installed kit paths (cache-managed, from registry)
14
+ * Build the ordered list of stash sources, walking every configured stash
15
+ * once. Iteration order:
14
16
  *
15
- * The first entry is always the primary stash. Additional entries come
16
- * from `stashes` config and `installed` kit entries.
17
+ * 1. The primary stash directory (the entry marked `primary: true`, or the
18
+ * legacy top-level `stashDir`). Always emitted, even when the directory
19
+ * does not yet exist on disk, so callers can use it as the clone target.
20
+ * 2. Each entry in `config.stashes[]` (in declared order), excluding the
21
+ * one already emitted as the primary.
22
+ * 3. Each entry in `config.installed[]` (registry-managed stashes).
23
+ *
24
+ * Replaces the previous four-pass loop that walked `stashes[]` separately
25
+ * for each provider kind. Disabled entries (`enabled: false`) and entries
26
+ * whose disk path doesn't exist are filtered after deduplication.
17
27
  */
18
28
  export function resolveStashSources(overrideStashDir, existingConfig) {
19
29
  const stashDir = overrideStashDir ?? resolveStashDir();
@@ -36,48 +46,70 @@ export function resolveStashSources(overrideStashDir, existingConfig) {
36
46
  });
37
47
  }
38
48
  };
39
- // Filesystem entries from stashes[]
40
- for (const entry of config.stashes ?? []) {
41
- if (entry.type === "filesystem" && entry.path && entry.enabled !== false) {
42
- addSource(entry.path, entry.name, entry.wikiName);
43
- }
49
+ // (1) + (2) Single pass over declared stashes — primary first if present,
50
+ // then the rest in declared order. The primary's directory is already
51
+ // injected as `sources[0]` above, so we only need to dedupe the source set.
52
+ const stashes = config.stashes ?? [];
53
+ const primaryIdx = stashes.findIndex((entry) => entry.primary === true);
54
+ const ordered = [];
55
+ if (primaryIdx >= 0) {
56
+ ordered.push(stashes[primaryIdx]);
57
+ stashes.forEach((entry, i) => {
58
+ if (i !== primaryIdx)
59
+ ordered.push(entry);
60
+ });
44
61
  }
45
- // Git stash entries: resolve cache directory so the indexer can walk it.
46
- // "git" provider type (and its legacy aliases "context-hub", "github") are handled.
47
- for (const entry of config.stashes ?? []) {
48
- if (GIT_STASH_TYPES.has(entry.type) && entry.url && entry.enabled !== false) {
49
- try {
50
- const repo = parseGitRepoUrl(entry.url);
51
- const cachePaths = getCachePaths(repo.canonicalUrl);
52
- // The content/ subdirectory inside the extracted repo is the actual
53
- // stash root containing DOC.md / SKILL.md files that the walker indexes.
54
- const contentDir = path.join(cachePaths.repoDir, "content");
55
- addSource(contentDir, entry.name, entry.wikiName);
56
- }
57
- catch (err) {
58
- warn(`Warning: failed to resolve git stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
59
- }
60
- }
62
+ else {
63
+ ordered.push(...stashes);
61
64
  }
62
- // Website stash entries: resolve cache directory so the indexer can walk
63
- // the scraped markdown snapshots.
64
- for (const entry of config.stashes ?? []) {
65
- if (entry.type === "website" && entry.url && entry.enabled !== false) {
66
- try {
67
- const cachePaths = getWebsiteCachePaths(entry.url);
68
- addSource(cachePaths.stashDir, entry.name ?? entry.url, entry.wikiName);
69
- }
70
- catch (err) {
71
- warn(`Warning: failed to resolve website stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
72
- }
73
- }
65
+ for (const entry of ordered) {
66
+ if (entry.enabled === false)
67
+ continue;
68
+ const dir = resolveEntryContentDir(entry);
69
+ if (dir == null)
70
+ continue;
71
+ addSource(dir, entry.name, entry.wikiName);
74
72
  }
75
- // Installed kits (registry and local)
73
+ // (3) Installed stashes (registry-managed). Always last.
76
74
  for (const entry of config.installed ?? []) {
77
75
  addSource(entry.stashRoot, entry.id, entry.wikiName);
78
76
  }
79
77
  return sources;
80
78
  }
79
+ /**
80
+ * Resolve the content directory the indexer should walk for a given config
81
+ * entry. Returns `undefined` if the entry has no walkable content (e.g. an
82
+ * `openviking` remote stash) so the caller can skip it.
83
+ */
84
+ function resolveEntryContentDir(entry) {
85
+ if (entry.type === "filesystem" && entry.path) {
86
+ return entry.path;
87
+ }
88
+ if (GIT_STASH_TYPES.has(entry.type) && entry.url) {
89
+ try {
90
+ const repo = parseGitRepoUrl(entry.url);
91
+ const cachePaths = getCachePaths(repo.canonicalUrl);
92
+ // The content/ subdirectory inside the extracted repo is the actual
93
+ // stash root containing DOC.md / SKILL.md files that the walker indexes.
94
+ return path.join(cachePaths.repoDir, "content");
95
+ }
96
+ catch (err) {
97
+ warn(`Warning: failed to resolve git stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
98
+ return undefined;
99
+ }
100
+ }
101
+ if (entry.type === "website" && entry.url) {
102
+ try {
103
+ return getWebsiteCachePaths(entry.url).stashDir;
104
+ }
105
+ catch (err) {
106
+ warn(`Warning: failed to resolve website stash cache for "${entry.url}": ${err instanceof Error ? err.message : String(err)}`);
107
+ return undefined;
108
+ }
109
+ }
110
+ // Remote-only providers (openviking) have no walkable directory.
111
+ return undefined;
112
+ }
81
113
  /**
82
114
  * Convenience: returns just the directory paths, preserving priority order.
83
115
  */
@@ -168,8 +200,7 @@ function isValidDirectory(dir) {
168
200
  return false;
169
201
  }
170
202
  }
171
- // ── Git stash cache integration ──────────────────────────────────────────────
172
- const GIT_STASH_TYPES = new Set(["context-hub", "github", "git"]);
203
+ // ── Stash cache integration ─────────────────────────────────────────────────
173
204
  /**
174
205
  * Ensure all cache-backed stash providers are refreshed so their cache
175
206
  * directories exist on disk. Must be called (async) before
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Composable runner abstraction for `akm setup`.
3
+ *
4
+ * The interactive wizard in `setup.ts` historically ran a fixed series of
5
+ * step functions (`stepStashDir`, `stepOllama`, `stepLlm`, ...) inline.
6
+ * This module formalizes that pattern so steps can be:
7
+ * - reused by `akm init` (non-interactive preset, see Finding 31),
8
+ * - tested in isolation by passing a stub `SetupContext`, and
9
+ * - extended by plugins without touching the wizard call site.
10
+ *
11
+ * Steps mutate state through `SetupContext.apply()`, which accumulates a
12
+ * delta on top of the original config. `stepLlm` reading the embedding
13
+ * endpoint that `stepSemanticSearch` produced is the canonical example of
14
+ * why mutable accumulation is preferred over immutable returns.
15
+ */
16
+ /**
17
+ * Build a fresh `SetupContext` over a starting config. The returned context
18
+ * applies deltas in-place onto an internal accumulator and exposes the
19
+ * latest snapshot via `ctx.config`.
20
+ */
21
+ export function createSetupContext(initial, options) {
22
+ let acc = { ...initial };
23
+ return {
24
+ get config() {
25
+ return acc;
26
+ },
27
+ nonInteractive: options.nonInteractive,
28
+ apply(delta) {
29
+ acc = { ...acc, ...delta };
30
+ },
31
+ };
32
+ }
33
+ /**
34
+ * Run a list of steps against a context. Steps marked interactive-only are
35
+ * skipped when `ctx.nonInteractive` is true. Returns the final accumulated
36
+ * config so callers can persist it without re-reading the context.
37
+ */
38
+ export async function runSetupSteps(steps, ctx) {
39
+ for (const step of steps) {
40
+ if (ctx.nonInteractive && !step.nonInteractive)
41
+ continue;
42
+ await step.run(ctx);
43
+ }
44
+ return ctx.config;
45
+ }
package/dist/setup.js CHANGED
@@ -17,15 +17,16 @@ import { akmInit } from "./init";
17
17
  import { probeLlmCapabilities } from "./llm";
18
18
  import { getDefaultStashDir } from "./paths";
19
19
  import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus } from "./semantic-status";
20
+ import { createSetupContext, runSetupSteps } from "./setup-steps";
20
21
  // ── Constants ───────────────────────────────────────────────────────────────
21
- /** Recommended GitHub repositories shown during setup. */
22
- const RECOMMENDED_GITHUB_REPOS = [
23
- {
24
- url: "https://github.com/andrewyng/context-hub",
25
- name: "context-hub",
26
- hint: "community knowledge",
27
- },
28
- ];
22
+ /**
23
+ * Recommended GitHub repositories shown during setup.
24
+ *
25
+ * Currently empty — populating from the akm-registry at runtime is a
26
+ * separate feature. The wizard prompt infrastructure is retained for that
27
+ * future use.
28
+ */
29
+ const RECOMMENDED_GITHUB_REPOS = [];
29
30
  // Approximate first-download sizes used in the setup note.
30
31
  // LOCAL_MODEL_APPROX_SIZE_MB tracks the default local model (DEFAULT_LOCAL_MODEL).
31
32
  const LOCAL_MODEL_APPROX_SIZE_MB = 130;
@@ -136,7 +137,7 @@ async function prepareSemanticSearchAssets(config) {
136
137
  const remote = isRemoteEmbeddingConfig(config.embedding);
137
138
  // For local embeddings, ensure the required package is installed first.
138
139
  if (!remote) {
139
- if (!(await isTransformersAvailable())) {
140
+ if (!isTransformersAvailable()) {
140
141
  const spin = p.spinner();
141
142
  spin.start("Installing @huggingface/transformers...");
142
143
  try {
@@ -505,34 +506,38 @@ export async function stepStashSources(current) {
505
506
  p.log.info(`You have ${stashes.length} existing stash source(s).`);
506
507
  }
507
508
  // ── Recommended GitHub repos ───────────────────────────────────────────
508
- const existingUrls = new Set(stashes.map((s) => s.url));
509
- const repoOptions = RECOMMENDED_GITHUB_REPOS.map((r) => ({
510
- value: r.url,
511
- label: r.name,
512
- hint: existingUrls.has(r.url) ? `${r.hint} (already added)` : r.hint,
513
- }));
514
- const selectedRepos = await prompt(() => p.multiselect({
515
- message: "Recommended GitHub repositories toggle to add or remove:",
516
- options: repoOptions,
517
- initialValues: repoOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value),
518
- required: false,
519
- }));
520
- // Add newly selected repos
521
- for (const url of selectedRepos) {
522
- if (!existingUrls.has(url)) {
523
- const rec = RECOMMENDED_GITHUB_REPOS.find((r) => r.url === url);
524
- stashes.push({ type: "git", url, name: rec?.name });
525
- existingUrls.add(url);
509
+ // Skip the prompt entirely when there are no recommendations to show.
510
+ // The infrastructure is retained for a future registry-driven version.
511
+ if (RECOMMENDED_GITHUB_REPOS.length > 0) {
512
+ const existingUrls = new Set(stashes.map((s) => s.url));
513
+ const repoOptions = RECOMMENDED_GITHUB_REPOS.map((r) => ({
514
+ value: r.url,
515
+ label: r.name,
516
+ hint: existingUrls.has(r.url) ? `${r.hint} (already added)` : r.hint,
517
+ }));
518
+ const selectedRepos = await prompt(() => p.multiselect({
519
+ message: "Recommended GitHub repositories — toggle to add or remove:",
520
+ options: repoOptions,
521
+ initialValues: repoOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value),
522
+ required: false,
523
+ }));
524
+ // Add newly selected repos
525
+ for (const url of selectedRepos) {
526
+ if (!existingUrls.has(url)) {
527
+ const rec = RECOMMENDED_GITHUB_REPOS.find((r) => r.url === url);
528
+ stashes.push({ type: "git", url, name: rec?.name });
529
+ existingUrls.add(url);
530
+ }
526
531
  }
527
- }
528
- // Remove deselected repos that were previously configured
529
- for (const rec of RECOMMENDED_GITHUB_REPOS) {
530
- if (existingUrls.has(rec.url) && !selectedRepos.includes(rec.url)) {
531
- const idx = stashes.findIndex((s) => s.url === rec.url);
532
- if (idx !== -1) {
533
- stashes.splice(idx, 1);
534
- existingUrls.delete(rec.url);
535
- p.log.info(`Removed ${rec.name}.`);
532
+ // Remove deselected repos that were previously configured
533
+ for (const rec of RECOMMENDED_GITHUB_REPOS) {
534
+ if (existingUrls.has(rec.url) && !selectedRepos.includes(rec.url)) {
535
+ const idx = stashes.findIndex((s) => s.url === rec.url);
536
+ if (idx !== -1) {
537
+ stashes.splice(idx, 1);
538
+ existingUrls.delete(rec.url);
539
+ p.log.info(`Removed ${rec.name}.`);
540
+ }
536
541
  }
537
542
  }
538
543
  }
@@ -685,60 +690,128 @@ async function stepAgentPlatforms(current) {
685
690
  return entries;
686
691
  }
687
692
  // ── Main Wizard ─────────────────────────────────────────────────────────────
693
+ /**
694
+ * Build the canonical list of `SetupStep`s for the interactive wizard.
695
+ * Exposed (and exported) so tests and `akm init` can compose subsets.
696
+ *
697
+ * Each step wraps the existing `step*` functions, accumulating its result
698
+ * into the shared `SetupContext`. The `nonInteractive` flag controls
699
+ * inclusion in `akm init` (a non-interactive preset of `akm setup`).
700
+ */
701
+ export function buildSetupSteps(options) {
702
+ const outcome = { semantic: options.semanticSearchOutcome };
703
+ // Local cache of Ollama-detected fields surfaced from the embedding step
704
+ // to the LLM step. Mutable by design — `stepLlm` needs them.
705
+ let ollamaEndpoint;
706
+ let ollamaChatModels;
707
+ const steps = [
708
+ {
709
+ id: "stash-dir",
710
+ label: "Stash Directory",
711
+ nonInteractive: true,
712
+ async run(ctx) {
713
+ const stashDir = await stepStashDir(ctx.config);
714
+ ctx.apply({ stashDir });
715
+ },
716
+ },
717
+ {
718
+ id: "embedding",
719
+ label: "Embedding",
720
+ async run(ctx) {
721
+ if (!options.online) {
722
+ ctx.apply({ embedding: ctx.config.embedding });
723
+ return;
724
+ }
725
+ const result = await stepOllama(ctx.config);
726
+ ollamaEndpoint = result.ollamaEndpoint;
727
+ ollamaChatModels = result.ollamaChatModels;
728
+ ctx.apply({ embedding: result.embedding });
729
+ },
730
+ },
731
+ {
732
+ id: "llm",
733
+ label: "LLM Provider",
734
+ async run(ctx) {
735
+ if (!options.online) {
736
+ ctx.apply({ llm: ctx.config.llm });
737
+ return;
738
+ }
739
+ const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels);
740
+ ctx.apply({ llm });
741
+ },
742
+ },
743
+ {
744
+ id: "semantic-search",
745
+ label: "Semantic Search",
746
+ async run(ctx) {
747
+ const semantic = await stepSemanticSearch(ctx.config, ctx.config.embedding);
748
+ outcome.semantic = semantic;
749
+ ctx.apply({ semanticSearchMode: semantic.mode });
750
+ },
751
+ },
752
+ {
753
+ id: "registries",
754
+ label: "Registries",
755
+ async run(ctx) {
756
+ const registries = await stepRegistries(ctx.config);
757
+ ctx.apply({ registries });
758
+ },
759
+ },
760
+ {
761
+ id: "stash-sources",
762
+ label: "Stash Sources",
763
+ async run(ctx) {
764
+ const stashes = await stepStashSources(ctx.config);
765
+ const platforms = await stepAgentPlatforms(ctx.config);
766
+ const merged = [...stashes];
767
+ for (const ps of platforms) {
768
+ if (!merged.some((s) => s.path === ps.path))
769
+ merged.push(ps);
770
+ }
771
+ ctx.apply({ stashes: merged.length > 0 ? merged : undefined });
772
+ },
773
+ },
774
+ ];
775
+ return { steps, outcome };
776
+ }
688
777
  export async function runSetupWizard() {
689
778
  p.intro("akm setup");
690
779
  const current = loadUserConfig();
691
780
  const configPath = getConfigPath();
692
- // Step 1: Stash directory
693
- p.log.step("Step 1: Stash Directory");
694
- const stashDir = await stepStashDir(current);
695
781
  // Quick connectivity check — skip network-dependent steps when offline
696
782
  const online = await isOnline();
697
783
  if (!online) {
698
784
  p.log.warn("No network connectivity detected. Skipping Ollama detection and remote embedding checks.\n" +
699
785
  "Local-only setup will continue. Re-run `akm setup` when online for full configuration.");
700
786
  }
701
- // Step 2: Embedding (Ollama detection drives the embedding choice + surfaces
702
- // the Ollama endpoint to the LLM step that follows).
703
- p.log.step("Step 2: Embedding");
704
- const { embedding, ollamaEndpoint, ollamaChatModels } = online
705
- ? await stepOllama(current)
706
- : { embedding: current.embedding };
707
- // Step 2b: LLM provider Anthropic / OpenAI / Gemini / Ollama / custom.
708
- p.log.step("Step 2b: LLM Provider");
709
- const llm = online ? await stepLlm(current, ollamaEndpoint, ollamaChatModels) : current.llm;
710
- // Step 3: Semantic search assets
711
- p.log.step("Step 3: Semantic Search");
712
- const semanticSearchMode = await stepSemanticSearch(current, embedding);
713
- // Step 4: Registries
714
- p.log.step("Step 4: Registries");
715
- const registries = await stepRegistries(current);
716
- // Step 5: Stash sources
717
- p.log.step("Step 5: Stash Sources");
718
- const stashes = await stepStashSources(current);
719
- // Step 6: Agent platform detection
720
- p.log.step("Step 6: Agent Platform Detection");
721
- const platformStashes = await stepAgentPlatforms(current);
722
- // Merge platform stashes into main stashes list
723
- const allStashes = [...stashes];
724
- for (const ps of platformStashes) {
725
- if (!allStashes.some((s) => s.path === ps.path)) {
726
- allStashes.push(ps);
727
- }
728
- }
729
- // Build final config
787
+ const ctx = createSetupContext(current, { nonInteractive: false });
788
+ const { steps, outcome } = buildSetupSteps({
789
+ online,
790
+ semanticSearchOutcome: { mode: current.semanticSearchMode, prepareAssets: false },
791
+ });
792
+ // Wrap each step with a `p.log.step()` header so the wizard UI is
793
+ // unchanged. The canonical `runSetupSteps()` runner is used directly by
794
+ // `akm init` (non-interactive) and by tests.
795
+ const labeledSteps = steps.map((step) => ({
796
+ ...step,
797
+ async run(stepCtx) {
798
+ p.log.step(step.label);
799
+ await step.run(stepCtx);
800
+ },
801
+ }));
802
+ await runSetupSteps(labeledSteps, ctx);
730
803
  const newConfig = {
731
- ...current,
732
- stashDir,
733
- embedding,
734
- llm,
735
- registries,
736
- stashes: allStashes.length > 0 ? allStashes : undefined,
737
- // Preserve existing fields
738
- semanticSearchMode: semanticSearchMode.mode,
804
+ ...ctx.config,
805
+ // Preserve fields the steps don't manage explicitly.
739
806
  installed: current.installed,
740
807
  output: current.output,
741
808
  };
809
+ const semanticSearchMode = outcome.semantic;
810
+ const stashDir = newConfig.stashDir ?? current.stashDir ?? getDefaultStashDir();
811
+ const embedding = newConfig.embedding;
812
+ const llm = newConfig.llm;
813
+ const registries = newConfig.registries;
814
+ const allStashes = newConfig.stashes ?? [];
742
815
  // Confirm before saving
743
816
  const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
744
817
  p.note([