akm-cli 0.5.0 → 0.6.0-rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -5
- package/README.md +9 -9
- package/dist/cli.js +379 -1448
- package/dist/{completions.js → commands/completions.js} +1 -1
- package/dist/{config-cli.js → commands/config-cli.js} +109 -11
- package/dist/commands/curate.js +263 -0
- package/dist/{info.js → commands/info.js} +17 -11
- package/dist/{init.js → commands/init.js} +4 -4
- package/dist/{install-audit.js → commands/install-audit.js} +14 -2
- package/dist/{installed-kits.js → commands/installed-stashes.js} +122 -50
- package/dist/commands/migration-help.js +141 -0
- package/dist/{registry-search.js → commands/registry-search.js} +68 -9
- package/dist/commands/remember.js +178 -0
- package/dist/{stash-search.js → commands/search.js} +28 -69
- package/dist/{self-update.js → commands/self-update.js} +3 -3
- package/dist/{stash-show.js → commands/show.js} +106 -81
- package/dist/{stash-add.js → commands/source-add.js} +133 -67
- package/dist/{stash-clone.js → commands/source-clone.js} +15 -13
- package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
- package/dist/{vault.js → commands/vault.js} +43 -0
- package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
- package/dist/{asset-registry.js → core/asset-registry.js} +30 -6
- package/dist/{asset-spec.js → core/asset-spec.js} +13 -6
- package/dist/{common.js → core/common.js} +147 -50
- package/dist/{config.js → core/config.js} +288 -29
- package/dist/core/errors.js +90 -0
- package/dist/{frontmatter.js → core/frontmatter.js} +64 -8
- package/dist/{paths.js → core/paths.js} +4 -4
- package/dist/core/write-source.js +280 -0
- package/dist/{local-search.js → indexer/db-search.js} +49 -32
- package/dist/{db.js → indexer/db.js} +210 -81
- package/dist/{file-context.js → indexer/file-context.js} +3 -3
- package/dist/{indexer.js → indexer/indexer.js} +153 -30
- package/dist/{manifest.js → indexer/manifest.js} +10 -10
- package/dist/{matchers.js → indexer/matchers.js} +4 -7
- package/dist/{metadata.js → indexer/metadata.js} +9 -5
- package/dist/{search-source.js → indexer/search-source.js} +97 -55
- package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
- package/dist/{walker.js → indexer/walker.js} +1 -1
- package/dist/{lockfile.js → integrations/lockfile.js} +29 -2
- package/dist/{llm.js → llm/client.js} +12 -48
- package/dist/llm/embedder.js +127 -0
- package/dist/llm/embedders/cache.js +47 -0
- package/dist/llm/embedders/local.js +152 -0
- package/dist/llm/embedders/remote.js +121 -0
- package/dist/llm/embedders/types.js +39 -0
- package/dist/llm/metadata-enhance.js +53 -0
- package/dist/output/cli-hints.js +301 -0
- package/dist/output/context.js +95 -0
- package/dist/{renderers.js → output/renderers.js} +57 -61
- package/dist/output/shapes.js +212 -0
- package/dist/output/text.js +520 -0
- package/dist/{registry-build-index.js → registry/build-index.js} +48 -32
- package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
- package/dist/registry/factory.js +33 -0
- package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
- package/dist/registry/providers/index.js +11 -0
- package/dist/{providers → registry/providers}/skills-sh.js +60 -4
- package/dist/{providers → registry/providers}/static-index.js +126 -56
- package/dist/registry/providers/types.js +25 -0
- package/dist/{registry-resolve.js → registry/resolve.js} +10 -6
- package/dist/{detect.js → setup/detect.js} +0 -27
- package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
- package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
- package/dist/{setup.js → setup/setup.js} +162 -129
- package/dist/setup/steps.js +45 -0
- package/dist/{kit-include.js → sources/include.js} +1 -1
- package/dist/sources/provider-factory.js +36 -0
- package/dist/sources/provider.js +21 -0
- package/dist/sources/providers/filesystem.js +35 -0
- package/dist/{stash-providers → sources/providers}/git.js +218 -28
- package/dist/{stash-providers → sources/providers}/index.js +4 -4
- package/dist/sources/providers/install-types.js +14 -0
- package/dist/sources/providers/npm.js +160 -0
- package/dist/sources/providers/provider-utils.js +173 -0
- package/dist/sources/providers/sync-from-ref.js +45 -0
- package/dist/sources/providers/tar-utils.js +154 -0
- package/dist/{stash-providers → sources/providers}/website.js +60 -20
- package/dist/{stash-resolve.js → sources/resolve.js} +13 -12
- package/dist/{wiki.js → wiki/wiki.js} +18 -17
- package/dist/{workflow-authoring.js → workflows/authoring.js} +48 -17
- package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
- package/dist/{workflow-db.js → workflows/db.js} +1 -1
- package/dist/workflows/document-cache.js +20 -0
- package/dist/workflows/parser.js +379 -0
- package/dist/workflows/renderer.js +78 -0
- package/dist/{workflow-runs.js → workflows/runs.js} +84 -30
- package/dist/workflows/schema.js +11 -0
- package/dist/workflows/validator.js +48 -0
- package/docs/README.md +30 -0
- package/docs/migration/release-notes/0.0.13.md +4 -0
- package/docs/migration/release-notes/0.1.0.md +6 -0
- package/docs/migration/release-notes/0.2.0.md +6 -0
- package/docs/migration/release-notes/0.3.0.md +5 -0
- package/docs/migration/release-notes/0.5.0.md +6 -0
- package/docs/migration/release-notes/0.6.0.md +75 -0
- package/docs/migration/release-notes/README.md +21 -0
- package/package.json +3 -2
- package/dist/embedder.js +0 -351
- package/dist/errors.js +0 -34
- package/dist/migration-help.js +0 -110
- package/dist/registry-factory.js +0 -19
- package/dist/registry-install.js +0 -532
- package/dist/ripgrep.js +0 -2
- package/dist/stash-provider-factory.js +0 -35
- package/dist/stash-provider.js +0 -1
- package/dist/stash-providers/filesystem.js +0 -41
- package/dist/stash-providers/openviking.js +0 -348
- package/dist/stash-providers/provider-utils.js +0 -11
- package/dist/stash-types.js +0 -1
- package/dist/workflow-markdown.js +0 -251
- /package/dist/{markdown.js → core/markdown.js} +0 -0
- /package/dist/{warn.js → core/warn.js} +0 -0
- /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
- /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
- /package/dist/{github.js → integrations/github.js} +0 -0
- /package/dist/{registry-provider.js → registry/types.js} +0 -0
- /package/dist/{registry-types.js → sources/types.js} +0 -0
|
@@ -3,9 +3,9 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { fetchWithRetry } from "
|
|
7
|
-
import { UsageError } from "
|
|
8
|
-
import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "
|
|
6
|
+
import { fetchWithRetry, jsonWithByteCap } from "../core/common";
|
|
7
|
+
import { UsageError } from "../core/errors";
|
|
8
|
+
import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "../integrations/github";
|
|
9
9
|
/**
|
|
10
10
|
* Validate that a URL is safe to pass to git.
|
|
11
11
|
* Allowlists https:, http:, ssh:, git: schemes and git@ SSH shorthand.
|
|
@@ -451,7 +451,7 @@ function fileUriToPath(ref) {
|
|
|
451
451
|
/**
|
|
452
452
|
* Build a human-readable local ID from an absolute path.
|
|
453
453
|
* /home/user/akm/skills → ~/akm/skills
|
|
454
|
-
* /tmp/my-
|
|
454
|
+
* /tmp/my-stash → /tmp/my-stash
|
|
455
455
|
*/
|
|
456
456
|
function toReadableLocalId(absolutePath) {
|
|
457
457
|
const home = os.homedir();
|
|
@@ -571,16 +571,20 @@ export function maxSatisfying(versions, range) {
|
|
|
571
571
|
candidates.sort((a, b) => compareSemver(b.parsed, a.parsed));
|
|
572
572
|
return candidates[0].version;
|
|
573
573
|
}
|
|
574
|
+
// Cap JSON responses at 10 MB — npm package manifests and GitHub API
|
|
575
|
+
// responses are typically a few KB; a compromised registry streaming
|
|
576
|
+
// tens of MB of JSON is a DoS surface, not a feature.
|
|
577
|
+
const REGISTRY_JSON_BYTE_CAP = 10 * 1024 * 1024;
|
|
574
578
|
async function fetchJson(url, headers) {
|
|
575
579
|
const response = await fetchWithRetry(url, { headers });
|
|
576
580
|
if (!response.ok) {
|
|
577
581
|
throw new Error(`Request failed (${response.status}) for ${url}`);
|
|
578
582
|
}
|
|
579
|
-
return (
|
|
583
|
+
return jsonWithByteCap(response, REGISTRY_JSON_BYTE_CAP);
|
|
580
584
|
}
|
|
581
585
|
async function tryFetchJson(url, headers) {
|
|
582
586
|
const response = await fetchWithRetry(url, { headers });
|
|
583
587
|
if (!response.ok)
|
|
584
588
|
return null;
|
|
585
|
-
return (
|
|
589
|
+
return jsonWithByteCap(response, REGISTRY_JSON_BYTE_CAP);
|
|
586
590
|
}
|
|
@@ -91,30 +91,3 @@ export function detectAgentPlatforms() {
|
|
|
91
91
|
path: path.join(home, p.relPath),
|
|
92
92
|
}));
|
|
93
93
|
}
|
|
94
|
-
// ── OpenViking Detection ────────────────────────────────────────────────────
|
|
95
|
-
/**
|
|
96
|
-
* Check if an OpenViking server is reachable at the given URL.
|
|
97
|
-
* Uses the lightweight /api/v1/fs/stat endpoint (GET) rather than
|
|
98
|
-
* the search endpoint which requires a running search index.
|
|
99
|
-
*/
|
|
100
|
-
export async function detectOpenViking(url) {
|
|
101
|
-
const normalized = url.replace(/\/+$/, "");
|
|
102
|
-
try {
|
|
103
|
-
// Any HTTP response (even non-2xx) from the API endpoint means the server is reachable.
|
|
104
|
-
// Only network errors / timeouts indicate the server is truly unavailable.
|
|
105
|
-
await fetch(`${normalized}/api/v1/fs/stat?uri=${encodeURIComponent("viking://")}`, {
|
|
106
|
-
signal: AbortSignal.timeout(5000),
|
|
107
|
-
});
|
|
108
|
-
return { available: true, url: normalized };
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
// stat endpoint unreachable — try root URL as fallback
|
|
112
|
-
try {
|
|
113
|
-
await fetch(normalized, { signal: AbortSignal.timeout(5000) });
|
|
114
|
-
return { available: true, url: normalized };
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return { available: false, url: normalized };
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { IS_WINDOWS } from "
|
|
4
|
+
import { IS_WINDOWS } from "../core/common";
|
|
5
5
|
import { RG_BINARY, resolveRg } from "./ripgrep-resolve";
|
|
6
6
|
/**
|
|
7
7
|
* Platform and architecture detection for ripgrep binary downloads.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { IS_WINDOWS } from "
|
|
4
|
-
import { getBinDir } from "
|
|
3
|
+
import { IS_WINDOWS } from "../core/common";
|
|
4
|
+
import { getBinDir } from "../core/paths";
|
|
5
5
|
export const RG_BINARY = IS_WINDOWS ? "rg.exe" : "rg";
|
|
6
6
|
function canExecute(filePath) {
|
|
7
7
|
if (!fs.existsSync(filePath))
|
|
@@ -7,25 +7,26 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import * as p from "@clack/prompts";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { akmIndex } from "
|
|
16
|
-
import {
|
|
17
|
-
import { probeLlmCapabilities } from "
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
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";
|
|
20
21
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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,11 +137,11 @@ 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 (!
|
|
140
|
+
if (!isTransformersAvailable()) {
|
|
140
141
|
const spin = p.spinner();
|
|
141
142
|
spin.start("Installing @huggingface/transformers...");
|
|
142
143
|
try {
|
|
143
|
-
const pkgRoot = path.resolve(import.meta.dir, "
|
|
144
|
+
const pkgRoot = path.resolve(import.meta.dir, "../..");
|
|
144
145
|
const proc = Bun.spawn(["bun", "add", "@huggingface/transformers"], {
|
|
145
146
|
cwd: pkgRoot,
|
|
146
147
|
stdout: "pipe",
|
|
@@ -499,40 +500,44 @@ async function stepRegistries(current) {
|
|
|
499
500
|
/**
|
|
500
501
|
* @internal Exported for testing only.
|
|
501
502
|
*/
|
|
502
|
-
export async function
|
|
503
|
-
const stashes = [...(current.stashes ?? [])];
|
|
503
|
+
export async function stepAddSources(current) {
|
|
504
|
+
const stashes = [...(current.sources ?? current.stashes ?? [])];
|
|
504
505
|
if (stashes.length > 0) {
|
|
505
506
|
p.log.info(`You have ${stashes.length} existing stash source(s).`);
|
|
506
507
|
}
|
|
507
508
|
// ── Recommended GitHub repos ───────────────────────────────────────────
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
existingUrls.
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
}
|
|
@@ -542,7 +547,6 @@ export async function stepStashSources(current) {
|
|
|
542
547
|
const action = await prompt(() => p.select({
|
|
543
548
|
message: "Add another stash source?",
|
|
544
549
|
options: [
|
|
545
|
-
{ value: "openviking", label: "OpenViking server", hint: "remote stash" },
|
|
546
550
|
{ value: "github-repo", label: "GitHub repository", hint: "custom URL" },
|
|
547
551
|
{ value: "filesystem", label: "Filesystem path", hint: "local directory" },
|
|
548
552
|
{ value: "done", label: "Done — no more sources" },
|
|
@@ -552,45 +556,6 @@ export async function stepStashSources(current) {
|
|
|
552
556
|
addMore = false;
|
|
553
557
|
break;
|
|
554
558
|
}
|
|
555
|
-
if (action === "openviking") {
|
|
556
|
-
const url = await promptOrBack(() => p.text({
|
|
557
|
-
message: "Enter the OpenViking server URL:",
|
|
558
|
-
placeholder: "https://your-openviking-server.example.com",
|
|
559
|
-
validate: (v) => {
|
|
560
|
-
if (!v?.trim())
|
|
561
|
-
return "URL cannot be empty";
|
|
562
|
-
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
563
|
-
return "URL must start with http:// or https://";
|
|
564
|
-
},
|
|
565
|
-
}));
|
|
566
|
-
if (url === null)
|
|
567
|
-
continue;
|
|
568
|
-
const spin = p.spinner();
|
|
569
|
-
spin.start("Checking OpenViking server...");
|
|
570
|
-
const result = await detectOpenViking(url.trim());
|
|
571
|
-
if (result.available) {
|
|
572
|
-
spin.stop("Server is reachable");
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
spin.stop("Server not reachable — adding anyway (it may be temporarily down)");
|
|
576
|
-
}
|
|
577
|
-
const name = await promptOrBack(() => p.text({
|
|
578
|
-
message: "Give this stash a name (optional):",
|
|
579
|
-
placeholder: "my-openviking",
|
|
580
|
-
}));
|
|
581
|
-
if (name === null)
|
|
582
|
-
continue;
|
|
583
|
-
// Use the normalized URL from detection (trailing slashes stripped)
|
|
584
|
-
const entry = { type: "openviking", url: result.url };
|
|
585
|
-
if (name.trim())
|
|
586
|
-
entry.name = name.trim();
|
|
587
|
-
if (!stashes.some((s) => s.url === entry.url)) {
|
|
588
|
-
stashes.push(entry);
|
|
589
|
-
}
|
|
590
|
-
else {
|
|
591
|
-
p.log.warn("This URL is already configured.");
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
559
|
if (action === "github-repo") {
|
|
595
560
|
const url = await promptOrBack(() => p.text({
|
|
596
561
|
message: "Enter the GitHub repository URL:",
|
|
@@ -685,60 +650,128 @@ async function stepAgentPlatforms(current) {
|
|
|
685
650
|
return entries;
|
|
686
651
|
}
|
|
687
652
|
// ── Main Wizard ─────────────────────────────────────────────────────────────
|
|
653
|
+
/**
|
|
654
|
+
* Build the canonical list of `SetupStep`s for the interactive wizard.
|
|
655
|
+
* Exposed (and exported) so tests and `akm init` can compose subsets.
|
|
656
|
+
*
|
|
657
|
+
* Each step wraps the existing `step*` functions, accumulating its result
|
|
658
|
+
* into the shared `SetupContext`. The `nonInteractive` flag controls
|
|
659
|
+
* inclusion in `akm init` (a non-interactive preset of `akm setup`).
|
|
660
|
+
*/
|
|
661
|
+
export function buildSetupSteps(options) {
|
|
662
|
+
const outcome = { semantic: options.semanticSearchOutcome };
|
|
663
|
+
// Local cache of Ollama-detected fields surfaced from the embedding step
|
|
664
|
+
// to the LLM step. Mutable by design — `stepLlm` needs them.
|
|
665
|
+
let ollamaEndpoint;
|
|
666
|
+
let ollamaChatModels;
|
|
667
|
+
const steps = [
|
|
668
|
+
{
|
|
669
|
+
id: "stash-dir",
|
|
670
|
+
label: "Stash Directory",
|
|
671
|
+
nonInteractive: true,
|
|
672
|
+
async run(ctx) {
|
|
673
|
+
const stashDir = await stepStashDir(ctx.config);
|
|
674
|
+
ctx.apply({ stashDir });
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: "embedding",
|
|
679
|
+
label: "Embedding",
|
|
680
|
+
async run(ctx) {
|
|
681
|
+
if (!options.online) {
|
|
682
|
+
ctx.apply({ embedding: ctx.config.embedding });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const result = await stepOllama(ctx.config);
|
|
686
|
+
ollamaEndpoint = result.ollamaEndpoint;
|
|
687
|
+
ollamaChatModels = result.ollamaChatModels;
|
|
688
|
+
ctx.apply({ embedding: result.embedding });
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
id: "llm",
|
|
693
|
+
label: "LLM Provider",
|
|
694
|
+
async run(ctx) {
|
|
695
|
+
if (!options.online) {
|
|
696
|
+
ctx.apply({ llm: ctx.config.llm });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const llm = await stepLlm(ctx.config, ollamaEndpoint, ollamaChatModels);
|
|
700
|
+
ctx.apply({ llm });
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
id: "semantic-search",
|
|
705
|
+
label: "Semantic Search",
|
|
706
|
+
async run(ctx) {
|
|
707
|
+
const semantic = await stepSemanticSearch(ctx.config, ctx.config.embedding);
|
|
708
|
+
outcome.semantic = semantic;
|
|
709
|
+
ctx.apply({ semanticSearchMode: semantic.mode });
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
id: "registries",
|
|
714
|
+
label: "Registries",
|
|
715
|
+
async run(ctx) {
|
|
716
|
+
const registries = await stepRegistries(ctx.config);
|
|
717
|
+
ctx.apply({ registries });
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: "stash-sources",
|
|
722
|
+
label: "Stash Sources",
|
|
723
|
+
async run(ctx) {
|
|
724
|
+
const stashes = await stepAddSources(ctx.config);
|
|
725
|
+
const platforms = await stepAgentPlatforms(ctx.config);
|
|
726
|
+
const merged = [...stashes];
|
|
727
|
+
for (const ps of platforms) {
|
|
728
|
+
if (!merged.some((s) => s.path === ps.path))
|
|
729
|
+
merged.push(ps);
|
|
730
|
+
}
|
|
731
|
+
ctx.apply({ sources: merged.length > 0 ? merged : undefined });
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
];
|
|
735
|
+
return { steps, outcome };
|
|
736
|
+
}
|
|
688
737
|
export async function runSetupWizard() {
|
|
689
738
|
p.intro("akm setup");
|
|
690
739
|
const current = loadUserConfig();
|
|
691
740
|
const configPath = getConfigPath();
|
|
692
|
-
// Step 1: Stash directory
|
|
693
|
-
p.log.step("Step 1: Stash Directory");
|
|
694
|
-
const stashDir = await stepStashDir(current);
|
|
695
741
|
// Quick connectivity check — skip network-dependent steps when offline
|
|
696
742
|
const online = await isOnline();
|
|
697
743
|
if (!online) {
|
|
698
744
|
p.log.warn("No network connectivity detected. Skipping Ollama detection and remote embedding checks.\n" +
|
|
699
745
|
"Local-only setup will continue. Re-run `akm setup` when online for full configuration.");
|
|
700
746
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
|
747
|
+
const ctx = createSetupContext(current, { nonInteractive: false });
|
|
748
|
+
const { steps, outcome } = buildSetupSteps({
|
|
749
|
+
online,
|
|
750
|
+
semanticSearchOutcome: { mode: current.semanticSearchMode, prepareAssets: false },
|
|
751
|
+
});
|
|
752
|
+
// Wrap each step with a `p.log.step()` header so the wizard UI is
|
|
753
|
+
// unchanged. The canonical `runSetupSteps()` runner is used directly by
|
|
754
|
+
// `akm init` (non-interactive) and by tests.
|
|
755
|
+
const labeledSteps = steps.map((step) => ({
|
|
756
|
+
...step,
|
|
757
|
+
async run(stepCtx) {
|
|
758
|
+
p.log.step(step.label);
|
|
759
|
+
await step.run(stepCtx);
|
|
760
|
+
},
|
|
761
|
+
}));
|
|
762
|
+
await runSetupSteps(labeledSteps, ctx);
|
|
730
763
|
const newConfig = {
|
|
731
|
-
...
|
|
732
|
-
|
|
733
|
-
embedding,
|
|
734
|
-
llm,
|
|
735
|
-
registries,
|
|
736
|
-
stashes: allStashes.length > 0 ? allStashes : undefined,
|
|
737
|
-
// Preserve existing fields
|
|
738
|
-
semanticSearchMode: semanticSearchMode.mode,
|
|
764
|
+
...ctx.config,
|
|
765
|
+
// Preserve fields the steps don't manage explicitly.
|
|
739
766
|
installed: current.installed,
|
|
740
767
|
output: current.output,
|
|
741
768
|
};
|
|
769
|
+
const semanticSearchMode = outcome.semantic;
|
|
770
|
+
const stashDir = newConfig.stashDir ?? current.stashDir ?? getDefaultStashDir();
|
|
771
|
+
const embedding = newConfig.embedding;
|
|
772
|
+
const llm = newConfig.llm;
|
|
773
|
+
const registries = newConfig.registries;
|
|
774
|
+
const allStashes = newConfig.stashes ?? [];
|
|
742
775
|
// Confirm before saving
|
|
743
776
|
const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
744
777
|
p.note([
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { isWithin } from "
|
|
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 };
|