akm-cli 0.6.1 → 0.7.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.
- package/CHANGELOG.md +66 -0
- package/dist/{cli.js → src/cli.js} +620 -26
- package/dist/{commands → src/commands}/config-cli.js +5 -4
- package/dist/src/commands/distill.js +283 -0
- package/dist/src/commands/events.js +108 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/{commands → src/commands}/installed-stashes.js +1 -1
- package/dist/src/commands/proposal.js +119 -0
- package/dist/src/commands/propose.js +171 -0
- package/dist/src/commands/reflect.js +193 -0
- package/dist/{commands → src/commands}/registry-search.js +2 -1
- package/dist/{commands → src/commands}/remember.js +12 -0
- package/dist/{commands → src/commands}/search.js +74 -1
- package/dist/{commands → src/commands}/self-update.js +4 -3
- package/dist/{commands → src/commands}/show.js +44 -0
- package/dist/{core → src/core}/asset-ref.js +5 -5
- package/dist/{core → src/core}/asset-spec.js +12 -0
- package/dist/{core → src/core}/common.js +1 -1
- package/dist/{core → src/core}/config.js +175 -121
- package/dist/{core → src/core}/errors.js +4 -0
- package/dist/src/core/events.js +239 -0
- package/dist/src/core/lesson-lint.js +86 -0
- package/dist/src/core/proposals.js +406 -0
- package/dist/src/core/warn.js +72 -0
- package/dist/{core → src/core}/write-source.js +80 -5
- package/dist/{indexer → src/indexer}/db-search.js +113 -24
- package/dist/{indexer → src/indexer}/db.js +76 -23
- package/dist/{indexer → src/indexer}/file-context.js +0 -3
- package/dist/src/indexer/graph-boost.js +179 -0
- package/dist/src/indexer/graph-extraction.js +212 -0
- package/dist/{indexer → src/indexer}/indexer.js +73 -6
- package/dist/src/indexer/memory-inference.js +263 -0
- package/dist/{indexer → src/indexer}/metadata.js +111 -3
- package/dist/src/integrations/agent/config.js +292 -0
- package/dist/src/integrations/agent/detect.js +94 -0
- package/dist/src/integrations/agent/index.js +17 -0
- package/dist/src/integrations/agent/profiles.js +65 -0
- package/dist/src/integrations/agent/prompts.js +167 -0
- package/dist/src/integrations/agent/spawn.js +221 -0
- package/dist/{integrations → src/integrations}/lockfile.js +0 -26
- package/dist/{llm → src/llm}/client.js +33 -2
- package/dist/src/llm/feature-gate.js +108 -0
- package/dist/src/llm/graph-extract.js +107 -0
- package/dist/src/llm/index-passes.js +35 -0
- package/dist/src/llm/memory-infer.js +86 -0
- package/dist/{output → src/output}/renderers.js +60 -1
- package/dist/src/output/shapes.js +516 -0
- package/dist/{output → src/output}/text.js +447 -4
- package/dist/{registry → src/registry}/build-index.js +14 -4
- package/dist/{registry → src/registry}/factory.js +0 -8
- package/dist/{registry → src/registry}/providers/static-index.js +3 -2
- package/dist/{registry → src/registry}/resolve.js +68 -2
- package/dist/{setup → src/setup}/setup.js +43 -5
- package/dist/{sources → src/sources}/providers/git.js +7 -15
- package/dist/tests/add-website-source.test.js +119 -0
- package/dist/tests/agent/agent-config-loader.test.js +70 -0
- package/dist/tests/agent/agent-config.test.js +221 -0
- package/dist/tests/agent/agent-detect.test.js +100 -0
- package/dist/tests/agent/agent-spawn.test.js +234 -0
- package/dist/tests/agent-output.test.js +186 -0
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
- package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
- package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
- package/dist/tests/asset-ref.test.js +192 -0
- package/dist/tests/asset-registry.test.js +103 -0
- package/dist/tests/asset-spec.test.js +241 -0
- package/dist/tests/bench/attribution.test.js +995 -0
- package/dist/tests/bench/cleanup-sigint.test.js +83 -0
- package/dist/tests/bench/cleanup.js +203 -0
- package/dist/tests/bench/cleanup.test.js +166 -0
- package/dist/tests/bench/cli.js +683 -0
- package/dist/tests/bench/cli.test.js +177 -0
- package/dist/tests/bench/compare.test.js +556 -0
- package/dist/tests/bench/corpus.js +314 -0
- package/dist/tests/bench/corpus.test.js +258 -0
- package/dist/tests/bench/driver.js +346 -0
- package/dist/tests/bench/driver.test.js +443 -0
- package/dist/tests/bench/evolve-metrics.js +179 -0
- package/dist/tests/bench/evolve-metrics.test.js +187 -0
- package/dist/tests/bench/evolve.js +580 -0
- package/dist/tests/bench/evolve.test.js +616 -0
- package/dist/tests/bench/failure-modes.test.js +300 -0
- package/dist/tests/bench/feedback-integrity.test.js +456 -0
- package/dist/tests/bench/leakage.test.js +125 -0
- package/dist/tests/bench/learning-curve.test.js +133 -0
- package/dist/tests/bench/metrics.js +2319 -0
- package/dist/tests/bench/metrics.test.js +1144 -0
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
- package/dist/tests/bench/report.js +1821 -0
- package/dist/tests/bench/report.test.js +989 -0
- package/dist/tests/bench/runner.js +536 -0
- package/dist/tests/bench/runner.test.js +958 -0
- package/dist/tests/bench/search-bridge.test.js +331 -0
- package/dist/tests/bench/tmp.js +41 -0
- package/dist/tests/bench/trajectory.js +116 -0
- package/dist/tests/bench/trajectory.test.js +127 -0
- package/dist/tests/bench/verifier.js +109 -0
- package/dist/tests/bench/verifier.test.js +118 -0
- package/dist/tests/bench/workflow-evaluator.js +557 -0
- package/dist/tests/bench/workflow-evaluator.test.js +421 -0
- package/dist/tests/bench/workflow-spec.js +358 -0
- package/dist/tests/bench/workflow-spec.test.js +363 -0
- package/dist/tests/bench/workflow-trace.js +438 -0
- package/dist/tests/bench/workflow-trace.test.js +254 -0
- package/dist/tests/benchmark-search-quality.js +536 -0
- package/dist/tests/benchmark-suite.js +1441 -0
- package/dist/tests/capture-cli.test.js +112 -0
- package/dist/tests/cli-errors.test.js +203 -0
- package/dist/tests/commands/events.test.js +370 -0
- package/dist/tests/commands/history.test.js +223 -0
- package/dist/tests/commands/import.test.js +103 -0
- package/dist/tests/commands/proposal-cli.test.js +209 -0
- package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
- package/dist/tests/commands/remember.test.js +97 -0
- package/dist/tests/commands/scope-flags.test.js +300 -0
- package/dist/tests/commands/search.test.js +537 -0
- package/dist/tests/commands/show-indexer-parity.test.js +117 -0
- package/dist/tests/commands/show.test.js +294 -0
- package/dist/tests/common.test.js +266 -0
- package/dist/tests/completions.test.js +142 -0
- package/dist/tests/config-cli.test.js +193 -0
- package/dist/tests/config-llm-features.test.js +139 -0
- package/dist/tests/config.test.js +544 -0
- package/dist/tests/contracts/migration-baseline.test.js +43 -0
- package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
- package/dist/tests/contracts/spec-helpers.js +46 -0
- package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
- package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
- package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
- package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
- package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
- package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
- package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
- package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
- package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
- package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
- package/dist/tests/core/write-source.test.js +366 -0
- package/dist/tests/curate-command.test.js +87 -0
- package/dist/tests/db-scoring.test.js +201 -0
- package/dist/tests/db.test.js +654 -0
- package/dist/tests/distill-cli-flag.test.js +208 -0
- package/dist/tests/distill.test.js +515 -0
- package/dist/tests/docker-install.test.js +120 -0
- package/dist/tests/e2e.test.js +1398 -0
- package/dist/tests/embedder.test.js +340 -0
- package/dist/tests/embedding-model-config.test.js +379 -0
- package/dist/tests/feedback-command.test.js +172 -0
- package/dist/tests/file-context.test.js +552 -0
- package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
- package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
- package/dist/tests/fixtures/stashes/load.js +166 -0
- package/dist/tests/fixtures/stashes/load.test.js +88 -0
- package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
- package/dist/tests/frontmatter.test.js +190 -0
- package/dist/tests/fts-field-weighting.test.js +254 -0
- package/dist/tests/fuzzy-search.test.js +230 -0
- package/dist/tests/git-provider-clone.test.js +45 -0
- package/dist/tests/github.test.js +161 -0
- package/dist/tests/graph-boost-ranking.test.js +305 -0
- package/dist/tests/graph-extraction.test.js +282 -0
- package/dist/tests/helpers/usage-events.js +8 -0
- package/dist/tests/index-pass-llm.test.js +161 -0
- package/dist/tests/indexer.test.js +559 -0
- package/dist/tests/info-command.test.js +166 -0
- package/dist/tests/init.test.js +69 -0
- package/dist/tests/install-script.test.js +246 -0
- package/dist/tests/integration/agent-real-profile.test.js +94 -0
- package/dist/tests/issue-36-repro.test.js +304 -0
- package/dist/tests/issues-191-194.test.js +160 -0
- package/dist/tests/lesson-lint.test.js +111 -0
- package/dist/tests/llm-client.test.js +115 -0
- package/dist/tests/llm-feature-gate.test.js +151 -0
- package/dist/tests/llm.test.js +139 -0
- package/dist/tests/lockfile.test.js +216 -0
- package/dist/tests/manifest.test.js +205 -0
- package/dist/tests/markdown.test.js +126 -0
- package/dist/tests/matchers-unit.test.js +189 -0
- package/dist/tests/memory-inference.test.js +299 -0
- package/dist/tests/merge-scoring.test.js +136 -0
- package/dist/tests/metadata.test.js +313 -0
- package/dist/tests/migration-help.test.js +89 -0
- package/dist/tests/origin-resolve.test.js +124 -0
- package/dist/tests/output-baseline.test.js +217 -0
- package/dist/tests/output-shapes-unit.test.js +476 -0
- package/dist/tests/parallel-search.test.js +272 -0
- package/dist/tests/parameter-metadata.test.js +365 -0
- package/dist/tests/paths.test.js +177 -0
- package/dist/tests/progressive-disclosure.test.js +280 -0
- package/dist/tests/proposals.test.js +279 -0
- package/dist/tests/proposed-quality.test.js +271 -0
- package/dist/tests/provider-registry.test.js +32 -0
- package/dist/tests/ranking-regression.test.js +548 -0
- package/dist/tests/reflect-propose.test.js +455 -0
- package/dist/tests/registry-build-index.test.js +378 -0
- package/dist/tests/registry-cli.test.js +290 -0
- package/dist/tests/registry-index-v2.test.js +430 -0
- package/dist/tests/registry-install.test.js +728 -0
- package/dist/tests/registry-providers/parity.test.js +189 -0
- package/dist/tests/registry-providers/skills-sh.test.js +309 -0
- package/dist/tests/registry-providers/static-index.test.js +204 -0
- package/dist/tests/registry-resolve.test.js +126 -0
- package/dist/tests/registry-search.test.js +723 -0
- package/dist/tests/remember-frontmatter.test.js +380 -0
- package/dist/tests/remember-unit.test.js +123 -0
- package/dist/tests/ripgrep-install.test.js +251 -0
- package/dist/tests/ripgrep-resolve.test.js +108 -0
- package/dist/tests/ripgrep.test.js +163 -0
- package/dist/tests/save-command.test.js +94 -0
- package/dist/tests/save-trust-qa-fixes.test.js +270 -0
- package/dist/tests/scoring-pipeline.test.js +648 -0
- package/dist/tests/search-include-proposed-cli.test.js +118 -0
- package/dist/tests/self-update.test.js +442 -0
- package/dist/tests/semantic-search-e2e.test.js +512 -0
- package/dist/tests/semantic-status.test.js +471 -0
- package/dist/tests/setup-run.integration.js +877 -0
- package/dist/tests/setup-wizard.test.js +198 -0
- package/dist/tests/setup.test.js +131 -0
- package/dist/tests/source-add.test.js +11 -0
- package/dist/tests/source-clone.test.js +254 -0
- package/dist/tests/source-manage.test.js +366 -0
- package/dist/tests/source-providers/filesystem.test.js +82 -0
- package/dist/tests/source-providers/git.test.js +252 -0
- package/dist/tests/source-providers/website.test.js +128 -0
- package/dist/tests/source-qa-fixes.test.js +268 -0
- package/dist/tests/source-registry.test.js +350 -0
- package/dist/tests/source-resolve.test.js +100 -0
- package/dist/tests/source-source.test.js +221 -0
- package/dist/tests/source.test.js +533 -0
- package/dist/tests/tar-utils-scan.test.js +73 -0
- package/dist/tests/toggle-components.test.js +73 -0
- package/dist/tests/usage-telemetry.test.js +265 -0
- package/dist/tests/utility-scoring.test.js +558 -0
- package/dist/tests/vault-load-error.test.js +78 -0
- package/dist/tests/vault-qa-fixes.test.js +194 -0
- package/dist/tests/vault.test.js +429 -0
- package/dist/tests/vector-search.test.js +608 -0
- package/dist/tests/walker.test.js +252 -0
- package/dist/tests/wave2-cluster-bc.test.js +228 -0
- package/dist/tests/wave2-cluster-d.test.js +180 -0
- package/dist/tests/wave2-cluster-e.test.js +179 -0
- package/dist/tests/wiki-qa-fixes.test.js +270 -0
- package/dist/tests/wiki.test.js +529 -0
- package/dist/tests/workflow-cli.test.js +271 -0
- package/dist/tests/workflow-markdown.test.js +171 -0
- package/dist/tests/workflow-path-escape.test.js +132 -0
- package/dist/tests/workflow-qa-fixes.test.js +377 -0
- package/dist/tests/workflows/indexer-rejection.test.js +213 -0
- package/docs/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +244 -0
- package/package.json +2 -2
- package/dist/core/warn.js +0 -27
- package/dist/output/shapes.js +0 -212
- /package/dist/{commands → src/commands}/completions.js +0 -0
- /package/dist/{commands → src/commands}/curate.js +0 -0
- /package/dist/{commands → src/commands}/info.js +0 -0
- /package/dist/{commands → src/commands}/init.js +0 -0
- /package/dist/{commands → src/commands}/install-audit.js +0 -0
- /package/dist/{commands → src/commands}/migration-help.js +0 -0
- /package/dist/{commands → src/commands}/source-add.js +0 -0
- /package/dist/{commands → src/commands}/source-clone.js +0 -0
- /package/dist/{commands → src/commands}/source-manage.js +0 -0
- /package/dist/{commands → src/commands}/vault.js +0 -0
- /package/dist/{core → src/core}/asset-registry.js +0 -0
- /package/dist/{core → src/core}/frontmatter.js +0 -0
- /package/dist/{core → src/core}/markdown.js +0 -0
- /package/dist/{core → src/core}/paths.js +0 -0
- /package/dist/{indexer → src/indexer}/manifest.js +0 -0
- /package/dist/{indexer → src/indexer}/matchers.js +0 -0
- /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
- /package/dist/{indexer → src/indexer}/search-source.js +0 -0
- /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
- /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
- /package/dist/{indexer → src/indexer}/walker.js +0 -0
- /package/dist/{integrations → src/integrations}/github.js +0 -0
- /package/dist/{llm → src/llm}/embedder.js +0 -0
- /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
- /package/dist/{llm → src/llm}/embedders/local.js +0 -0
- /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
- /package/dist/{llm → src/llm}/embedders/types.js +0 -0
- /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
- /package/dist/{output → src/output}/cli-hints.js +0 -0
- /package/dist/{output → src/output}/context.js +0 -0
- /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
- /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
- /package/dist/{registry → src/registry}/providers/index.js +0 -0
- /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
- /package/dist/{registry → src/registry}/providers/types.js +0 -0
- /package/dist/{registry → src/registry}/types.js +0 -0
- /package/dist/{setup → src/setup}/detect.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
- /package/dist/{setup → src/setup}/steps.js +0 -0
- /package/dist/{sources → src/sources}/include.js +0 -0
- /package/dist/{sources → src/sources}/provider-factory.js +0 -0
- /package/dist/{sources → src/sources}/provider.js +0 -0
- /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
- /package/dist/{sources → src/sources}/providers/index.js +0 -0
- /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
- /package/dist/{sources → src/sources}/providers/npm.js +0 -0
- /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
- /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/website.js +0 -0
- /package/dist/{sources → src/sources}/resolve.js +0 -0
- /package/dist/{sources → src/sources}/types.js +0 -0
- /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
- /package/dist/{version.js → src/version.js} +0 -0
- /package/dist/{wiki → src/wiki}/wiki.js +0 -0
- /package/dist/{workflows → src/workflows}/authoring.js +0 -0
- /package/dist/{workflows → src/workflows}/cli.js +0 -0
- /package/dist/{workflows → src/workflows}/db.js +0 -0
- /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
- /package/dist/{workflows → src/workflows}/parser.js +0 -0
- /package/dist/{workflows → src/workflows}/renderer.js +0 -0
- /package/dist/{workflows → src/workflows}/runs.js +0 -0
- /package/dist/{workflows → src/workflows}/schema.js +0 -0
- /package/dist/{workflows → src/workflows}/validator.js +0 -0
|
@@ -14,6 +14,7 @@ import { getDefaultStashDir } from "../core/paths";
|
|
|
14
14
|
import { closeDatabase, isVecAvailable, openDatabase } from "../indexer/db";
|
|
15
15
|
import { akmIndex } from "../indexer/indexer";
|
|
16
16
|
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/semantic-status";
|
|
17
|
+
import { detectAgentCliProfiles, pickDefaultAgentProfile, } from "../integrations/agent";
|
|
17
18
|
import { probeLlmCapabilities } from "../llm/client";
|
|
18
19
|
import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder";
|
|
19
20
|
import { detectAgentPlatforms, detectOllama } from "./detect";
|
|
@@ -284,6 +285,7 @@ async function stepOllama(current) {
|
|
|
284
285
|
bge: 384,
|
|
285
286
|
};
|
|
286
287
|
const guessedDim = Object.entries(knownDims).find(([k]) => embChoice.includes(k))?.[1] ?? 384;
|
|
288
|
+
p.note("Embedding dimension must match the model. Common values: 384 (BGE small), 768 (BGE base), 1024 (BGE large). Press Enter to accept the detected default.", "Embedding dimension");
|
|
287
289
|
const dimChoice = await prompt(() => p.text({
|
|
288
290
|
message: `Embedding dimension for ${embChoice}:`,
|
|
289
291
|
placeholder: String(guessedDim),
|
|
@@ -313,7 +315,6 @@ const LLM_PRESETS = [
|
|
|
313
315
|
endpoint: "https://api.anthropic.com/v1/chat/completions",
|
|
314
316
|
defaultModel: "claude-sonnet-4-5",
|
|
315
317
|
hint: "beta OpenAI-compat layer; set AKM_LLM_API_KEY; override the model if the default is unavailable",
|
|
316
|
-
contextWindow: 200_000,
|
|
317
318
|
},
|
|
318
319
|
{
|
|
319
320
|
value: "openai",
|
|
@@ -321,7 +322,6 @@ const LLM_PRESETS = [
|
|
|
321
322
|
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
322
323
|
defaultModel: "gpt-4o-mini",
|
|
323
324
|
hint: "AKM_LLM_API_KEY required",
|
|
324
|
-
contextWindow: 128_000,
|
|
325
325
|
},
|
|
326
326
|
{
|
|
327
327
|
value: "google",
|
|
@@ -329,7 +329,6 @@ const LLM_PRESETS = [
|
|
|
329
329
|
endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
|
|
330
330
|
defaultModel: "gemini-2.0-flash",
|
|
331
331
|
hint: "OpenAI-compat endpoint, AKM_LLM_API_KEY required",
|
|
332
|
-
contextWindow: 1_000_000,
|
|
333
332
|
},
|
|
334
333
|
];
|
|
335
334
|
/**
|
|
@@ -431,7 +430,6 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
|
|
|
431
430
|
model: model.trim() || preset.defaultModel,
|
|
432
431
|
temperature: 0.3,
|
|
433
432
|
maxTokens: 1024,
|
|
434
|
-
contextWindow: preset.contextWindow,
|
|
435
433
|
};
|
|
436
434
|
}
|
|
437
435
|
// Remind the user about API key placement. We do not offer a "store in config"
|
|
@@ -649,6 +647,29 @@ async function stepAgentPlatforms(current) {
|
|
|
649
647
|
}
|
|
650
648
|
return entries;
|
|
651
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Detect installed agent CLIs and produce an updated `agent` config block
|
|
652
|
+
* with a sensible `default` (the first detected profile that the user has
|
|
653
|
+
* not already overridden).
|
|
654
|
+
*
|
|
655
|
+
* Pure-ish: file system / PATH probes are routed through `detectFn` so
|
|
656
|
+
* tests can drive the branches without touching the real PATH.
|
|
657
|
+
*
|
|
658
|
+
* @internal Exported for testing only.
|
|
659
|
+
*/
|
|
660
|
+
export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles) {
|
|
661
|
+
const detections = detectFn(current.agent);
|
|
662
|
+
const defaultName = pickDefaultAgentProfile(detections, current.agent?.default);
|
|
663
|
+
// No installed agents found and no existing config → leave block absent.
|
|
664
|
+
if (!defaultName && !current.agent) {
|
|
665
|
+
return { detections };
|
|
666
|
+
}
|
|
667
|
+
const agent = {
|
|
668
|
+
...(current.agent ?? {}),
|
|
669
|
+
...(defaultName ? { default: defaultName } : {}),
|
|
670
|
+
};
|
|
671
|
+
return { agent, detections };
|
|
672
|
+
}
|
|
652
673
|
// ── Main Wizard ─────────────────────────────────────────────────────────────
|
|
653
674
|
/**
|
|
654
675
|
* Build the canonical list of `SetupStep`s for the interactive wizard.
|
|
@@ -731,6 +752,23 @@ export function buildSetupSteps(options) {
|
|
|
731
752
|
ctx.apply({ sources: merged.length > 0 ? merged : undefined });
|
|
732
753
|
},
|
|
733
754
|
},
|
|
755
|
+
{
|
|
756
|
+
id: "agent-cli",
|
|
757
|
+
label: "Agent CLI",
|
|
758
|
+
nonInteractive: true,
|
|
759
|
+
async run(ctx) {
|
|
760
|
+
const result = stepAgentCliDetection(ctx.config);
|
|
761
|
+
const detected = result.detections.filter((d) => d.available);
|
|
762
|
+
if (detected.length > 0) {
|
|
763
|
+
p.log.info(`Detected agent CLIs: ${detected.map((d) => d.name).join(", ")}.` +
|
|
764
|
+
(result.agent?.default ? ` Default profile: ${result.agent.default}.` : ""));
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
|
|
768
|
+
}
|
|
769
|
+
ctx.apply({ agent: result.agent });
|
|
770
|
+
},
|
|
771
|
+
},
|
|
734
772
|
];
|
|
735
773
|
return { steps, outcome };
|
|
736
774
|
}
|
|
@@ -771,7 +809,7 @@ export async function runSetupWizard() {
|
|
|
771
809
|
const embedding = newConfig.embedding;
|
|
772
810
|
const llm = newConfig.llm;
|
|
773
811
|
const registries = newConfig.registries;
|
|
774
|
-
const allStashes = newConfig.stashes ?? [];
|
|
812
|
+
const allStashes = newConfig.sources ?? newConfig.stashes ?? [];
|
|
775
813
|
// Confirm before saving
|
|
776
814
|
const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
777
815
|
p.note([
|
|
@@ -6,6 +6,7 @@ import { resolveStashDir } from "../../core/common";
|
|
|
6
6
|
import { loadConfig } from "../../core/config";
|
|
7
7
|
import { ConfigError, UsageError } from "../../core/errors";
|
|
8
8
|
import { getRegistryCacheDir, getRegistryIndexCacheDir } from "../../core/paths";
|
|
9
|
+
import { sanitizeCommitMessage } from "../../core/write-source";
|
|
9
10
|
import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../../registry/resolve";
|
|
10
11
|
import { registerSourceProvider } from "../provider-factory";
|
|
11
12
|
import { applyAkmIncludeConfig, buildInstallCacheDir, copyDirectoryContents, detectStashRoot, isDirectory, isExpired, sanitizeString, } from "./provider-utils";
|
|
@@ -77,20 +78,6 @@ function getCachePaths(repoUrl) {
|
|
|
77
78
|
const key = createHash("sha256").update(repoUrl).digest("hex").slice(0, 16);
|
|
78
79
|
const cacheRoot = getRegistryIndexCacheDir();
|
|
79
80
|
const rootDir = path.join(cacheRoot, `git-${key}`);
|
|
80
|
-
// One-time silent migration: legacy `context-hub-${key}` directories were
|
|
81
|
-
// created for ALL git stashes (not just the andrewyng/context-hub repo). If
|
|
82
|
-
// the new path doesn't yet exist but the legacy one does, rename it in place
|
|
83
|
-
// so existing clones aren't silently invalidated. Failures are non-fatal —
|
|
84
|
-
// worst case the repo is re-cloned on the next refresh.
|
|
85
|
-
try {
|
|
86
|
-
const legacyRootDir = path.join(cacheRoot, `context-hub-${key}`);
|
|
87
|
-
if (!fs.existsSync(rootDir) && fs.existsSync(legacyRootDir)) {
|
|
88
|
-
fs.renameSync(legacyRootDir, rootDir);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
/* migration is best-effort */
|
|
93
|
-
}
|
|
94
81
|
return {
|
|
95
82
|
rootDir,
|
|
96
83
|
repoDir: path.join(rootDir, "repo"),
|
|
@@ -398,7 +385,12 @@ function parseGitRepoUrl(rawUrl) {
|
|
|
398
385
|
*/
|
|
399
386
|
export function saveGitStash(name, message, writableOverride) {
|
|
400
387
|
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
401
|
-
|
|
388
|
+
// Sanitize the user-supplied message: strip CR/LF/NUL, collapse whitespace,
|
|
389
|
+
// clamp length. An attacker can otherwise pass `--message "subject\n\n\
|
|
390
|
+
// Co-Authored-By: someone-else"` and forge trailers in the commit log.
|
|
391
|
+
// Empty result falls back to the timestamped default.
|
|
392
|
+
const sanitized = message ? sanitizeCommitMessage(message) : "";
|
|
393
|
+
const commitMessage = sanitized || `akm save ${timestamp}`;
|
|
402
394
|
let repoDir;
|
|
403
395
|
let writable = false;
|
|
404
396
|
if (name) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const CLI = path.join(__dirname, "..", "src", "cli.ts");
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
const servers = [];
|
|
9
|
+
const CLI_TIMEOUT_MS = 30_000;
|
|
10
|
+
const TEST_TIMEOUT_MS = 60_000;
|
|
11
|
+
function makeTempDir(prefix) {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
13
|
+
tempDirs.push(dir);
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
function createWorkingStash() {
|
|
17
|
+
const dir = makeTempDir("akm-add-website-stash-");
|
|
18
|
+
for (const sub of ["skills", "commands", "agents", "knowledge", "scripts"]) {
|
|
19
|
+
fs.mkdirSync(path.join(dir, sub), { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
function serveWebsite() {
|
|
24
|
+
const server = Bun.serve({
|
|
25
|
+
port: 0,
|
|
26
|
+
fetch(request) {
|
|
27
|
+
const url = new URL(request.url);
|
|
28
|
+
if (url.pathname === "/") {
|
|
29
|
+
return new Response("<html><head><title>Example Docs</title></head><body><h1>Example Docs</h1><p>Welcome to the docs.</p><a href='/getting-started'>Getting started</a></body></html>", {
|
|
30
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (url.pathname === "/getting-started") {
|
|
34
|
+
return new Response("<html><body><h1>Getting started</h1><p>Run setup first.</p></body></html>", {
|
|
35
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return new Response("not found", { status: 404 });
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
servers.push(server);
|
|
42
|
+
return `http://127.0.0.1:${server.port}`;
|
|
43
|
+
}
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
for (const server of servers.splice(0)) {
|
|
46
|
+
server.stop(true);
|
|
47
|
+
}
|
|
48
|
+
for (const dir of tempDirs.splice(0)) {
|
|
49
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
for (const server of servers.splice(0)) {
|
|
54
|
+
server.stop(true);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
describe("akm add website", () => {
|
|
58
|
+
test("adds a website stash source, caches markdown, and indexes it", async () => {
|
|
59
|
+
const stashDir = createWorkingStash();
|
|
60
|
+
const xdgCache = makeTempDir("akm-add-website-cache-");
|
|
61
|
+
const xdgConfig = makeTempDir("akm-add-website-config-");
|
|
62
|
+
const websiteUrl = serveWebsite();
|
|
63
|
+
const configDir = path.join(xdgConfig, "akm");
|
|
64
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
65
|
+
fs.writeFileSync(path.join(configDir, "config.json"), `${JSON.stringify({ semanticSearchMode: "off" }, null, 2)}\n`);
|
|
66
|
+
const child = spawn("bun", [CLI, "add", websiteUrl, "--name", "docs-site", "--format=json"], {
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
68
|
+
env: {
|
|
69
|
+
...process.env,
|
|
70
|
+
AKM_STASH_DIR: stashDir,
|
|
71
|
+
XDG_CACHE_HOME: xdgCache,
|
|
72
|
+
XDG_CONFIG_HOME: xdgConfig,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
let stdout = "";
|
|
76
|
+
let stderr = "";
|
|
77
|
+
child.stdout.on("data", (chunk) => {
|
|
78
|
+
stdout += String(chunk);
|
|
79
|
+
});
|
|
80
|
+
child.stderr.on("data", (chunk) => {
|
|
81
|
+
stderr += String(chunk);
|
|
82
|
+
});
|
|
83
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
child.kill("SIGKILL");
|
|
86
|
+
reject(new Error("CLI website add timed out"));
|
|
87
|
+
}, CLI_TIMEOUT_MS);
|
|
88
|
+
child.on("error", (err) => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(err);
|
|
91
|
+
});
|
|
92
|
+
child.on("close", (code) => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve(code ?? 1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
expect(exitCode).toBe(0);
|
|
98
|
+
expect(stderr.trim()).toBe("");
|
|
99
|
+
const parsed = JSON.parse(stdout.trim());
|
|
100
|
+
const normalizedWebsiteUrl = `${websiteUrl}/`;
|
|
101
|
+
expect(parsed.sourceAdded).toBeDefined();
|
|
102
|
+
expect(parsed.sourceAdded?.type).toBe("website");
|
|
103
|
+
expect(parsed.sourceAdded?.url).toBe(normalizedWebsiteUrl);
|
|
104
|
+
expect(parsed.sourceAdded?.name).toBe("docs-site");
|
|
105
|
+
expect(parsed.index?.totalEntries).toBeGreaterThanOrEqual(2);
|
|
106
|
+
const configPath = path.join(xdgConfig, "akm", "config.json");
|
|
107
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
108
|
+
expect(config.sources).toContainEqual({
|
|
109
|
+
type: "website",
|
|
110
|
+
url: normalizedWebsiteUrl,
|
|
111
|
+
name: "docs-site",
|
|
112
|
+
});
|
|
113
|
+
expect(parsed.sourceAdded?.stashRoot).toBeDefined();
|
|
114
|
+
const knowledgeFiles = fs.readdirSync(path.join(parsed.sourceAdded?.stashRoot, "knowledge")).sort();
|
|
115
|
+
expect(knowledgeFiles).toEqual(["getting-started.md", "index.md"]);
|
|
116
|
+
const homeDoc = fs.readFileSync(path.join(parsed.sourceAdded?.stashRoot, "knowledge", "index.md"), "utf8");
|
|
117
|
+
expect(homeDoc).toContain("Example Docs");
|
|
118
|
+
}, { timeout: TEST_TIMEOUT_MS });
|
|
119
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: the AkmConfig loader propagates the `agent` block
|
|
3
|
+
* through `loadConfig()` (and through the on-disk JSONC parser, exercising
|
|
4
|
+
* the `pickKnownKeys` path).
|
|
5
|
+
*
|
|
6
|
+
* The acceptance criterion "config schema accepts an optional agent block"
|
|
7
|
+
* lives at the loader boundary, not just the parser; this test pins the
|
|
8
|
+
* end-to-end shape.
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
let tmpHome;
|
|
15
|
+
let originalHome;
|
|
16
|
+
let originalXdg;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "akm-agent-cfg-"));
|
|
19
|
+
originalHome = process.env.HOME;
|
|
20
|
+
originalXdg = process.env.XDG_CONFIG_HOME;
|
|
21
|
+
process.env.HOME = tmpHome;
|
|
22
|
+
process.env.XDG_CONFIG_HOME = path.join(tmpHome, ".config");
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (originalHome === undefined)
|
|
26
|
+
delete process.env.HOME;
|
|
27
|
+
else
|
|
28
|
+
process.env.HOME = originalHome;
|
|
29
|
+
if (originalXdg === undefined)
|
|
30
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
31
|
+
else
|
|
32
|
+
process.env.XDG_CONFIG_HOME = originalXdg;
|
|
33
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
describe("AkmConfig loader — agent block", () => {
|
|
36
|
+
test("loads agent.default + agent.profiles from disk", async () => {
|
|
37
|
+
const { getConfigPath, loadUserConfig, resetConfigCache } = await import("../../src/core/config");
|
|
38
|
+
const cfgPath = getConfigPath();
|
|
39
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
40
|
+
fs.writeFileSync(cfgPath, JSON.stringify({
|
|
41
|
+
semanticSearchMode: "auto",
|
|
42
|
+
agent: {
|
|
43
|
+
default: "claude",
|
|
44
|
+
timeoutMs: 45000,
|
|
45
|
+
profiles: {
|
|
46
|
+
claude: { args: ["--print"] },
|
|
47
|
+
rover: { bin: "rover-cli", parseOutput: "json" },
|
|
48
|
+
},
|
|
49
|
+
// Unknown key — must not throw at load.
|
|
50
|
+
mystery: 1,
|
|
51
|
+
},
|
|
52
|
+
}, null, 2));
|
|
53
|
+
resetConfigCache();
|
|
54
|
+
const cfg = loadUserConfig();
|
|
55
|
+
expect(cfg.agent?.default).toBe("claude");
|
|
56
|
+
expect(cfg.agent?.timeoutMs).toBe(45000);
|
|
57
|
+
expect(cfg.agent?.profiles?.claude?.args).toEqual(["--print"]);
|
|
58
|
+
expect(cfg.agent?.profiles?.rover?.bin).toBe("rover-cli");
|
|
59
|
+
expect(cfg.agent?.profiles?.rover?.parseOutput).toBe("json");
|
|
60
|
+
});
|
|
61
|
+
test("agent block absent → cfg.agent is undefined → requireAgentProfile throws", async () => {
|
|
62
|
+
const { loadUserConfig, resetConfigCache } = await import("../../src/core/config");
|
|
63
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
64
|
+
const { ConfigError } = await import("../../src/core/errors");
|
|
65
|
+
resetConfigCache();
|
|
66
|
+
const cfg = loadUserConfig();
|
|
67
|
+
expect(cfg.agent).toBeUndefined();
|
|
68
|
+
expect(() => requireAgentProfile(cfg.agent)).toThrow(ConfigError);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `agent.*` config block parser and profile resolver.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance coverage:
|
|
5
|
+
* • Parser accepts the documented shape.
|
|
6
|
+
* • Unknown keys are warn-and-ignored (no throw).
|
|
7
|
+
* • Built-in profiles resolve for opencode, claude, codex, gemini, aider.
|
|
8
|
+
* • Missing block surfaces a stable ConfigError via requireAgentProfile.
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
const warnings = [];
|
|
12
|
+
// NOTE: `mock.module` in Bun is process-global — once installed it persists
|
|
13
|
+
// across test files run in the same `bun test` invocation. So this mock has
|
|
14
|
+
// to remain a faithful drop-in for the real `src/core/warn` module:
|
|
15
|
+
//
|
|
16
|
+
// 1. Every export that the real module ships must be represented here,
|
|
17
|
+
// otherwise tests in other files that import a missing export get
|
|
18
|
+
// `undefined` and silently break (issue #273).
|
|
19
|
+
// 2. `warn()` must also forward to `console.warn` so other test files that
|
|
20
|
+
// capture stderr (e.g. the noise-gate tests in
|
|
21
|
+
// tests/workflows/indexer-rejection.test.ts) continue to see the calls.
|
|
22
|
+
// We push to the local `warnings[]` so this file's own assertions still
|
|
23
|
+
// work, AND forward to `console.warn` so callers that intercept it
|
|
24
|
+
// still observe what was emitted.
|
|
25
|
+
let mockedQuiet = false;
|
|
26
|
+
let mockedVerbose = false;
|
|
27
|
+
mock.module("../../src/core/warn", () => ({
|
|
28
|
+
warn: (...args) => {
|
|
29
|
+
warnings.push(args.join(" "));
|
|
30
|
+
if (!mockedQuiet)
|
|
31
|
+
console.warn(...args);
|
|
32
|
+
},
|
|
33
|
+
warnVerbose: (...args) => {
|
|
34
|
+
if (!mockedVerbose)
|
|
35
|
+
return;
|
|
36
|
+
warnings.push(args.join(" "));
|
|
37
|
+
if (!mockedQuiet)
|
|
38
|
+
console.warn(...args);
|
|
39
|
+
},
|
|
40
|
+
setQuiet: (value) => {
|
|
41
|
+
mockedQuiet = value;
|
|
42
|
+
},
|
|
43
|
+
resetQuiet: () => {
|
|
44
|
+
mockedQuiet = false;
|
|
45
|
+
},
|
|
46
|
+
isQuiet: () => mockedQuiet,
|
|
47
|
+
setVerbose: (value) => {
|
|
48
|
+
mockedVerbose = value;
|
|
49
|
+
},
|
|
50
|
+
resetVerbose: () => {
|
|
51
|
+
mockedVerbose = false;
|
|
52
|
+
},
|
|
53
|
+
isVerbose: () => {
|
|
54
|
+
const env = process.env.AKM_VERBOSE?.trim().toLowerCase();
|
|
55
|
+
if (env === "1" || env === "true" || env === "yes" || env === "on")
|
|
56
|
+
return true;
|
|
57
|
+
if (env === "0" || env === "false" || env === "no" || env === "off")
|
|
58
|
+
return false;
|
|
59
|
+
return mockedVerbose;
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
warnings.length = 0;
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
warnings.length = 0;
|
|
67
|
+
});
|
|
68
|
+
describe("parseAgentConfig", () => {
|
|
69
|
+
test("returns undefined when block is absent", async () => {
|
|
70
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
71
|
+
expect(parseAgentConfig(undefined)).toBeUndefined();
|
|
72
|
+
expect(warnings).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
test("warns and returns undefined for non-object root", async () => {
|
|
75
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
76
|
+
expect(parseAgentConfig("oops")).toBeUndefined();
|
|
77
|
+
expect(warnings.some((w) => w.includes('"agent"'))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
test("accepts the documented shape", async () => {
|
|
80
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
81
|
+
const parsed = parseAgentConfig({
|
|
82
|
+
default: "opencode",
|
|
83
|
+
timeoutMs: 30000,
|
|
84
|
+
profiles: {
|
|
85
|
+
opencode: { bin: "opencode", args: ["--non-interactive"], stdio: "captured" },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
expect(parsed?.default).toBe("opencode");
|
|
89
|
+
expect(parsed?.timeoutMs).toBe(30000);
|
|
90
|
+
expect(parsed?.profiles?.opencode).toEqual({
|
|
91
|
+
bin: "opencode",
|
|
92
|
+
args: ["--non-interactive"],
|
|
93
|
+
stdio: "captured",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
test("warn-and-ignore unknown top-level keys (no throw)", async () => {
|
|
97
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
98
|
+
const parsed = parseAgentConfig({
|
|
99
|
+
default: "claude",
|
|
100
|
+
moonRoutingTable: { foo: "bar" }, // unknown
|
|
101
|
+
});
|
|
102
|
+
expect(parsed?.default).toBe("claude");
|
|
103
|
+
expect(warnings.some((w) => w.includes("moonRoutingTable"))).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
test("warn-and-ignore unknown per-profile keys", async () => {
|
|
106
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
107
|
+
const parsed = parseAgentConfig({
|
|
108
|
+
profiles: {
|
|
109
|
+
custom: { bin: "ok", quirks: "nope" },
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
expect(parsed?.profiles?.custom?.bin).toBe("ok");
|
|
113
|
+
expect(warnings.some((w) => w.includes("quirks"))).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
test("warn-and-ignore malformed timeoutMs", async () => {
|
|
116
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
117
|
+
const parsed = parseAgentConfig({ timeoutMs: "60s" });
|
|
118
|
+
expect(parsed?.timeoutMs).toBeUndefined();
|
|
119
|
+
expect(warnings.some((w) => w.includes("timeoutMs"))).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
test("rejects non-string args entries", async () => {
|
|
122
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
123
|
+
const parsed = parseAgentConfig({
|
|
124
|
+
profiles: { opencode: { args: ["--ok", 5, "--also-ok"] } },
|
|
125
|
+
});
|
|
126
|
+
expect(parsed?.profiles?.opencode?.args).toEqual(["--ok", "--also-ok"]);
|
|
127
|
+
expect(warnings.some((w) => w.includes("args"))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
test("rejects bad stdio mode", async () => {
|
|
130
|
+
const { parseAgentConfig } = await import("../../src/integrations/agent/config");
|
|
131
|
+
const parsed = parseAgentConfig({
|
|
132
|
+
profiles: { opencode: { stdio: "weird" } },
|
|
133
|
+
});
|
|
134
|
+
expect(parsed?.profiles?.opencode?.stdio).toBeUndefined();
|
|
135
|
+
expect(warnings.some((w) => w.includes("stdio"))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe("built-in profile resolution", () => {
|
|
139
|
+
test("resolves opencode, claude, codex, gemini, aider out of the box", async () => {
|
|
140
|
+
const { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile } = await import("../../src/integrations/agent/profiles");
|
|
141
|
+
expect(BUILTIN_AGENT_PROFILE_NAMES).toEqual(["aider", "claude", "codex", "gemini", "opencode"]);
|
|
142
|
+
for (const name of ["opencode", "claude", "codex", "gemini", "aider"]) {
|
|
143
|
+
const profile = getBuiltinAgentProfile(name);
|
|
144
|
+
expect(profile).toBeDefined();
|
|
145
|
+
expect(profile?.bin).toBeTruthy();
|
|
146
|
+
expect(profile?.envPassthrough).toContain("PATH");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
test("user override merges on top of built-in", async () => {
|
|
150
|
+
const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
|
|
151
|
+
const merged = resolveAgentProfile("opencode", { args: ["--scripted"], stdio: "captured" });
|
|
152
|
+
expect(merged?.bin).toBe("opencode"); // built-in default
|
|
153
|
+
expect(merged?.args).toEqual(["--scripted"]); // override
|
|
154
|
+
expect(merged?.stdio).toBe("captured"); // override
|
|
155
|
+
expect(merged?.envPassthrough).toContain("PATH"); // built-in retained
|
|
156
|
+
});
|
|
157
|
+
test("user-defined profile (no built-in) requires bin", async () => {
|
|
158
|
+
const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
|
|
159
|
+
expect(resolveAgentProfile("rover", undefined)).toBeUndefined();
|
|
160
|
+
expect(resolveAgentProfile("rover", {})).toBeUndefined();
|
|
161
|
+
const ok = resolveAgentProfile("rover", { bin: "rover-cli", args: ["--silent"] });
|
|
162
|
+
expect(ok?.bin).toBe("rover-cli");
|
|
163
|
+
expect(ok?.args).toEqual(["--silent"]);
|
|
164
|
+
expect(ok?.stdio).toBe("captured");
|
|
165
|
+
});
|
|
166
|
+
test("envPassthrough merges base + override", async () => {
|
|
167
|
+
const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
|
|
168
|
+
const merged = resolveAgentProfile("opencode", { envPassthrough: ["MY_TOKEN"] });
|
|
169
|
+
expect(merged?.envPassthrough).toContain("PATH"); // from built-in
|
|
170
|
+
expect(merged?.envPassthrough).toContain("MY_TOKEN"); // from override
|
|
171
|
+
});
|
|
172
|
+
test("listAgentProfileNames includes built-ins plus user-defined", async () => {
|
|
173
|
+
const { listAgentProfileNames } = await import("../../src/integrations/agent/config");
|
|
174
|
+
const names = listAgentProfileNames({ profiles: { rover: { bin: "rover" } } });
|
|
175
|
+
expect(names).toContain("rover");
|
|
176
|
+
expect(names).toContain("opencode");
|
|
177
|
+
expect(names).toContain("claude");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe("requireAgentProfile", () => {
|
|
181
|
+
test("throws ConfigError when the agent block is missing", async () => {
|
|
182
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
183
|
+
const { ConfigError } = await import("../../src/core/errors");
|
|
184
|
+
let caught;
|
|
185
|
+
try {
|
|
186
|
+
requireAgentProfile(undefined);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
caught = err;
|
|
190
|
+
}
|
|
191
|
+
expect(caught).toBeInstanceOf(ConfigError);
|
|
192
|
+
expect(caught.message).toContain("agent commands are disabled");
|
|
193
|
+
const hint = caught.hint();
|
|
194
|
+
expect(hint).toBeTruthy();
|
|
195
|
+
expect(hint).toContain("akm setup");
|
|
196
|
+
});
|
|
197
|
+
test("throws when no default and no requested name", async () => {
|
|
198
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
199
|
+
const { ConfigError } = await import("../../src/core/errors");
|
|
200
|
+
let caught;
|
|
201
|
+
try {
|
|
202
|
+
requireAgentProfile({});
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
caught = err;
|
|
206
|
+
}
|
|
207
|
+
expect(caught).toBeInstanceOf(ConfigError);
|
|
208
|
+
expect(caught.message).toContain("require a profile");
|
|
209
|
+
});
|
|
210
|
+
test("resolves the requested profile when valid", async () => {
|
|
211
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
212
|
+
const profile = requireAgentProfile({ default: "claude" });
|
|
213
|
+
expect(profile.name).toBe("claude");
|
|
214
|
+
expect(profile.bin).toBe("claude");
|
|
215
|
+
});
|
|
216
|
+
test("explicit requested name beats config default", async () => {
|
|
217
|
+
const { requireAgentProfile } = await import("../../src/integrations/agent/config");
|
|
218
|
+
const profile = requireAgentProfile({ default: "claude" }, "codex");
|
|
219
|
+
expect(profile.name).toBe("codex");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for setup-time agent CLI detection.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance coverage:
|
|
5
|
+
* • Detects every built-in profile bin via the injected `which` probe.
|
|
6
|
+
* • Picks the first available profile as the default.
|
|
7
|
+
* • Honours an existing `agent.default` when that profile is still
|
|
8
|
+
* available (round-trip stability).
|
|
9
|
+
* • Returns `undefined` when nothing is installed.
|
|
10
|
+
* • `stepAgentCliDetection` produces a config-shaped result the wizard
|
|
11
|
+
* can `apply()`.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../../src/integrations/agent/detect";
|
|
15
|
+
function whichOnly(installed) {
|
|
16
|
+
const set = new Set(installed);
|
|
17
|
+
return (bin) => (set.has(bin) ? `/usr/local/bin/${bin}` : undefined);
|
|
18
|
+
}
|
|
19
|
+
describe("detectAgentCliProfiles", () => {
|
|
20
|
+
test("reports every built-in profile, available iff bin found", () => {
|
|
21
|
+
const results = detectAgentCliProfiles(undefined, whichOnly(["claude", "codex"]));
|
|
22
|
+
const names = results.map((r) => r.name).sort();
|
|
23
|
+
expect(names).toEqual(["aider", "claude", "codex", "gemini", "opencode"]);
|
|
24
|
+
expect(results.find((r) => r.name === "claude")?.available).toBe(true);
|
|
25
|
+
expect(results.find((r) => r.name === "codex")?.available).toBe(true);
|
|
26
|
+
expect(results.find((r) => r.name === "gemini")?.available).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
test("includes user-defined profiles via the resolver", () => {
|
|
29
|
+
const results = detectAgentCliProfiles({ profiles: { rover: { bin: "rover-cli" } } }, whichOnly(["rover-cli"]));
|
|
30
|
+
const rover = results.find((r) => r.name === "rover");
|
|
31
|
+
expect(rover?.available).toBe(true);
|
|
32
|
+
expect(rover?.resolvedPath).toContain("rover-cli");
|
|
33
|
+
});
|
|
34
|
+
test("returns nothing-installed when the probe always says no", () => {
|
|
35
|
+
const results = detectAgentCliProfiles(undefined, whichOnly([]));
|
|
36
|
+
expect(results.every((r) => !r.available)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("pickDefaultAgentProfile", () => {
|
|
40
|
+
test("picks the first available result when no existing default", () => {
|
|
41
|
+
const picked = pickDefaultAgentProfile([
|
|
42
|
+
{ name: "aider", bin: "aider", available: false },
|
|
43
|
+
{ name: "claude", bin: "claude", available: true },
|
|
44
|
+
{ name: "codex", bin: "codex", available: true },
|
|
45
|
+
]);
|
|
46
|
+
expect(picked).toBe("claude");
|
|
47
|
+
});
|
|
48
|
+
test("keeps an existing available default", () => {
|
|
49
|
+
const picked = pickDefaultAgentProfile([
|
|
50
|
+
{ name: "claude", bin: "claude", available: true },
|
|
51
|
+
{ name: "codex", bin: "codex", available: true },
|
|
52
|
+
], "codex");
|
|
53
|
+
expect(picked).toBe("codex");
|
|
54
|
+
});
|
|
55
|
+
test("falls back when the existing default is no longer available", () => {
|
|
56
|
+
const picked = pickDefaultAgentProfile([
|
|
57
|
+
{ name: "claude", bin: "claude", available: true },
|
|
58
|
+
{ name: "codex", bin: "codex", available: false },
|
|
59
|
+
], "codex");
|
|
60
|
+
expect(picked).toBe("claude");
|
|
61
|
+
});
|
|
62
|
+
test("returns undefined when nothing is available", () => {
|
|
63
|
+
const picked = pickDefaultAgentProfile([
|
|
64
|
+
{ name: "claude", bin: "claude", available: false },
|
|
65
|
+
{ name: "codex", bin: "codex", available: false },
|
|
66
|
+
]);
|
|
67
|
+
expect(picked).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("stepAgentCliDetection (setup wizard)", () => {
|
|
71
|
+
test("persists default + leaves block absent when nothing detected & no prior config", async () => {
|
|
72
|
+
const { stepAgentCliDetection } = await import("../../src/setup/setup");
|
|
73
|
+
const result = stepAgentCliDetection({ semanticSearchMode: "auto" }, () => [
|
|
74
|
+
{ name: "claude", bin: "claude", available: false },
|
|
75
|
+
{ name: "codex", bin: "codex", available: false },
|
|
76
|
+
]);
|
|
77
|
+
expect(result.agent).toBeUndefined();
|
|
78
|
+
expect(result.detections).toHaveLength(2);
|
|
79
|
+
});
|
|
80
|
+
test("writes agent.default to the first detected profile", async () => {
|
|
81
|
+
const { stepAgentCliDetection } = await import("../../src/setup/setup");
|
|
82
|
+
const result = stepAgentCliDetection({ semanticSearchMode: "auto" }, () => [
|
|
83
|
+
{ name: "claude", bin: "claude", available: false },
|
|
84
|
+
{ name: "codex", bin: "codex", available: true },
|
|
85
|
+
]);
|
|
86
|
+
expect(result.agent?.default).toBe("codex");
|
|
87
|
+
});
|
|
88
|
+
test("preserves user-overridden default when still available", async () => {
|
|
89
|
+
const { stepAgentCliDetection } = await import("../../src/setup/setup");
|
|
90
|
+
const result = stepAgentCliDetection({
|
|
91
|
+
semanticSearchMode: "auto",
|
|
92
|
+
agent: { default: "aider", profiles: { aider: { args: ["--no-auto-commits"] } } },
|
|
93
|
+
}, () => [
|
|
94
|
+
{ name: "claude", bin: "claude", available: true },
|
|
95
|
+
{ name: "aider", bin: "aider", available: true },
|
|
96
|
+
]);
|
|
97
|
+
expect(result.agent?.default).toBe("aider");
|
|
98
|
+
expect(result.agent?.profiles?.aider?.args).toEqual(["--no-auto-commits"]);
|
|
99
|
+
});
|
|
100
|
+
});
|