akm-cli 0.9.0-beta.5 → 0.9.0-beta.51
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 +711 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +6 -2
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +38 -10
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +27 -11
- package/dist/commands/agent/agent-dispatch.js +2 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +17 -4
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +33 -26
- package/dist/commands/env/secret-cli.js +36 -22
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/checks.js +49 -1
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +386 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +81 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +635 -660
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +159 -69
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +124 -2
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +389 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +138 -7
- package/dist/commands/improve/improve-cli.js +36 -61
- package/dist/commands/improve/improve-profiles.js +14 -0
- package/dist/commands/improve/improve-result-file.js +14 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2498
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1083 -0
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/preparation.js +1966 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +850 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +183 -40
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +43 -50
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +7 -2
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +189 -63
- package/dist/commands/read/curate.js +414 -94
- package/dist/commands/read/knowledge.js +6 -3
- package/dist/commands/read/search-cli.js +9 -4
- package/dist/commands/read/search.js +10 -6
- package/dist/commands/read/show.js +86 -7
- package/dist/commands/sources/init.js +49 -17
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +43 -45
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/stash-cli.js +28 -40
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +18 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +0 -5
- package/dist/core/config/config-migration.js +12 -11
- package/dist/core/config/config-schema.js +340 -56
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +28 -7
- package/dist/core/events.js +3 -7
- package/dist/core/improve-types.js +11 -8
- package/dist/core/logs-db.js +10 -66
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +714 -0
- package/dist/core/state-db.js +525 -474
- package/dist/indexer/db/db.js +439 -247
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +218 -4
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +123 -221
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +19 -6
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/search/search-source.js +17 -18
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/config.js +6 -53
- package/dist/integrations/agent/index.js +2 -18
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +45 -15
- package/dist/llm/embedder.js +42 -3
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +74 -2
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/text/helpers.js +16 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +22 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2617 -1961
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
- package/dist/setup/setup.js +29 -8
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/registry-cache.js +92 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +5 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +3 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +13 -11
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
package/dist/setup/setup.js
CHANGED
|
@@ -22,13 +22,14 @@ import { backupExistingConfig } from "../core/config/config-io.js";
|
|
|
22
22
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
23
23
|
import { assertSafeStashDir, getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths.js";
|
|
24
24
|
import { warn } from "../core/warn.js";
|
|
25
|
-
import { closeDatabase, isVecAvailable,
|
|
25
|
+
import { closeDatabase, isVecAvailable, openIndexDatabase } from "../indexer/db/db.js";
|
|
26
26
|
import { akmIndex } from "../indexer/indexer.js";
|
|
27
27
|
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/search/semantic-status.js";
|
|
28
28
|
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent/index.js";
|
|
29
29
|
import { defaultProfileName, v1ProfilePlatform } from "../integrations/harnesses/index.js";
|
|
30
30
|
import { probeLlmCapabilities } from "../llm/client.js";
|
|
31
31
|
import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder.js";
|
|
32
|
+
import { getOutputMode } from "../output/context.js";
|
|
32
33
|
import { getDirname, spawn } from "../runtime.js";
|
|
33
34
|
import { saveGitStash } from "../sources/providers/git.js";
|
|
34
35
|
import { backendNameForPlatform } from "../tasks/backends/index.js";
|
|
@@ -461,7 +462,7 @@ async function prepareSemanticSearchAssets(config) {
|
|
|
461
462
|
let probeDir;
|
|
462
463
|
try {
|
|
463
464
|
probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-setup-vec-probe-"));
|
|
464
|
-
db =
|
|
465
|
+
db = openIndexDatabase(path.join(probeDir, "probe.db"), config.embedding?.dimension ? { embeddingDim: config.embedding.dimension } : undefined);
|
|
465
466
|
if (isVecAvailable(db)) {
|
|
466
467
|
p.log.info("sqlite-vec is available for fast vector search.");
|
|
467
468
|
}
|
|
@@ -1791,7 +1792,7 @@ export async function runSetupWizard(opts) {
|
|
|
1791
1792
|
// Bootstrap directory structure before any prompts so the stash exists
|
|
1792
1793
|
// even if the wizard is interrupted after this point.
|
|
1793
1794
|
if (!opts?.noInit) {
|
|
1794
|
-
await akmInit({ dir: resolvedStashDir });
|
|
1795
|
+
await akmInit({ dir: resolvedStashDir, setDefault: true });
|
|
1795
1796
|
}
|
|
1796
1797
|
// Quick connectivity check — skip network-dependent steps when offline
|
|
1797
1798
|
const online = await isOnline();
|
|
@@ -1967,11 +1968,31 @@ export async function runSetupWizard(opts) {
|
|
|
1967
1968
|
*/
|
|
1968
1969
|
function backupAndAnnounce(configPath) {
|
|
1969
1970
|
const result = backupExistingConfig(configPath);
|
|
1970
|
-
|
|
1971
|
-
|
|
1971
|
+
const message = result ? `Config backed up to ${result.timestamped}` : "No existing config to back up.";
|
|
1972
|
+
// In JSON output mode the structured envelope (which already carries
|
|
1973
|
+
// `configPath`) MUST be the only thing on stdout — `setup --yes | jq` is a
|
|
1974
|
+
// supported scripting contract. @clack's `p.log.info` writes to stdout, which
|
|
1975
|
+
// would corrupt that envelope, so route this human-progress notice to stderr
|
|
1976
|
+
// when emitting JSON. Interactive/text runs keep the inline clack banner.
|
|
1977
|
+
if (isJsonOutputMode()) {
|
|
1978
|
+
process.stderr.write(`${message}\n`);
|
|
1972
1979
|
}
|
|
1973
1980
|
else {
|
|
1974
|
-
p.log.info(
|
|
1981
|
+
p.log.info(message);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* True when the process-level output mode is JSON (the default machine format).
|
|
1986
|
+
* Defensive: setup is also invoked programmatically (tests) where the output
|
|
1987
|
+
* mode singleton may not be initialized — treat that as "not JSON" so the
|
|
1988
|
+
* human-readable clack banner is used.
|
|
1989
|
+
*/
|
|
1990
|
+
function isJsonOutputMode() {
|
|
1991
|
+
try {
|
|
1992
|
+
return getOutputMode().format === "json";
|
|
1993
|
+
}
|
|
1994
|
+
catch {
|
|
1995
|
+
return false;
|
|
1975
1996
|
}
|
|
1976
1997
|
}
|
|
1977
1998
|
/**
|
|
@@ -1986,7 +2007,7 @@ export async function runSetupWithDefaults(opts) {
|
|
|
1986
2007
|
// Bootstrap directory structure first
|
|
1987
2008
|
let initResult;
|
|
1988
2009
|
if (!opts.noInit) {
|
|
1989
|
-
initResult = await akmInit({ dir: stashDir });
|
|
2010
|
+
initResult = await akmInit({ dir: stashDir, setDefault: true });
|
|
1990
2011
|
}
|
|
1991
2012
|
// Run steps in non-interactive mode (applies defaults, skips prompts)
|
|
1992
2013
|
const ctx = createSetupContext(current, { nonInteractive: true });
|
|
@@ -2283,7 +2304,7 @@ export async function runSetupFromConfig(opts) {
|
|
|
2283
2304
|
// Bootstrap directory structure
|
|
2284
2305
|
let initResult;
|
|
2285
2306
|
if (!opts.noInit) {
|
|
2286
|
-
initResult = await akmInit({ dir: stashDir });
|
|
2307
|
+
initResult = await akmInit({ dir: stashDir, setDefault: true });
|
|
2287
2308
|
}
|
|
2288
2309
|
// Optional probe
|
|
2289
2310
|
const mergedLlm = getDefaultLlmConfig(merged);
|
package/dist/sources/include.js
CHANGED
|
@@ -101,10 +101,14 @@ function copyDirectoryContents(sourceDir, destinationDir) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
function copyPath(sourcePath, destinationPath) {
|
|
104
|
-
const stat = fs.
|
|
104
|
+
const stat = fs.lstatSync(sourcePath);
|
|
105
|
+
if (stat.isSymbolicLink()) {
|
|
106
|
+
throw new Error(`Path in akm.include must not be a symlink: ${sourcePath}`);
|
|
107
|
+
}
|
|
105
108
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
106
109
|
if (stat.isDirectory()) {
|
|
107
|
-
fs.
|
|
110
|
+
fs.mkdirSync(destinationPath, { recursive: true });
|
|
111
|
+
copyDirectoryContents(sourcePath, destinationPath);
|
|
108
112
|
return;
|
|
109
113
|
}
|
|
110
114
|
fs.copyFileSync(sourcePath, destinationPath);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { UsageError } from "../../core/errors.js";
|
|
9
|
+
import { getRegistryCacheDir } from "../../core/paths.js";
|
|
10
|
+
import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../../registry/resolve.js";
|
|
11
|
+
import { applyAkmIncludeConfig, buildInstallCacheDir, copyDirectoryContents, detectStashRoot, isDirectory, } from "./provider-utils.js";
|
|
12
|
+
/**
|
|
13
|
+
* Shared subprocess wrapper for `git` invocations. Disables git's interactive
|
|
14
|
+
* terminal prompt so a missing credential never hangs the process.
|
|
15
|
+
*/
|
|
16
|
+
export function runGit(args, options) {
|
|
17
|
+
return spawnSync("git", args, {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
...options,
|
|
20
|
+
env: { ...process.env, ...options?.env, GIT_TERMINAL_PROMPT: "0" },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Sync mode for a one-shot install ref (`akm add github:owner/repo` or
|
|
25
|
+
* `akm add git:url`). Runs the clone → strip → include-filter pipeline that
|
|
26
|
+
* historically lived in `installRegistryRef()`.
|
|
27
|
+
*/
|
|
28
|
+
export async function syncRegistryGitRef(ref, options) {
|
|
29
|
+
const parsed = parseRegistryRef(ref);
|
|
30
|
+
if (parsed.source === "github") {
|
|
31
|
+
const githubRef = {
|
|
32
|
+
source: "git",
|
|
33
|
+
ref: parsed.ref,
|
|
34
|
+
id: parsed.id,
|
|
35
|
+
url: `https://github.com/${parsed.owner}/${parsed.repo}.git`,
|
|
36
|
+
requestedRef: parsed.requestedRef,
|
|
37
|
+
};
|
|
38
|
+
const result = await doSyncGit(githubRef, options);
|
|
39
|
+
return { ...result, source: "github" };
|
|
40
|
+
}
|
|
41
|
+
if (parsed.source !== "git") {
|
|
42
|
+
throw new UsageError(`syncRegistryGitRef requires a git: or github: ref, got "${ref}"`);
|
|
43
|
+
}
|
|
44
|
+
return doSyncGit(parsed, options);
|
|
45
|
+
}
|
|
46
|
+
async function doSyncGit(parsed, options) {
|
|
47
|
+
const resolved = await resolveRegistryArtifact(parsed);
|
|
48
|
+
const syncedAt = (options?.now ?? new Date()).toISOString();
|
|
49
|
+
const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheDir();
|
|
50
|
+
const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, resolved.resolvedRevision);
|
|
51
|
+
const cloneDir = path.join(cacheDir, "clone");
|
|
52
|
+
const extractedDir = path.join(cacheDir, "extracted");
|
|
53
|
+
// Cache hit
|
|
54
|
+
if (!options?.force && isDirectory(extractedDir)) {
|
|
55
|
+
try {
|
|
56
|
+
const provisionalKitRoot = detectStashRoot(extractedDir);
|
|
57
|
+
const installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
58
|
+
const stashRoot = detectStashRoot(installRoot);
|
|
59
|
+
if (stashRoot) {
|
|
60
|
+
return {
|
|
61
|
+
id: resolved.id,
|
|
62
|
+
source: resolved.source,
|
|
63
|
+
ref: resolved.ref,
|
|
64
|
+
artifactUrl: resolved.artifactUrl,
|
|
65
|
+
resolvedVersion: resolved.resolvedVersion,
|
|
66
|
+
resolvedRevision: resolved.resolvedRevision,
|
|
67
|
+
contentDir: stashRoot,
|
|
68
|
+
cacheDir,
|
|
69
|
+
extractedDir,
|
|
70
|
+
writable: options?.writable,
|
|
71
|
+
syncedAt,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Cache invalid, re-clone
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
80
|
+
// Validate URL and ref before passing to git to prevent command injection
|
|
81
|
+
validateGitUrl(parsed.url);
|
|
82
|
+
if (parsed.requestedRef)
|
|
83
|
+
validateGitRef(parsed.requestedRef);
|
|
84
|
+
let provisionalKitRoot;
|
|
85
|
+
let installRoot;
|
|
86
|
+
let stashRoot;
|
|
87
|
+
try {
|
|
88
|
+
const cloneArgs = ["clone", "--depth", "1"];
|
|
89
|
+
if (parsed.requestedRef) {
|
|
90
|
+
cloneArgs.push("--branch", parsed.requestedRef);
|
|
91
|
+
}
|
|
92
|
+
cloneArgs.push(parsed.url, cloneDir);
|
|
93
|
+
const cloneResult = runGit(cloneArgs, { timeout: 120_000 });
|
|
94
|
+
if (cloneResult.status !== 0) {
|
|
95
|
+
throw new Error(classifyCloneFailure(parsed.url, cloneResult.stderr, cloneResult.error));
|
|
96
|
+
}
|
|
97
|
+
// Copy contents to extracted dir without .git
|
|
98
|
+
fs.mkdirSync(extractedDir, { recursive: true });
|
|
99
|
+
copyDirectoryContents(cloneDir, extractedDir);
|
|
100
|
+
// Clean up the clone dir
|
|
101
|
+
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
102
|
+
provisionalKitRoot = detectStashRoot(extractedDir);
|
|
103
|
+
installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
104
|
+
stashRoot = detectStashRoot(installRoot);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// Clean up the cache directory so stale or partially-cloned artifacts
|
|
108
|
+
// don't cause false cache hits on the next install attempt.
|
|
109
|
+
try {
|
|
110
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* best-effort */
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
id: resolved.id,
|
|
119
|
+
source: resolved.source,
|
|
120
|
+
ref: resolved.ref,
|
|
121
|
+
artifactUrl: resolved.artifactUrl,
|
|
122
|
+
resolvedVersion: resolved.resolvedVersion,
|
|
123
|
+
resolvedRevision: resolved.resolvedRevision,
|
|
124
|
+
contentDir: stashRoot,
|
|
125
|
+
cacheDir,
|
|
126
|
+
extractedDir,
|
|
127
|
+
writable: options?.writable,
|
|
128
|
+
syncedAt,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function cloneRepo(cloneUrl, ref, destDir, writable = false) {
|
|
132
|
+
// Stage the clone into a sibling temp dir so that a failed clone never
|
|
133
|
+
// destroys a previously-valid destDir (e.g. when the remote is temporarily
|
|
134
|
+
// unreachable and we have a valid cached copy).
|
|
135
|
+
const tmpDir = `${destDir}.tmp-${randomBytes(4).toString("hex")}`;
|
|
136
|
+
const args = ["clone", "--depth", "1"];
|
|
137
|
+
if (ref)
|
|
138
|
+
args.push("--branch", ref);
|
|
139
|
+
args.push(cloneUrl, tmpDir);
|
|
140
|
+
const result = runGit(args, { timeout: 120_000 });
|
|
141
|
+
if (result.status !== 0) {
|
|
142
|
+
// Clean up the (possibly partial) temp dir but leave destDir untouched.
|
|
143
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
144
|
+
throw new Error(classifyCloneFailure(cloneUrl, result.stderr, result.error));
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
if (!writable) {
|
|
148
|
+
// Remove .git directory — we only need the working tree for read-only stashes
|
|
149
|
+
const gitDir = path.join(tmpDir, ".git");
|
|
150
|
+
if (fs.existsSync(gitDir))
|
|
151
|
+
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
// Swap: remove the old destDir (if any) then atomically rename tmpDir into place.
|
|
154
|
+
if (fs.existsSync(destDir))
|
|
155
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
156
|
+
fs.renameSync(tmpDir, destDir);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
// Post-clone steps failed — clean up the temp dir to avoid orphaned dirs.
|
|
160
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ── Clone-failure classification (#487) ─────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Translate git's stderr into an actionable message. Without this, a user
|
|
167
|
+
* who passes a nonexistent or private repo to `akm add` sees:
|
|
168
|
+
*
|
|
169
|
+
* "could not read Username for 'https://github.com': No such device or
|
|
170
|
+
* address"
|
|
171
|
+
*
|
|
172
|
+
* That is git falling through to its auth-prompt path — the actual cause
|
|
173
|
+
* is "repo doesn't exist (or is private)". We classify the common patterns
|
|
174
|
+
* and emit a message that names the cause and the fix.
|
|
175
|
+
*/
|
|
176
|
+
export function classifyCloneFailure(url, stderr, spawnError) {
|
|
177
|
+
const safeUrl = redactUrlUserinfo(url);
|
|
178
|
+
const raw = (stderr ?? "").trim();
|
|
179
|
+
const spawnMsg = spawnError?.message ?? "";
|
|
180
|
+
// `git` binary not on PATH.
|
|
181
|
+
if (spawnError?.code === "ENOENT") {
|
|
182
|
+
return `Failed to clone ${safeUrl}: 'git' is not installed or not on PATH. Install git, then re-run.`;
|
|
183
|
+
}
|
|
184
|
+
// Auth-prompt fall-through (the headline #487 case).
|
|
185
|
+
if (/could not read Username|terminal prompts disabled|Authentication failed|fatal: Authentication/i.test(raw)) {
|
|
186
|
+
return (`Failed to clone ${safeUrl}: repository not found or private. ` +
|
|
187
|
+
`If the repository is public, double-check the URL and try again. ` +
|
|
188
|
+
`If it is private, set GH_TOKEN (or configure a git credential helper) before re-running.`);
|
|
189
|
+
}
|
|
190
|
+
// 404-style messages from git http.
|
|
191
|
+
if (/repository '.*' not found|HTTP 404|fatal: remote error|not found:|Not Found/i.test(raw)) {
|
|
192
|
+
return (`Failed to clone ${safeUrl}: repository not found. ` +
|
|
193
|
+
`Check the URL — for GitHub, the form is 'owner/repo' or 'github:owner/repo'.`);
|
|
194
|
+
}
|
|
195
|
+
// SSH connection issues.
|
|
196
|
+
if (/Permission denied \(publickey\)|kex_exchange_identification|Connection refused|Connection timed out/i.test(raw)) {
|
|
197
|
+
return (`Failed to clone ${safeUrl}: network or SSH failure. ` +
|
|
198
|
+
`Check connectivity, your SSH agent, and the remote host's availability.`);
|
|
199
|
+
}
|
|
200
|
+
// Branch / ref-specific failures.
|
|
201
|
+
if (/Remote branch .* not found in upstream origin|couldn't find remote ref/i.test(raw)) {
|
|
202
|
+
return (`Failed to clone ${safeUrl}: the requested branch/tag does not exist on the remote. ` +
|
|
203
|
+
`Verify the ref name and re-run.`);
|
|
204
|
+
}
|
|
205
|
+
const detail = raw || spawnMsg || "unknown error";
|
|
206
|
+
return `Failed to clone ${safeUrl}: ${redactUrlUserinfo(detail)}`;
|
|
207
|
+
}
|
|
208
|
+
function redactUrlUserinfo(text) {
|
|
209
|
+
return text.replace(/\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^\s/@]+)@/g, "$1[REDACTED]@");
|
|
210
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
8
|
+
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
9
|
+
import { getRegistryIndexCacheDir } from "../../core/paths.js";
|
|
10
|
+
import { validateGitUrl } from "../../registry/resolve.js";
|
|
11
|
+
import { registerSourceProvider } from "../provider-factory.js";
|
|
12
|
+
import { cloneRepo, runGit, syncRegistryGitRef } from "./git-install.js";
|
|
13
|
+
import { isExpired, sanitizeString } from "./provider-utils.js";
|
|
14
|
+
/** Cache TTL before refreshing the mirrored repo (12 hours). */
|
|
15
|
+
const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
16
|
+
/** Maximum stale age allowed when refresh fails (7 days). */
|
|
17
|
+
const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
18
|
+
/**
|
|
19
|
+
* Git source provider — clones (and re-pulls) a remote repo into a local
|
|
20
|
+
* cache directory. Implements the v1 {@link SourceProvider} interface (spec
|
|
21
|
+
* §2.1, §2.5): `{ name, kind, init, path, sync }`.
|
|
22
|
+
*
|
|
23
|
+
* Reading is the indexer's job — this class doesn't implement `search` or
|
|
24
|
+
* `show`. The install-time helpers `syncRegistryGitRef` / `syncMirroredRepo`
|
|
25
|
+
* live below as standalone functions used by `akm add` / `akm update`.
|
|
26
|
+
*/
|
|
27
|
+
export class GitSourceProvider {
|
|
28
|
+
kind = "git";
|
|
29
|
+
name;
|
|
30
|
+
#config;
|
|
31
|
+
#path = null;
|
|
32
|
+
constructor(config) {
|
|
33
|
+
this.#config = config;
|
|
34
|
+
this.name = config.name ?? "git";
|
|
35
|
+
}
|
|
36
|
+
path() {
|
|
37
|
+
if (this.#path == null) {
|
|
38
|
+
// Lazy resolution: providers are sometimes constructed without an
|
|
39
|
+
// explicit init() call (e.g. by legacy callers that just want the
|
|
40
|
+
// path). Resolve on demand and cache.
|
|
41
|
+
this.#path = resolveGitContentDir(this.#config);
|
|
42
|
+
}
|
|
43
|
+
return this.#path;
|
|
44
|
+
}
|
|
45
|
+
async sync(options) {
|
|
46
|
+
// Two execution modes:
|
|
47
|
+
// 1. Long-lived configured source (config.url) — mirror into the
|
|
48
|
+
// registry-index cache and serve as a read-only working tree.
|
|
49
|
+
// 2. One-shot install ref (options.ref like "git:..." / "github:...") —
|
|
50
|
+
// delegate to the install-time pipeline.
|
|
51
|
+
if (typeof this.#config.options?.ref === "string" && this.#config.options.ref) {
|
|
52
|
+
await syncRegistryGitRef(String(this.#config.options.ref), { force: options?.force });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await syncMirroredRepo(this.#config, { force: options?.force });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Resolve the on-disk content directory for a configured git source. */
|
|
59
|
+
function resolveGitContentDir(config) {
|
|
60
|
+
if (config.path)
|
|
61
|
+
return config.path;
|
|
62
|
+
if (config.url) {
|
|
63
|
+
const repo = parseGitRepoUrl(config.url);
|
|
64
|
+
return getCachePaths(repo.canonicalUrl).repoDir;
|
|
65
|
+
}
|
|
66
|
+
throw new ConfigError("git source entry must have either `path` or `url`");
|
|
67
|
+
}
|
|
68
|
+
// ── Self-register ───────────────────────────────────────────────────────────
|
|
69
|
+
registerSourceProvider("git", (config) => new GitSourceProvider(config));
|
|
70
|
+
// ── Cache management ────────────────────────────────────────────────────────
|
|
71
|
+
export function getCachePaths(repoUrl) {
|
|
72
|
+
const key = createHash("sha256").update(repoUrl).digest("hex").slice(0, 16);
|
|
73
|
+
const cacheRoot = getRegistryIndexCacheDir();
|
|
74
|
+
const rootDir = path.join(cacheRoot, `git-${key}`);
|
|
75
|
+
return {
|
|
76
|
+
rootDir,
|
|
77
|
+
repoDir: path.join(rootDir, "repo"),
|
|
78
|
+
indexPath: path.join(rootDir, "index.json"),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function ensureGitMirror(repo, cachePaths, options) {
|
|
82
|
+
const requireRepoDir = options?.requireRepoDir === true;
|
|
83
|
+
const writable = options?.writable === true;
|
|
84
|
+
const force = options?.force === true;
|
|
85
|
+
// Check if cache is fresh
|
|
86
|
+
let mtime = 0;
|
|
87
|
+
try {
|
|
88
|
+
mtime = fs.statSync(cachePaths.indexPath).mtimeMs;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
/* no cached index */
|
|
92
|
+
}
|
|
93
|
+
if (!force && mtime && !isExpired(mtime, CACHE_TTL_MS) && (!requireRepoDir || hasExtractedRepo(cachePaths.repoDir))) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
fs.mkdirSync(cachePaths.rootDir, { recursive: true });
|
|
98
|
+
if (writable && fs.existsSync(path.join(cachePaths.repoDir, ".git"))) {
|
|
99
|
+
// Writable repo already cloned — pull instead of re-clone to preserve local changes
|
|
100
|
+
pullRepo(cachePaths.repoDir);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
cloneRepo(repo.cloneUrl, repo.ref, cachePaths.repoDir, writable);
|
|
104
|
+
}
|
|
105
|
+
// Touch index file to track freshness
|
|
106
|
+
fs.writeFileSync(cachePaths.indexPath, "[]", { encoding: "utf8", mode: 0o600 });
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (mtime && !isExpired(mtime, CACHE_STALE_MS) && (!requireRepoDir || hasExtractedRepo(cachePaths.repoDir))) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Sync mode for a long-lived configured git stash. Mirrors the repo into the
|
|
117
|
+
* shared registry-index cache (12h TTL) and exposes the working tree as the
|
|
118
|
+
* stash content directory.
|
|
119
|
+
*/
|
|
120
|
+
export async function syncMirroredRepo(config, options) {
|
|
121
|
+
if (!config.url) {
|
|
122
|
+
throw new ConfigError("git stash entry requires a URL when no install ref is supplied");
|
|
123
|
+
}
|
|
124
|
+
const repo = parseGitRepoUrl(config.url);
|
|
125
|
+
const cachePaths = getCachePaths(repo.canonicalUrl);
|
|
126
|
+
await ensureGitMirror(repo, cachePaths, {
|
|
127
|
+
requireRepoDir: true,
|
|
128
|
+
writable: options?.writable ?? config.writable === true,
|
|
129
|
+
force: options?.force,
|
|
130
|
+
});
|
|
131
|
+
const syncedAt = (options?.now ?? new Date()).toISOString();
|
|
132
|
+
const contentDir = cachePaths.repoDir;
|
|
133
|
+
return {
|
|
134
|
+
id: repo.canonicalUrl,
|
|
135
|
+
source: "git",
|
|
136
|
+
ref: repo.canonicalUrl,
|
|
137
|
+
artifactUrl: repo.canonicalUrl,
|
|
138
|
+
contentDir,
|
|
139
|
+
cacheDir: cachePaths.rootDir,
|
|
140
|
+
extractedDir: contentDir,
|
|
141
|
+
writable: options?.writable ?? config.writable === true,
|
|
142
|
+
syncedAt,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function pullRepo(repoDir) {
|
|
146
|
+
const result = runGit(["-C", repoDir, "pull", "--ff-only"], {
|
|
147
|
+
timeout: 120_000,
|
|
148
|
+
});
|
|
149
|
+
if (result.status !== 0) {
|
|
150
|
+
const err = result.stderr?.trim() || result.error?.message || "unknown error";
|
|
151
|
+
throw new Error(`Failed to pull ${repoDir}: ${err}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function hasExtractedRepo(repoDir) {
|
|
155
|
+
try {
|
|
156
|
+
if (!fs.statSync(repoDir).isDirectory())
|
|
157
|
+
return false;
|
|
158
|
+
if (fs.statSync(path.join(repoDir, "content")).isDirectory())
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
/* fall through to root-layout detection */
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
if (!fs.statSync(repoDir).isDirectory())
|
|
166
|
+
return false;
|
|
167
|
+
return Object.values(TYPE_DIRS).some((dirName) => fs.existsSync(path.join(repoDir, dirName)));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export function parseGitRepoUrl(rawUrl) {
|
|
174
|
+
if (!rawUrl) {
|
|
175
|
+
throw new ConfigError("Git provider requires a repository URL");
|
|
176
|
+
}
|
|
177
|
+
// SSH shorthand: git@host:path — valid as-is, delegated to system git credentials
|
|
178
|
+
if (/^git@[^:]+:.+$/.test(rawUrl)) {
|
|
179
|
+
return { cloneUrl: rawUrl, ref: null, canonicalUrl: rawUrl };
|
|
180
|
+
}
|
|
181
|
+
// Validate URL scheme is safe before parsing
|
|
182
|
+
try {
|
|
183
|
+
validateGitUrl(rawUrl);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
if (err instanceof UsageError)
|
|
187
|
+
throw new ConfigError(err.message);
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
let parsed;
|
|
191
|
+
try {
|
|
192
|
+
parsed = new URL(rawUrl);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
throw new ConfigError(`Git provider URL is not valid: "${rawUrl}"`);
|
|
196
|
+
}
|
|
197
|
+
// GitHub web URLs: extract a clean clone URL and optional branch from /tree/<ref>
|
|
198
|
+
if (parsed.hostname === "github.com" && parsed.protocol === "https:") {
|
|
199
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
200
|
+
if (segments.length < 2) {
|
|
201
|
+
throw new ConfigError(`Git provider URL must point to a repository, got "${rawUrl}"`);
|
|
202
|
+
}
|
|
203
|
+
const owner = sanitizeString(segments[0]);
|
|
204
|
+
const repo = sanitizeString(segments[1].replace(/\.git$/i, ""));
|
|
205
|
+
if (!owner || !repo || !/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo)) {
|
|
206
|
+
throw new ConfigError(`Unsupported repository URL: "${rawUrl}"`);
|
|
207
|
+
}
|
|
208
|
+
let ref = null;
|
|
209
|
+
if (segments[2] === "tree" && segments.length >= 4) {
|
|
210
|
+
const rawRef = sanitizeString(segments.slice(3).join("/"), 255);
|
|
211
|
+
if (rawRef && !rawRef.includes("..") && /^[A-Za-z0-9._/-]+$/.test(rawRef)) {
|
|
212
|
+
ref = rawRef;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const cloneUrl = `https://github.com/${owner}/${repo}`;
|
|
216
|
+
const canonicalUrl = ref ? `${cloneUrl}/tree/${ref}` : cloneUrl;
|
|
217
|
+
return { cloneUrl, ref, canonicalUrl };
|
|
218
|
+
}
|
|
219
|
+
// Any other valid git URL: use as-is for cloning, but strip embedded credentials
|
|
220
|
+
// from canonicalUrl so secrets don't leak into cache keys or warning messages.
|
|
221
|
+
let canonicalUrl = rawUrl;
|
|
222
|
+
try {
|
|
223
|
+
const u = new URL(rawUrl);
|
|
224
|
+
u.username = "";
|
|
225
|
+
u.password = "";
|
|
226
|
+
u.search = "";
|
|
227
|
+
u.hash = "";
|
|
228
|
+
canonicalUrl = u.toString();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// URL failed to parse — fall back to raw (validateGitUrl already accepted it)
|
|
232
|
+
}
|
|
233
|
+
return { cloneUrl: rawUrl, ref: null, canonicalUrl };
|
|
234
|
+
}
|