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
@@ -7,17 +7,17 @@
7
7
  */
8
8
  import path from "node:path";
9
9
  import * as p from "@clack/prompts";
10
- import { isHttpUrl } from "./common";
11
- import { DEFAULT_CONFIG, getConfigPath, loadUserConfig, saveConfig } from "./config";
12
- import { closeDatabase, isVecAvailable, openDatabase } from "./db";
13
- import { detectAgentPlatforms, detectOllama, detectOpenViking } from "./detect";
14
- import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "./embedder";
15
- import { akmIndex } from "./indexer";
16
- import { akmInit } from "./init";
17
- import { probeLlmCapabilities } from "./llm";
18
- import { getDefaultStashDir } from "./paths";
19
- import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus } from "./semantic-status";
20
- import { createSetupContext, runSetupSteps } from "./setup-steps";
10
+ import { akmInit } from "../commands/init";
11
+ import { isHttpUrl } from "../core/common";
12
+ import { DEFAULT_CONFIG, getConfigPath, loadUserConfig, saveConfig } from "../core/config";
13
+ import { getDefaultStashDir } from "../core/paths";
14
+ import { closeDatabase, isVecAvailable, openDatabase } from "../indexer/db";
15
+ import { akmIndex } from "../indexer/indexer";
16
+ import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/semantic-status";
17
+ import { probeLlmCapabilities } from "../llm/client";
18
+ import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder";
19
+ import { detectAgentPlatforms, detectOllama } from "./detect";
20
+ import { createSetupContext, runSetupSteps } from "./steps";
21
21
  // ── Constants ───────────────────────────────────────────────────────────────
22
22
  /**
23
23
  * Recommended GitHub repositories shown during setup.
@@ -141,7 +141,7 @@ async function prepareSemanticSearchAssets(config) {
141
141
  const spin = p.spinner();
142
142
  spin.start("Installing @huggingface/transformers...");
143
143
  try {
144
- const pkgRoot = path.resolve(import.meta.dir, "..");
144
+ const pkgRoot = path.resolve(import.meta.dir, "../..");
145
145
  const proc = Bun.spawn(["bun", "add", "@huggingface/transformers"], {
146
146
  cwd: pkgRoot,
147
147
  stdout: "pipe",
@@ -500,8 +500,8 @@ async function stepRegistries(current) {
500
500
  /**
501
501
  * @internal Exported for testing only.
502
502
  */
503
- export async function stepStashSources(current) {
504
- const stashes = [...(current.stashes ?? [])];
503
+ export async function stepAddSources(current) {
504
+ const stashes = [...(current.sources ?? current.stashes ?? [])];
505
505
  if (stashes.length > 0) {
506
506
  p.log.info(`You have ${stashes.length} existing stash source(s).`);
507
507
  }
@@ -547,7 +547,6 @@ export async function stepStashSources(current) {
547
547
  const action = await prompt(() => p.select({
548
548
  message: "Add another stash source?",
549
549
  options: [
550
- { value: "openviking", label: "OpenViking server", hint: "remote stash" },
551
550
  { value: "github-repo", label: "GitHub repository", hint: "custom URL" },
552
551
  { value: "filesystem", label: "Filesystem path", hint: "local directory" },
553
552
  { value: "done", label: "Done — no more sources" },
@@ -557,45 +556,6 @@ export async function stepStashSources(current) {
557
556
  addMore = false;
558
557
  break;
559
558
  }
560
- if (action === "openviking") {
561
- const url = await promptOrBack(() => p.text({
562
- message: "Enter the OpenViking server URL:",
563
- placeholder: "https://your-openviking-server.example.com",
564
- validate: (v) => {
565
- if (!v?.trim())
566
- return "URL cannot be empty";
567
- if (!v.startsWith("http://") && !v.startsWith("https://"))
568
- return "URL must start with http:// or https://";
569
- },
570
- }));
571
- if (url === null)
572
- continue;
573
- const spin = p.spinner();
574
- spin.start("Checking OpenViking server...");
575
- const result = await detectOpenViking(url.trim());
576
- if (result.available) {
577
- spin.stop("Server is reachable");
578
- }
579
- else {
580
- spin.stop("Server not reachable — adding anyway (it may be temporarily down)");
581
- }
582
- const name = await promptOrBack(() => p.text({
583
- message: "Give this stash a name (optional):",
584
- placeholder: "my-openviking",
585
- }));
586
- if (name === null)
587
- continue;
588
- // Use the normalized URL from detection (trailing slashes stripped)
589
- const entry = { type: "openviking", url: result.url };
590
- if (name.trim())
591
- entry.name = name.trim();
592
- if (!stashes.some((s) => s.url === entry.url)) {
593
- stashes.push(entry);
594
- }
595
- else {
596
- p.log.warn("This URL is already configured.");
597
- }
598
- }
599
559
  if (action === "github-repo") {
600
560
  const url = await promptOrBack(() => p.text({
601
561
  message: "Enter the GitHub repository URL:",
@@ -761,14 +721,14 @@ export function buildSetupSteps(options) {
761
721
  id: "stash-sources",
762
722
  label: "Stash Sources",
763
723
  async run(ctx) {
764
- const stashes = await stepStashSources(ctx.config);
724
+ const stashes = await stepAddSources(ctx.config);
765
725
  const platforms = await stepAgentPlatforms(ctx.config);
766
726
  const merged = [...stashes];
767
727
  for (const ps of platforms) {
768
728
  if (!merged.some((s) => s.path === ps.path))
769
729
  merged.push(ps);
770
730
  }
771
- ctx.apply({ stashes: merged.length > 0 ? merged : undefined });
731
+ ctx.apply({ sources: merged.length > 0 ? merged : undefined });
772
732
  },
773
733
  },
774
734
  ];
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { isWithin } from "./common";
3
+ import { isWithin } from "../core/common";
4
4
  // ── Helpers ─────────────────────────────────────────────────────────────────
5
5
  /** Key to check in package.json for akm include configuration. */
6
6
  const INCLUDE_CONFIG_KEYS = ["akm"];
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Source provider factory map.
3
+ *
4
+ * Maps source kind identifiers (e.g. "filesystem", "git", "website", "npm")
5
+ * to factory functions that build {@link SourceProvider} instances from a
6
+ * {@link SourceConfigEntry}.
7
+ *
8
+ * Distinct from the registry-discovery factory (`registry/factory.ts`).
9
+ * Both share `create-provider-registry.ts` for the underlying string→factory
10
+ * map.
11
+ */
12
+ import { createProviderRegistry } from "../registry/create-provider-registry";
13
+ // ── Factory map ─────────────────────────────────────────────────────────────
14
+ const registry = createProviderRegistry();
15
+ export function registerSourceProvider(type, factory) {
16
+ registry.register(type, factory);
17
+ }
18
+ export function resolveSourceProviderFactory(type) {
19
+ return registry.resolve(type);
20
+ }
21
+ /**
22
+ * Build a {@link SourceProvider} for every enabled source in the config that
23
+ * has a registered factory.
24
+ */
25
+ export function resolveSourceProviders(config) {
26
+ const providers = [];
27
+ for (const entry of config.sources ?? config.stashes ?? []) {
28
+ if (entry.enabled === false)
29
+ continue;
30
+ const factory = registry.resolve(entry.type);
31
+ if (factory) {
32
+ providers.push(factory(entry));
33
+ }
34
+ }
35
+ return providers;
36
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * SourceProvider — minimal v1 interface (spec §2.1).
3
+ *
4
+ * A SourceProvider gets files into a directory. The indexer walks `path()`
5
+ * and reads files from disk. Search and show go through the indexer, not
6
+ * through provider methods.
7
+ *
8
+ * Three required members + one optional:
9
+ * - name configured source name
10
+ * - kind "filesystem" | "git" | "website" | "npm"
11
+ * - init(ctx) called once after construction
12
+ * - path() the directory the indexer walks (stable for instance lifetime)
13
+ * - sync?() refresh the directory from upstream (no-op for filesystem)
14
+ *
15
+ * All other writing/reading concerns live outside this interface:
16
+ * - Writes: src/core/write-source.ts (Phase 5)
17
+ * - Reads: src/indexer.ts (Phase 4)
18
+ * - Install: src/sources/providers/sync-from-ref.ts (install-time helpers,
19
+ * separate from configured-source plumbing)
20
+ */
21
+ export {};
@@ -0,0 +1,35 @@
1
+ import { resolveStashDir } from "../../core/common";
2
+ import { ConfigError } from "../../core/errors";
3
+ import { registerSourceProvider } from "../provider-factory";
4
+ /**
5
+ * Filesystem source — points at a directory the user already manages.
6
+ *
7
+ * Implements the v1 {@link SourceProvider} interface (spec §2.1, §2.4):
8
+ * just `{ name, kind, init, path }`. No `sync()` — content is the user's
9
+ * own directory, never refreshed by akm.
10
+ */
11
+ class FilesystemSourceProvider {
12
+ kind = "filesystem";
13
+ name;
14
+ #stashDir;
15
+ constructor(entry) {
16
+ if (entry.type !== "filesystem") {
17
+ throw new ConfigError(`FilesystemSourceProvider invoked with type="${entry.type}"`);
18
+ }
19
+ this.#stashDir = entry.path ?? resolveStashDir();
20
+ if (!this.#stashDir) {
21
+ throw new ConfigError("filesystem source requires a `path`");
22
+ }
23
+ this.name = entry.name ?? this.#stashDir;
24
+ }
25
+ async init(_ctx) {
26
+ // Filesystem sources resolve their path eagerly in the constructor;
27
+ // init has nothing to do beyond letting the registry know we're ready.
28
+ }
29
+ path() {
30
+ return this.#stashDir;
31
+ }
32
+ }
33
+ // ── Self-register ───────────────────────────────────────────────────────────
34
+ registerSourceProvider("filesystem", (config) => new FilesystemSourceProvider(config));
35
+ export { FilesystemSourceProvider };
@@ -2,12 +2,12 @@ import { spawnSync } from "node:child_process";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import { resolveStashDir } from "../common";
6
- import { loadConfig } from "../config";
7
- import { ConfigError, UsageError } from "../errors";
8
- import { getRegistryCacheDir, getRegistryIndexCacheDir } from "../paths";
9
- import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../registry-resolve";
10
- import { registerStashProvider } from "../stash-provider-factory";
5
+ import { resolveStashDir } from "../../core/common";
6
+ import { loadConfig } from "../../core/config";
7
+ import { ConfigError, UsageError } from "../../core/errors";
8
+ import { getRegistryCacheDir, getRegistryIndexCacheDir } from "../../core/paths";
9
+ import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../../registry/resolve";
10
+ import { registerSourceProvider } from "../provider-factory";
11
11
  import { applyAkmIncludeConfig, buildInstallCacheDir, copyDirectoryContents, detectStashRoot, isDirectory, isExpired, sanitizeString, } from "./provider-utils";
12
12
  /** Cache TTL before refreshing the mirrored repo (12 hours). */
13
13
  const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
@@ -15,74 +15,63 @@ const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
15
15
  const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
16
16
  const GIT_STASH_TYPES = new Set(["git"]);
17
17
  /**
18
- * Git stash provider. Implements {@link SyncableStashProvider} (which extends
19
- * the LiveStashProvider surface) the indexer walks the cloned repo, and
20
- * `sync()` clones/pulls into a local mirror.
18
+ * Git source provider clones (and re-pulls) a remote repo into a local
19
+ * cache directory. Implements the v1 {@link SourceProvider} interface (spec
20
+ * §2.1, §2.5): `{ name, kind, init, path, sync }`.
21
+ *
22
+ * Reading is the indexer's job — this class doesn't implement `search` or
23
+ * `show`. The install-time helpers `syncRegistryGitRef` / `syncMirroredRepo`
24
+ * live below as standalone functions used by `akm add` / `akm update`.
21
25
  */
22
- class GitStashProvider {
23
- type = "git";
24
- kind = "syncable";
26
+ class GitSourceProvider {
27
+ kind = "git";
25
28
  name;
26
- config;
29
+ #config;
30
+ #path = null;
27
31
  constructor(config) {
28
- this.config = config;
32
+ this.#config = config;
29
33
  this.name = config.name ?? "git";
30
34
  }
31
- /** Content is indexed through the standard FTS5 pipeline. */
32
- async search(_options) {
33
- return { hits: [] };
34
- }
35
- /** Content is local files, shown via showLocal. */
36
- async show(_ref, _view) {
37
- throw new Error("Git provider content is shown via local index");
38
- }
39
- /** Content is local; no remote show needed. */
40
- canShow(_ref) {
41
- return false;
35
+ async init(_ctx) {
36
+ // Resolve the on-disk content directory once. For configured git sources
37
+ // this is the cached working tree; for one-shot install refs it's the
38
+ // path the install pipeline materialised.
39
+ this.#path = resolveGitContentDir(this.#config);
40
+ }
41
+ path() {
42
+ if (this.#path == null) {
43
+ // Lazy resolution: providers are sometimes constructed without an
44
+ // explicit init() call (e.g. by legacy callers that just want the
45
+ // path). Resolve on demand and cache.
46
+ this.#path = resolveGitContentDir(this.#config);
47
+ }
48
+ return this.#path;
42
49
  }
43
- async sync(config, options) {
50
+ async sync() {
44
51
  // Two execution modes:
45
- // 1. Long-lived configured stash (config.url) — mirror into the
52
+ // 1. Long-lived configured source (config.url) — mirror into the
46
53
  // registry-index cache and serve as a read-only working tree.
47
- // 2. One-shot install ref (config.options.ref like "git:..." / "github:...") —
48
- // run the historical clone-into-cache flow used by `akm add`.
49
- if (typeof config.options?.ref === "string" && config.options.ref) {
50
- return syncRegistryGitRef(String(config.options.ref), options);
51
- }
52
- return syncMirroredRepo(config, options);
53
- }
54
- getContentDir(config) {
55
- if (config.path)
56
- return config.path;
57
- if (config.url) {
58
- const repo = parseGitRepoUrl(config.url);
59
- return getCachePaths(repo.canonicalUrl).repoDir;
60
- }
61
- throw new ConfigError("git stash entry must have either `path` or `url`");
62
- }
63
- async remove(config) {
64
- if (config.path && isDirectory(config.path)) {
65
- try {
66
- fs.rmSync(path.dirname(config.path), { recursive: true, force: true });
67
- }
68
- catch {
69
- /* best-effort */
70
- }
71
- }
72
- else if (config.url) {
73
- const repo = parseGitRepoUrl(config.url);
74
- const paths = getCachePaths(repo.canonicalUrl);
75
- try {
76
- fs.rmSync(paths.rootDir, { recursive: true, force: true });
77
- }
78
- catch {
79
- /* best-effort */
80
- }
54
+ // 2. One-shot install ref (options.ref like "git:..." / "github:...") —
55
+ // delegate to the install-time pipeline.
56
+ if (typeof this.#config.options?.ref === "string" && this.#config.options.ref) {
57
+ await syncRegistryGitRef(String(this.#config.options.ref));
58
+ return;
81
59
  }
60
+ await syncMirroredRepo(this.#config);
82
61
  }
83
62
  }
63
+ /** Resolve the on-disk content directory for a configured git source. */
64
+ function resolveGitContentDir(config) {
65
+ if (config.path)
66
+ return config.path;
67
+ if (config.url) {
68
+ const repo = parseGitRepoUrl(config.url);
69
+ return getCachePaths(repo.canonicalUrl).repoDir;
70
+ }
71
+ throw new ConfigError("git source entry must have either `path` or `url`");
72
+ }
84
73
  // ── Self-register ───────────────────────────────────────────────────────────
85
- registerStashProvider("git", (config) => new GitStashProvider(config));
74
+ registerSourceProvider("git", (config) => new GitSourceProvider(config));
86
75
  // ── Cache management ────────────────────────────────────────────────────────
87
76
  function getCachePaths(repoUrl) {
88
77
  const key = createHash("sha256").update(repoUrl).digest("hex").slice(0, 16);
@@ -414,7 +403,7 @@ export function saveGitStash(name, message, writableOverride) {
414
403
  let writable = false;
415
404
  if (name) {
416
405
  const config = loadConfig();
417
- const stash = config.stashes?.find((s) => s.name === name || s.url === name);
406
+ const stash = (config.sources ?? config.stashes ?? []).find((s) => s.name === name || s.url === name);
418
407
  if (!stash)
419
408
  throw new UsageError(`No git stash found with name "${name}"`);
420
409
  if (!GIT_STASH_TYPES.has(stash.type)) {
@@ -476,4 +465,4 @@ export function saveGitStash(name, message, writableOverride) {
476
465
  };
477
466
  }
478
467
  // ── Exports ─────────────────────────────────────────────────────────────────
479
- export { ensureGitMirror, GitStashProvider, getCachePaths, parseGitRepoUrl };
468
+ export { ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
@@ -1,12 +1,11 @@
1
1
  /**
2
- * Centralized stash provider registration.
2
+ * Centralized source provider registration.
3
3
  *
4
- * Import this module (side-effect import) to register all built-in stash
4
+ * Import this module (side-effect import) to register all built-in source
5
5
  * providers with the provider registry. This replaces the individual
6
- * side-effect imports that were duplicated in stash-search.ts and stash-show.ts.
6
+ * side-effect imports that were duplicated in source-search.ts and source-show.ts.
7
7
  */
8
8
  import "./filesystem";
9
9
  import "./git";
10
10
  import "./npm";
11
- import "./openviking";
12
11
  import "./website";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Install-time types used by `syncFromRef` and the legacy install pipeline.
3
+ *
4
+ * Distinct from the v1 {@link SourceProvider} interface (which only deals
5
+ * with "configured sources" — entries already resolved into a directory).
6
+ * These types describe the resolution+lockfile step that runs when
7
+ * `akm add <install-ref>` materialises an upstream artifact into a local
8
+ * cache directory.
9
+ *
10
+ * They live here, outside `provider.ts`, so the v1 SourceProvider
11
+ * interface stays minimal (`{ name, kind, init, path, sync? }`) per the
12
+ * architecture spec §2.1.
13
+ */
14
+ export {};
@@ -12,53 +12,46 @@
12
12
  */
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
15
- import { ConfigError, UsageError } from "../errors";
16
- import { getRegistryCacheDir } from "../paths";
17
- import { parseRegistryRef, resolveRegistryArtifact } from "../registry-resolve";
18
- import { registerStashProvider } from "../stash-provider-factory";
15
+ import { ConfigError, UsageError } from "../../core/errors";
16
+ import { getRegistryCacheDir } from "../../core/paths";
17
+ import { parseRegistryRef, resolveRegistryArtifact } from "../../registry/resolve";
18
+ import { registerSourceProvider } from "../provider-factory";
19
19
  import { applyAkmIncludeConfig, buildInstallCacheDir, computeFileHash, detectStashRoot, downloadArchive, isDirectory, } from "./provider-utils";
20
20
  import { extractTarGzSecure, verifyArchiveIntegrity } from "./tar-utils";
21
- class NpmStashProvider {
22
- type = "npm";
23
- kind = "syncable";
21
+ /**
22
+ * NPM source provider — fetches a tarball from the npm registry and extracts
23
+ * it into a local cache. Implements the v1 {@link SourceProvider} interface
24
+ * (spec §2.1): `{ name, kind, init, path, sync }`.
25
+ *
26
+ * The install-time pipeline (`syncNpmRef`) lives below as a standalone
27
+ * function used by `akm add` / `akm update` — that path produces a
28
+ * {@link SourceLockData} record for lockfile bookkeeping. The provider's own
29
+ * `sync()` is a void refresh (delegates to the install pipeline but discards
30
+ * the lock data, which is owned by `lockfile.ts`).
31
+ */
32
+ class NpmSourceProvider {
33
+ kind = "npm";
24
34
  name;
35
+ #config;
25
36
  constructor(config) {
37
+ this.#config = config;
26
38
  this.name = config.name ?? config.url ?? "npm";
27
39
  }
28
- /** Content is indexed through the standard FTS5 pipeline. */
29
- async search(_options) {
30
- return { hits: [] };
40
+ async init(_ctx) {
41
+ // Resolution happens lazily in path(): until `sync()` runs there's no
42
+ // reliable on-disk path. Init is the registration handshake.
31
43
  }
32
- /** Content is local files, shown via showLocal. */
33
- async show(_ref, _view) {
34
- throw new Error("NPM provider content is shown via local index");
44
+ path() {
45
+ if (this.#config.path)
46
+ return this.#config.path;
47
+ throw new ConfigError(`npm source "${this.name}" has no resolved content path — run \`akm update\` to sync it before indexing.`);
35
48
  }
36
- canShow(_ref) {
37
- return false;
38
- }
39
- async sync(config, options) {
40
- const ref = npmRefFromConfig(config);
41
- return syncNpmRef(ref, options);
42
- }
43
- getContentDir(config) {
44
- if (config.path)
45
- return config.path;
46
- throw new ConfigError("npm stash entry missing resolved content path");
47
- }
48
- async remove(config) {
49
- if (config.path && isDirectory(config.path)) {
50
- // Remove the whole versioned cache dir if we know the parent layout.
51
- const parent = path.dirname(config.path);
52
- try {
53
- fs.rmSync(parent, { recursive: true, force: true });
54
- }
55
- catch {
56
- /* best-effort */
57
- }
58
- }
49
+ async sync() {
50
+ const ref = npmRefFromConfig(this.#config);
51
+ await syncNpmRef(ref);
59
52
  }
60
53
  }
61
- registerStashProvider("npm", (config) => new NpmStashProvider(config));
54
+ registerSourceProvider("npm", (config) => new NpmSourceProvider(config));
62
55
  function npmRefFromConfig(config) {
63
56
  // Prefer an explicit ref-bearing field (set by akmAdd when persisting), else fall back
64
57
  // to options or url so the provider stays usable from a hand-rolled config.
@@ -69,7 +62,7 @@ function npmRefFromConfig(config) {
69
62
  return candidate.startsWith("npm:") ? candidate : `npm:${candidate}`;
70
63
  }
71
64
  /**
72
- * Fetch and extract an npm tarball, returning a populated `StashLockData`.
65
+ * Fetch and extract an npm tarball, returning a populated `SourceLockData`.
73
66
  *
74
67
  * Mirrors the historical `installRegistryRef()` path for npm sources:
75
68
  * - resolve artifact URL + integrity from the npm registry
@@ -127,9 +120,17 @@ async function doSyncNpm(parsed, options) {
127
120
  verifyArchiveIntegrity(archivePath, resolved.resolvedRevision, resolved.source);
128
121
  integrity = await computeFileHash(archivePath);
129
122
  extractTarGzSecure(archivePath, extractedDir);
130
- provisionalKitRoot = detectStashRoot(extractedDir);
123
+ const detectedProvisionalKitRoot = detectStashRoot(extractedDir);
124
+ if (!detectedProvisionalKitRoot) {
125
+ throw new UsageError(`Unable to detect a stash root in extracted npm package: ${resolved.ref}`);
126
+ }
127
+ provisionalKitRoot = detectedProvisionalKitRoot;
131
128
  installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
132
- stashRoot = detectStashRoot(installRoot);
129
+ const detectedStashRoot = detectStashRoot(installRoot);
130
+ if (!detectedStashRoot) {
131
+ throw new UsageError(`Unable to detect a stash root after applying .akm-include configuration for npm package: ${resolved.ref}`);
132
+ }
133
+ stashRoot = detectedStashRoot;
133
134
  }
134
135
  catch (err) {
135
136
  // Clean up so stale or partial extractions don't cause false cache hits.
@@ -156,4 +157,4 @@ async function doSyncNpm(parsed, options) {
156
157
  syncedAt,
157
158
  };
158
159
  }
159
- export { NpmStashProvider };
160
+ export { NpmSourceProvider };
@@ -1,9 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { TYPE_DIRS } from "../asset-spec";
5
- import { fetchWithRetry } from "../common";
6
- import { copyIncludedPaths, findNearestIncludeConfig } from "../stash-include";
4
+ import { TYPE_DIRS } from "../../core/asset-spec";
5
+ import { fetchWithRetry } from "../../core/common";
6
+ import { copyIncludedPaths, findNearestIncludeConfig } from "../include";
7
7
  const REGISTRY_STASH_DIR_NAMES = new Set(Object.values(TYPE_DIRS));
8
8
  /** Strip terminal control characters from untrusted strings. */
9
9
  export function sanitizeString(value, maxLength = 255) {
@@ -9,8 +9,8 @@
9
9
  * `akmUpdate`) decide whether to run `auditInstallCandidate` on the
10
10
  * synced `contentDir` because they own the `--trust` flag.
11
11
  */
12
- import { UsageError } from "../errors";
13
- import { parseRegistryRef } from "../registry-resolve";
12
+ import { UsageError } from "../../core/errors";
13
+ import { parseRegistryRef } from "../../registry/resolve";
14
14
  import { detectStashRoot } from "./provider-utils";
15
15
  export async function syncFromRef(ref, options) {
16
16
  const parsed = parseRegistryRef(ref);
@@ -6,15 +6,15 @@
6
6
  * and verify integrity hashes (SRI or hex shasum) before extraction.
7
7
  *
8
8
  * Extracted from `registry-install.ts` and shared by all syncable
9
- * providers that fetch tarballs (currently `NpmStashProvider` and the
9
+ * providers that fetch tarballs (currently `NpmSourceProvider` and the
10
10
  * registry index builder).
11
11
  */
12
12
  import { spawnSync } from "node:child_process";
13
13
  import { createHash } from "node:crypto";
14
14
  import fs from "node:fs";
15
15
  import path from "node:path";
16
- import { isWithin } from "../common";
17
- import { warn } from "../warn";
16
+ import { isWithin } from "../../core/common";
17
+ import { warn } from "../../core/warn";
18
18
  /**
19
19
  * Verify an archive's integrity against a known hash. Throws and removes
20
20
  * the archive when verification fails.
@@ -103,11 +103,14 @@ function scanExtractedFiles(dir, root) {
103
103
  throw new Error(`Post-extraction scan: symlink escapes destination directory: ${fullPath} -> ${target}`);
104
104
  }
105
105
  }
106
- // Belt-and-suspenders: any regular entry whose resolved path lands outside
107
- // the destination root is rejected, regardless of how its name looks. This
108
- // catches anything the tar pre-validation missed.
109
- if (!entry.isSymbolicLink() && !isWithin(fullPath, root)) {
110
- throw new Error(`Post-extraction scan: entry escapes destination directory: ${fullPath}`);
106
+ // Belt-and-suspenders: check that the resolved path of regular entries
107
+ // stays within the destination root. This catches path traversal attempts
108
+ // via symlink TOCTOU, directory renames, or any other anomalies.
109
+ if (!entry.isSymbolicLink()) {
110
+ const resolved = path.resolve(fullPath);
111
+ if (!isWithin(resolved, root)) {
112
+ throw new Error(`Post-extraction scan: entry escapes destination directory: ${fullPath}`);
113
+ }
111
114
  }
112
115
  if (entry.isDirectory()) {
113
116
  scanExtractedFiles(fullPath, root);