akm-cli 0.9.0-beta.9 → 0.9.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +592 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- 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/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- 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 +11 -3
- 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 +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -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 +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -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/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +382 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- 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 +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +259 -769
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- 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 +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- 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 +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2661 -2369
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- 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/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/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- 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 +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -1,664 +1,13 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../../registry/resolve.js";
|
|
15
|
-
import { registerSourceProvider } from "../provider-factory.js";
|
|
16
|
-
import { applyAkmIncludeConfig, buildInstallCacheDir, copyDirectoryContents, detectStashRoot, isDirectory, isExpired, sanitizeString, } from "./provider-utils.js";
|
|
17
|
-
/** Cache TTL before refreshing the mirrored repo (12 hours). */
|
|
18
|
-
const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
19
|
-
/** Maximum stale age allowed when refresh fails (7 days). */
|
|
20
|
-
const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
21
|
-
function runGit(args, options) {
|
|
22
|
-
return spawnSync("git", args, {
|
|
23
|
-
encoding: "utf8",
|
|
24
|
-
...options,
|
|
25
|
-
env: { ...process.env, ...options?.env, GIT_TERMINAL_PROMPT: "0" },
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Git source provider — clones (and re-pulls) a remote repo into a local
|
|
30
|
-
* cache directory. Implements the v1 {@link SourceProvider} interface (spec
|
|
31
|
-
* §2.1, §2.5): `{ name, kind, init, path, sync }`.
|
|
32
|
-
*
|
|
33
|
-
* Reading is the indexer's job — this class doesn't implement `search` or
|
|
34
|
-
* `show`. The install-time helpers `syncRegistryGitRef` / `syncMirroredRepo`
|
|
35
|
-
* live below as standalone functions used by `akm add` / `akm update`.
|
|
36
|
-
*/
|
|
37
|
-
class GitSourceProvider {
|
|
38
|
-
kind = "git";
|
|
39
|
-
name;
|
|
40
|
-
#config;
|
|
41
|
-
#path = null;
|
|
42
|
-
constructor(config) {
|
|
43
|
-
this.#config = config;
|
|
44
|
-
this.name = config.name ?? "git";
|
|
45
|
-
}
|
|
46
|
-
async init(_ctx) {
|
|
47
|
-
// Resolve the on-disk content directory once. For configured git sources
|
|
48
|
-
// this is the cached working tree; for one-shot install refs it's the
|
|
49
|
-
// path the install pipeline materialised.
|
|
50
|
-
this.#path = resolveGitContentDir(this.#config);
|
|
51
|
-
}
|
|
52
|
-
path() {
|
|
53
|
-
if (this.#path == null) {
|
|
54
|
-
// Lazy resolution: providers are sometimes constructed without an
|
|
55
|
-
// explicit init() call (e.g. by legacy callers that just want the
|
|
56
|
-
// path). Resolve on demand and cache.
|
|
57
|
-
this.#path = resolveGitContentDir(this.#config);
|
|
58
|
-
}
|
|
59
|
-
return this.#path;
|
|
60
|
-
}
|
|
61
|
-
async sync() {
|
|
62
|
-
// Two execution modes:
|
|
63
|
-
// 1. Long-lived configured source (config.url) — mirror into the
|
|
64
|
-
// registry-index cache and serve as a read-only working tree.
|
|
65
|
-
// 2. One-shot install ref (options.ref like "git:..." / "github:...") —
|
|
66
|
-
// delegate to the install-time pipeline.
|
|
67
|
-
if (typeof this.#config.options?.ref === "string" && this.#config.options.ref) {
|
|
68
|
-
await syncRegistryGitRef(String(this.#config.options.ref));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
await syncMirroredRepo(this.#config);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/** Resolve the on-disk content directory for a configured git source. */
|
|
75
|
-
function resolveGitContentDir(config) {
|
|
76
|
-
if (config.path)
|
|
77
|
-
return config.path;
|
|
78
|
-
if (config.url) {
|
|
79
|
-
const repo = parseGitRepoUrl(config.url);
|
|
80
|
-
return getCachePaths(repo.canonicalUrl).repoDir;
|
|
81
|
-
}
|
|
82
|
-
throw new ConfigError("git source entry must have either `path` or `url`");
|
|
83
|
-
}
|
|
84
|
-
// ── Self-register ───────────────────────────────────────────────────────────
|
|
85
|
-
registerSourceProvider("git", (config) => new GitSourceProvider(config));
|
|
86
|
-
// ── Cache management ────────────────────────────────────────────────────────
|
|
87
|
-
function getCachePaths(repoUrl) {
|
|
88
|
-
const key = createHash("sha256").update(repoUrl).digest("hex").slice(0, 16);
|
|
89
|
-
const cacheRoot = getRegistryIndexCacheDir();
|
|
90
|
-
const rootDir = path.join(cacheRoot, `git-${key}`);
|
|
91
|
-
return {
|
|
92
|
-
rootDir,
|
|
93
|
-
repoDir: path.join(rootDir, "repo"),
|
|
94
|
-
indexPath: path.join(rootDir, "index.json"),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
async function ensureGitMirror(repo, cachePaths, options) {
|
|
98
|
-
const requireRepoDir = options?.requireRepoDir === true;
|
|
99
|
-
const writable = options?.writable === true;
|
|
100
|
-
const force = options?.force === true;
|
|
101
|
-
// Check if cache is fresh
|
|
102
|
-
let mtime = 0;
|
|
103
|
-
try {
|
|
104
|
-
mtime = fs.statSync(cachePaths.indexPath).mtimeMs;
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
/* no cached index */
|
|
108
|
-
}
|
|
109
|
-
if (!force && mtime && !isExpired(mtime, CACHE_TTL_MS) && (!requireRepoDir || hasExtractedRepo(cachePaths.repoDir))) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
fs.mkdirSync(cachePaths.rootDir, { recursive: true });
|
|
114
|
-
if (writable && fs.existsSync(path.join(cachePaths.repoDir, ".git"))) {
|
|
115
|
-
// Writable repo already cloned — pull instead of re-clone to preserve local changes
|
|
116
|
-
pullRepo(cachePaths.repoDir);
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
cloneRepo(repo.cloneUrl, repo.ref, cachePaths.repoDir, writable);
|
|
120
|
-
}
|
|
121
|
-
// Touch index file to track freshness
|
|
122
|
-
fs.writeFileSync(cachePaths.indexPath, "[]", { encoding: "utf8", mode: 0o600 });
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
if (mtime && !isExpired(mtime, CACHE_STALE_MS) && (!requireRepoDir || hasExtractedRepo(cachePaths.repoDir))) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
throw err;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Sync mode for a long-lived configured git stash. Mirrors the repo into the
|
|
133
|
-
* shared registry-index cache (12h TTL) and exposes the working tree as the
|
|
134
|
-
* stash content directory.
|
|
135
|
-
*/
|
|
136
|
-
export async function syncMirroredRepo(config, options) {
|
|
137
|
-
if (!config.url) {
|
|
138
|
-
throw new ConfigError("git stash entry requires a URL when no install ref is supplied");
|
|
139
|
-
}
|
|
140
|
-
const repo = parseGitRepoUrl(config.url);
|
|
141
|
-
const cachePaths = getCachePaths(repo.canonicalUrl);
|
|
142
|
-
await ensureGitMirror(repo, cachePaths, {
|
|
143
|
-
requireRepoDir: true,
|
|
144
|
-
writable: options?.writable ?? config.writable === true,
|
|
145
|
-
force: options?.force,
|
|
146
|
-
});
|
|
147
|
-
const syncedAt = (options?.now ?? new Date()).toISOString();
|
|
148
|
-
const contentDir = cachePaths.repoDir;
|
|
149
|
-
return {
|
|
150
|
-
id: repo.canonicalUrl,
|
|
151
|
-
source: "git",
|
|
152
|
-
ref: repo.canonicalUrl,
|
|
153
|
-
artifactUrl: repo.canonicalUrl,
|
|
154
|
-
contentDir,
|
|
155
|
-
cacheDir: cachePaths.rootDir,
|
|
156
|
-
extractedDir: contentDir,
|
|
157
|
-
writable: options?.writable ?? config.writable === true,
|
|
158
|
-
syncedAt,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Sync mode for a one-shot install ref (`akm add github:owner/repo` or
|
|
163
|
-
* `akm add git:url`). Runs the clone → strip → include-filter pipeline that
|
|
164
|
-
* historically lived in `installRegistryRef()`.
|
|
165
|
-
*/
|
|
166
|
-
export async function syncRegistryGitRef(ref, options) {
|
|
167
|
-
const parsed = parseRegistryRef(ref);
|
|
168
|
-
if (parsed.source === "github") {
|
|
169
|
-
const githubRef = {
|
|
170
|
-
source: "git",
|
|
171
|
-
ref: parsed.ref,
|
|
172
|
-
id: parsed.id,
|
|
173
|
-
url: `https://github.com/${parsed.owner}/${parsed.repo}.git`,
|
|
174
|
-
requestedRef: parsed.requestedRef,
|
|
175
|
-
};
|
|
176
|
-
const result = await doSyncGit(githubRef, options);
|
|
177
|
-
return { ...result, source: "github" };
|
|
178
|
-
}
|
|
179
|
-
if (parsed.source !== "git") {
|
|
180
|
-
throw new UsageError(`syncRegistryGitRef requires a git: or github: ref, got "${ref}"`);
|
|
181
|
-
}
|
|
182
|
-
return doSyncGit(parsed, options);
|
|
183
|
-
}
|
|
184
|
-
async function doSyncGit(parsed, options) {
|
|
185
|
-
const resolved = await resolveRegistryArtifact(parsed);
|
|
186
|
-
const syncedAt = (options?.now ?? new Date()).toISOString();
|
|
187
|
-
const cacheRootDir = options?.cacheRootDir ?? getRegistryCacheDir();
|
|
188
|
-
const cacheDir = buildInstallCacheDir(cacheRootDir, parsed.source, parsed.id, resolved.resolvedRevision);
|
|
189
|
-
const cloneDir = path.join(cacheDir, "clone");
|
|
190
|
-
const extractedDir = path.join(cacheDir, "extracted");
|
|
191
|
-
// Cache hit
|
|
192
|
-
if (!options?.force && isDirectory(extractedDir)) {
|
|
193
|
-
try {
|
|
194
|
-
const provisionalKitRoot = detectStashRoot(extractedDir);
|
|
195
|
-
const installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
196
|
-
const stashRoot = detectStashRoot(installRoot);
|
|
197
|
-
if (stashRoot) {
|
|
198
|
-
return {
|
|
199
|
-
id: resolved.id,
|
|
200
|
-
source: resolved.source,
|
|
201
|
-
ref: resolved.ref,
|
|
202
|
-
artifactUrl: resolved.artifactUrl,
|
|
203
|
-
resolvedVersion: resolved.resolvedVersion,
|
|
204
|
-
resolvedRevision: resolved.resolvedRevision,
|
|
205
|
-
contentDir: stashRoot,
|
|
206
|
-
cacheDir,
|
|
207
|
-
extractedDir,
|
|
208
|
-
writable: options?.writable,
|
|
209
|
-
syncedAt,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
// Cache invalid, re-clone
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
218
|
-
// Validate URL and ref before passing to git to prevent command injection
|
|
219
|
-
validateGitUrl(parsed.url);
|
|
220
|
-
if (parsed.requestedRef)
|
|
221
|
-
validateGitRef(parsed.requestedRef);
|
|
222
|
-
let provisionalKitRoot;
|
|
223
|
-
let installRoot;
|
|
224
|
-
let stashRoot;
|
|
225
|
-
try {
|
|
226
|
-
const cloneArgs = ["clone", "--depth", "1"];
|
|
227
|
-
if (parsed.requestedRef) {
|
|
228
|
-
cloneArgs.push("--branch", parsed.requestedRef);
|
|
229
|
-
}
|
|
230
|
-
cloneArgs.push(parsed.url, cloneDir);
|
|
231
|
-
const cloneResult = runGit(cloneArgs, { timeout: 120_000 });
|
|
232
|
-
if (cloneResult.status !== 0) {
|
|
233
|
-
throw new Error(classifyCloneFailure(parsed.url, cloneResult.stderr, cloneResult.error));
|
|
234
|
-
}
|
|
235
|
-
// Copy contents to extracted dir without .git
|
|
236
|
-
fs.mkdirSync(extractedDir, { recursive: true });
|
|
237
|
-
copyDirectoryContents(cloneDir, extractedDir);
|
|
238
|
-
// Clean up the clone dir
|
|
239
|
-
fs.rmSync(cloneDir, { recursive: true, force: true });
|
|
240
|
-
provisionalKitRoot = detectStashRoot(extractedDir);
|
|
241
|
-
installRoot = applyAkmIncludeConfig(provisionalKitRoot, cacheDir, extractedDir) ?? provisionalKitRoot;
|
|
242
|
-
stashRoot = detectStashRoot(installRoot);
|
|
243
|
-
}
|
|
244
|
-
catch (err) {
|
|
245
|
-
// Clean up the cache directory so stale or partially-cloned artifacts
|
|
246
|
-
// don't cause false cache hits on the next install attempt.
|
|
247
|
-
try {
|
|
248
|
-
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
249
|
-
}
|
|
250
|
-
catch {
|
|
251
|
-
/* best-effort */
|
|
252
|
-
}
|
|
253
|
-
throw err;
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
id: resolved.id,
|
|
257
|
-
source: resolved.source,
|
|
258
|
-
ref: resolved.ref,
|
|
259
|
-
artifactUrl: resolved.artifactUrl,
|
|
260
|
-
resolvedVersion: resolved.resolvedVersion,
|
|
261
|
-
resolvedRevision: resolved.resolvedRevision,
|
|
262
|
-
contentDir: stashRoot,
|
|
263
|
-
cacheDir,
|
|
264
|
-
extractedDir,
|
|
265
|
-
writable: options?.writable,
|
|
266
|
-
syncedAt,
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
export function cloneRepo(cloneUrl, ref, destDir, writable = false) {
|
|
270
|
-
// Stage the clone into a sibling temp dir so that a failed clone never
|
|
271
|
-
// destroys a previously-valid destDir (e.g. when the remote is temporarily
|
|
272
|
-
// unreachable and we have a valid cached copy).
|
|
273
|
-
const tmpDir = `${destDir}.tmp-${randomBytes(4).toString("hex")}`;
|
|
274
|
-
const args = ["clone", "--depth", "1"];
|
|
275
|
-
if (ref)
|
|
276
|
-
args.push("--branch", ref);
|
|
277
|
-
args.push(cloneUrl, tmpDir);
|
|
278
|
-
const result = runGit(args, { timeout: 120_000 });
|
|
279
|
-
if (result.status !== 0) {
|
|
280
|
-
// Clean up the (possibly partial) temp dir but leave destDir untouched.
|
|
281
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
282
|
-
throw new Error(classifyCloneFailure(cloneUrl, result.stderr, result.error));
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
if (!writable) {
|
|
286
|
-
// Remove .git directory — we only need the working tree for read-only stashes
|
|
287
|
-
const gitDir = path.join(tmpDir, ".git");
|
|
288
|
-
if (fs.existsSync(gitDir))
|
|
289
|
-
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
290
|
-
}
|
|
291
|
-
// Swap: remove the old destDir (if any) then atomically rename tmpDir into place.
|
|
292
|
-
if (fs.existsSync(destDir))
|
|
293
|
-
fs.rmSync(destDir, { recursive: true, force: true });
|
|
294
|
-
fs.renameSync(tmpDir, destDir);
|
|
295
|
-
}
|
|
296
|
-
catch (err) {
|
|
297
|
-
// Post-clone steps failed — clean up the temp dir to avoid orphaned dirs.
|
|
298
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
299
|
-
throw err;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
function pullRepo(repoDir) {
|
|
303
|
-
const result = runGit(["-C", repoDir, "pull", "--ff-only"], {
|
|
304
|
-
timeout: 120_000,
|
|
305
|
-
});
|
|
306
|
-
if (result.status !== 0) {
|
|
307
|
-
const err = result.stderr?.trim() || result.error?.message || "unknown error";
|
|
308
|
-
throw new Error(`Failed to pull ${repoDir}: ${err}`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
function hasExtractedRepo(repoDir) {
|
|
312
|
-
try {
|
|
313
|
-
if (!fs.statSync(repoDir).isDirectory())
|
|
314
|
-
return false;
|
|
315
|
-
if (fs.statSync(path.join(repoDir, "content")).isDirectory())
|
|
316
|
-
return true;
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
/* fall through to root-layout detection */
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
if (!fs.statSync(repoDir).isDirectory())
|
|
323
|
-
return false;
|
|
324
|
-
return Object.values(TYPE_DIRS).some((dirName) => fs.existsSync(path.join(repoDir, dirName)));
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
function parseGitRepoUrl(rawUrl) {
|
|
331
|
-
if (!rawUrl) {
|
|
332
|
-
throw new ConfigError("Git provider requires a repository URL");
|
|
333
|
-
}
|
|
334
|
-
// SSH shorthand: git@host:path — valid as-is, delegated to system git credentials
|
|
335
|
-
if (/^git@[^:]+:.+$/.test(rawUrl)) {
|
|
336
|
-
return { cloneUrl: rawUrl, ref: null, canonicalUrl: rawUrl };
|
|
337
|
-
}
|
|
338
|
-
// Validate URL scheme is safe before parsing
|
|
339
|
-
try {
|
|
340
|
-
validateGitUrl(rawUrl);
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
if (err instanceof UsageError)
|
|
344
|
-
throw new ConfigError(err.message);
|
|
345
|
-
throw err;
|
|
346
|
-
}
|
|
347
|
-
let parsed;
|
|
348
|
-
try {
|
|
349
|
-
parsed = new URL(rawUrl);
|
|
350
|
-
}
|
|
351
|
-
catch {
|
|
352
|
-
throw new ConfigError(`Git provider URL is not valid: "${rawUrl}"`);
|
|
353
|
-
}
|
|
354
|
-
// GitHub web URLs: extract a clean clone URL and optional branch from /tree/<ref>
|
|
355
|
-
if (parsed.hostname === "github.com" && parsed.protocol === "https:") {
|
|
356
|
-
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
357
|
-
if (segments.length < 2) {
|
|
358
|
-
throw new ConfigError(`Git provider URL must point to a repository, got "${rawUrl}"`);
|
|
359
|
-
}
|
|
360
|
-
const owner = sanitizeString(segments[0]);
|
|
361
|
-
const repo = sanitizeString(segments[1].replace(/\.git$/i, ""));
|
|
362
|
-
if (!owner || !repo || !/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo)) {
|
|
363
|
-
throw new ConfigError(`Unsupported repository URL: "${rawUrl}"`);
|
|
364
|
-
}
|
|
365
|
-
let ref = null;
|
|
366
|
-
if (segments[2] === "tree" && segments.length >= 4) {
|
|
367
|
-
const rawRef = sanitizeString(segments.slice(3).join("/"), 255);
|
|
368
|
-
if (rawRef && !rawRef.includes("..") && /^[A-Za-z0-9._/-]+$/.test(rawRef)) {
|
|
369
|
-
ref = rawRef;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
const cloneUrl = `https://github.com/${owner}/${repo}`;
|
|
373
|
-
const canonicalUrl = ref ? `${cloneUrl}/tree/${ref}` : cloneUrl;
|
|
374
|
-
return { cloneUrl, ref, canonicalUrl };
|
|
375
|
-
}
|
|
376
|
-
// Any other valid git URL: use as-is for cloning, but strip embedded credentials
|
|
377
|
-
// from canonicalUrl so secrets don't leak into cache keys or warning messages.
|
|
378
|
-
let canonicalUrl = rawUrl;
|
|
379
|
-
try {
|
|
380
|
-
const u = new URL(rawUrl);
|
|
381
|
-
u.username = "";
|
|
382
|
-
u.password = "";
|
|
383
|
-
u.search = "";
|
|
384
|
-
u.hash = "";
|
|
385
|
-
canonicalUrl = u.toString();
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
// URL failed to parse — fall back to raw (validateGitUrl already accepted it)
|
|
389
|
-
}
|
|
390
|
-
return { cloneUrl: rawUrl, ref: null, canonicalUrl };
|
|
391
|
-
}
|
|
392
|
-
// ── Save support ─────────────────────────────────────────────────────────────
|
|
393
|
-
/**
|
|
394
|
-
* Recognize a stash directory as git-backed by the presence of a `.git` entry.
|
|
395
|
-
*
|
|
396
|
-
* Recognition is deliberately by `.git` presence — NOT by a configured remote.
|
|
397
|
-
* `akm init` git-inits the primary stash (see init.ts `ensureGitRepo`), so a
|
|
398
|
-
* freshly-initialized local stash with no remote is still git-backed. This is
|
|
399
|
-
* the single source of truth used both by `saveGitStash` (below) and by the
|
|
400
|
-
* end-of-run improve auto-sync gate.
|
|
401
|
-
*/
|
|
402
|
-
export function isGitBackedStash(stashDir) {
|
|
403
|
-
return fs.existsSync(path.join(stashDir, ".git"));
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Resolve the writable-override flag for an end-of-run / `akm sync` commit on
|
|
407
|
-
* the primary stash. Returns `true` when the root config explicitly marks the
|
|
408
|
-
* primary stash writable, otherwise `undefined` (leave the per-stash default
|
|
409
|
-
* untouched). Extracted so `akm sync`, `akm improve`'s end-of-run sync, and the
|
|
410
|
-
* CLI body all derive this identically instead of re-copying the expression.
|
|
411
|
-
*/
|
|
412
|
-
export function resolveWritableOverride(config) {
|
|
413
|
-
return config.writable === true ? true : undefined;
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Commit (and optionally push) local changes in a git-backed stash.
|
|
417
|
-
*
|
|
418
|
-
* Behaviour:
|
|
419
|
-
* - Not a git repo → skipped (no-op)
|
|
420
|
-
* - Git repo, no remote → commit only
|
|
421
|
-
* - Git repo, has remote, but stash is not writable → commit only
|
|
422
|
-
* - Git repo, has remote, stash is writable → commit + push
|
|
423
|
-
*
|
|
424
|
-
* When `name` is omitted the primary stash directory is used.
|
|
425
|
-
* When `message` is omitted a timestamp is used.
|
|
426
|
-
*
|
|
427
|
-
* `options.repoDir` overrides the primary-stash directory the commit targets
|
|
428
|
-
* (only honoured when `name` is omitted). Callers that already resolved the
|
|
429
|
-
* primary stash dir (e.g. `akm improve`'s end-of-run sync, whose pre-commit
|
|
430
|
-
* gate validates that exact directory) pass it here so the gate and the commit
|
|
431
|
-
* operate on the SAME directory instead of independently calling
|
|
432
|
-
* `resolveStashDir({ readOnly: true })`. When absent, behaviour is unchanged.
|
|
433
|
-
*/
|
|
434
|
-
export function saveGitStash(name, message, writableOverride, options) {
|
|
435
|
-
// `push: false` (from `akm sync --no-push`) commits but never pushes, even
|
|
436
|
-
// when the stash is writable with a remote configured.
|
|
437
|
-
const allowPush = options?.push !== false;
|
|
438
|
-
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
439
|
-
// Sanitize the user-supplied message: strip CR/LF/NUL, collapse whitespace,
|
|
440
|
-
// clamp length. An attacker can otherwise pass `--message "subject\n\n\
|
|
441
|
-
// Co-Authored-By: someone-else"` and forge trailers in the commit log.
|
|
442
|
-
// Empty result falls back to the timestamped default.
|
|
443
|
-
const sanitized = message ? sanitizeCommitMessage(message) : "";
|
|
444
|
-
const commitMessage = sanitized || `akm save ${timestamp}`;
|
|
445
|
-
let repoDir;
|
|
446
|
-
let writable = false;
|
|
447
|
-
if (name) {
|
|
448
|
-
const config = loadConfig();
|
|
449
|
-
const stash = findGitStashByTarget(getSources(config), name);
|
|
450
|
-
if (!stash)
|
|
451
|
-
throw new UsageError(`No git stash found with name "${name}"`);
|
|
452
|
-
if (stash.type !== "git") {
|
|
453
|
-
throw new UsageError(`Stash "${name}" is not a git stash (type: ${stash.type})`);
|
|
454
|
-
}
|
|
455
|
-
if (!stash.url)
|
|
456
|
-
throw new UsageError(`Stash "${name}" has no URL configured`);
|
|
457
|
-
const repo = parseGitRepoUrl(stash.url);
|
|
458
|
-
repoDir = getCachePaths(repo.canonicalUrl).repoDir;
|
|
459
|
-
writable = stash.writable === true;
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
// Honour an explicit primary-stash dir override (keeps the improve gate and
|
|
463
|
-
// the commit on the same directory); otherwise resolve the default.
|
|
464
|
-
repoDir = options?.repoDir ?? resolveStashDir({ readOnly: true });
|
|
465
|
-
// Allow caller to override writable for the primary stash (e.g. from root config.writable)
|
|
466
|
-
if (writableOverride !== undefined) {
|
|
467
|
-
writable = writableOverride;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
// No-op: not a git repo
|
|
471
|
-
if (!isGitBackedStash(repoDir)) {
|
|
472
|
-
return { committed: false, pushed: false, skipped: true, reason: "not a git repository", output: "" };
|
|
473
|
-
}
|
|
474
|
-
// Nothing to commit?
|
|
475
|
-
const statusResult = runGit(["-C", repoDir, "status", "--porcelain"]);
|
|
476
|
-
if (statusResult.error || statusResult.status !== 0) {
|
|
477
|
-
throw new Error(`git status failed: ${statusResult.error?.message || statusResult.stderr?.trim() || "unknown error"}`);
|
|
478
|
-
}
|
|
479
|
-
if (!statusResult.stdout.trim()) {
|
|
480
|
-
return { committed: false, pushed: false, skipped: false, output: "nothing to commit, working tree clean" };
|
|
481
|
-
}
|
|
482
|
-
// Safety check (#476): when the stash dir is shared with a non-akm project
|
|
483
|
-
// (stash root == project repo root), `git add -A` would stage every dirty
|
|
484
|
-
// file in the user's working tree and push their unrelated WIP to the
|
|
485
|
-
// stash's remote. Refuse if any dirty path is outside the known akm-
|
|
486
|
-
// managed subtrees (TYPE_DIRS + `.akm/` state).
|
|
487
|
-
const nonAkmDirty = collectNonAkmDirtyPaths(statusResult.stdout);
|
|
488
|
-
if (nonAkmDirty.length > 0) {
|
|
489
|
-
const sample = nonAkmDirty.slice(0, 10);
|
|
490
|
-
const more = nonAkmDirty.length > sample.length ? `\n ...and ${nonAkmDirty.length - sample.length} more` : "";
|
|
491
|
-
throw new Error(`refusing to push: stash repo at ${repoDir} has uncommitted non-akm changes:\n` +
|
|
492
|
-
sample.map((p) => ` ${p}`).join("\n") +
|
|
493
|
-
more +
|
|
494
|
-
`\nCommit or stash these manually before running an akm push. ` +
|
|
495
|
-
`Akm-managed paths are: ${Object.values(TYPE_DIRS).join(", ")}, .akm/`);
|
|
496
|
-
}
|
|
497
|
-
// Stage and commit — supply fallback identity so fresh environments without
|
|
498
|
-
// user.name/user.email configured can always commit to the default stash.
|
|
499
|
-
// `add -A` is safe here because nonAkmDirty was just verified empty.
|
|
500
|
-
const addResult = runGit(["-C", repoDir, "add", "-A"]);
|
|
501
|
-
if (addResult.status !== 0) {
|
|
502
|
-
throw new Error(`git add failed: ${addResult.stderr?.trim() || "unknown error"}`);
|
|
503
|
-
}
|
|
504
|
-
const commitResult = runGit([
|
|
505
|
-
"-C",
|
|
506
|
-
repoDir,
|
|
507
|
-
"-c",
|
|
508
|
-
"user.name=akm",
|
|
509
|
-
"-c",
|
|
510
|
-
"user.email=akm@local",
|
|
511
|
-
"commit",
|
|
512
|
-
"-m",
|
|
513
|
-
commitMessage,
|
|
514
|
-
]);
|
|
515
|
-
if (commitResult.status !== 0) {
|
|
516
|
-
throw new Error(`git commit failed: ${commitResult.stderr?.trim() || "unknown error"}`);
|
|
517
|
-
}
|
|
518
|
-
// Push only when there is a remote AND the stash is marked writable
|
|
519
|
-
const remoteResult = runGit(["-C", repoDir, "remote"]);
|
|
520
|
-
if (remoteResult.status !== 0) {
|
|
521
|
-
throw new Error(`git remote failed: ${remoteResult.stderr?.trim() || "unknown error"}`);
|
|
522
|
-
}
|
|
523
|
-
const hasRemote = remoteResult.stdout.trim().length > 0;
|
|
524
|
-
if (!hasRemote || !writable || !allowPush) {
|
|
525
|
-
return { committed: true, pushed: false, skipped: false, output: commitResult.stdout.trim() };
|
|
526
|
-
}
|
|
527
|
-
const pushResult = runGit(["-C", repoDir, "push"], { timeout: 120_000 });
|
|
528
|
-
if (pushResult.status !== 0) {
|
|
529
|
-
throw new Error(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`);
|
|
530
|
-
}
|
|
531
|
-
return {
|
|
532
|
-
committed: true,
|
|
533
|
-
pushed: true,
|
|
534
|
-
skipped: false,
|
|
535
|
-
output: (commitResult.stdout + pushResult.stdout).trim() || "changes committed and pushed",
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
function findGitStashByTarget(stashes, target) {
|
|
539
|
-
return stashes.find((stash) => matchesGitStashTarget(stash, target));
|
|
540
|
-
}
|
|
541
|
-
function matchesGitStashTarget(stash, target) {
|
|
542
|
-
if (stash.type !== "git")
|
|
543
|
-
return false;
|
|
544
|
-
if (stash.name === target || stash.url === target)
|
|
545
|
-
return true;
|
|
546
|
-
if (!stash.url)
|
|
547
|
-
return false;
|
|
548
|
-
try {
|
|
549
|
-
const repo = parseGitRepoUrl(stash.url);
|
|
550
|
-
if (repo.canonicalUrl === target)
|
|
551
|
-
return true;
|
|
552
|
-
return buildGithubTargetAliases(repo.canonicalUrl).has(target);
|
|
553
|
-
}
|
|
554
|
-
catch {
|
|
555
|
-
return false;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
function buildGithubTargetAliases(canonicalUrl) {
|
|
559
|
-
try {
|
|
560
|
-
const parsed = new URL(canonicalUrl);
|
|
561
|
-
if (parsed.hostname !== "github.com")
|
|
562
|
-
return new Set();
|
|
563
|
-
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
564
|
-
if (segments.length < 2)
|
|
565
|
-
return new Set();
|
|
566
|
-
const owner = segments[0];
|
|
567
|
-
const repo = segments[1];
|
|
568
|
-
const aliases = new Set([`${owner}/${repo}`, `github:${owner}/${repo}`]);
|
|
569
|
-
if (segments[2] === "tree" && segments.length >= 4) {
|
|
570
|
-
const ref = segments.slice(3).join("/");
|
|
571
|
-
aliases.add(`${owner}/${repo}#${ref}`);
|
|
572
|
-
aliases.add(`github:${owner}/${repo}#${ref}`);
|
|
573
|
-
}
|
|
574
|
-
return aliases;
|
|
575
|
-
}
|
|
576
|
-
catch {
|
|
577
|
-
return new Set();
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
// ── Clone-failure classification (#487) ─────────────────────────────────────
|
|
581
|
-
/**
|
|
582
|
-
* Translate git's stderr into an actionable message. Without this, a user
|
|
583
|
-
* who passes a nonexistent or private repo to `akm add` sees:
|
|
584
|
-
*
|
|
585
|
-
* "could not read Username for 'https://github.com': No such device or
|
|
586
|
-
* address"
|
|
587
|
-
*
|
|
588
|
-
* That is git falling through to its auth-prompt path — the actual cause
|
|
589
|
-
* is "repo doesn't exist (or is private)". We classify the common patterns
|
|
590
|
-
* and emit a message that names the cause and the fix.
|
|
591
|
-
*/
|
|
592
|
-
export function classifyCloneFailure(url, stderr, spawnError) {
|
|
593
|
-
const raw = (stderr ?? "").trim();
|
|
594
|
-
const spawnMsg = spawnError?.message ?? "";
|
|
595
|
-
// `git` binary not on PATH.
|
|
596
|
-
if (spawnError?.code === "ENOENT") {
|
|
597
|
-
return `Failed to clone ${url}: 'git' is not installed or not on PATH. Install git, then re-run.`;
|
|
598
|
-
}
|
|
599
|
-
// Auth-prompt fall-through (the headline #487 case).
|
|
600
|
-
if (/could not read Username|terminal prompts disabled|Authentication failed|fatal: Authentication/i.test(raw)) {
|
|
601
|
-
return (`Failed to clone ${url}: repository not found or private. ` +
|
|
602
|
-
`If the repository is public, double-check the URL and try again. ` +
|
|
603
|
-
`If it is private, set GH_TOKEN (or configure a git credential helper) before re-running.`);
|
|
604
|
-
}
|
|
605
|
-
// 404-style messages from git http.
|
|
606
|
-
if (/repository '.*' not found|HTTP 404|fatal: remote error|not found:|Not Found/i.test(raw)) {
|
|
607
|
-
return (`Failed to clone ${url}: repository not found. ` +
|
|
608
|
-
`Check the URL — for GitHub, the form is 'owner/repo' or 'github:owner/repo'.`);
|
|
609
|
-
}
|
|
610
|
-
// SSH connection issues.
|
|
611
|
-
if (/Permission denied \(publickey\)|kex_exchange_identification|Connection refused|Connection timed out/i.test(raw)) {
|
|
612
|
-
return (`Failed to clone ${url}: network or SSH failure. ` +
|
|
613
|
-
`Check connectivity, your SSH agent, and the remote host's availability.`);
|
|
614
|
-
}
|
|
615
|
-
// Branch / ref-specific failures.
|
|
616
|
-
if (/Remote branch .* not found in upstream origin|couldn't find remote ref/i.test(raw)) {
|
|
617
|
-
return (`Failed to clone ${url}: the requested branch/tag does not exist on the remote. ` +
|
|
618
|
-
`Verify the ref name and re-run.`);
|
|
619
|
-
}
|
|
620
|
-
const detail = raw || spawnMsg || "unknown error";
|
|
621
|
-
return `Failed to clone ${url}: ${detail}`;
|
|
622
|
-
}
|
|
623
|
-
// ── Stash-safety helpers (#476) ──────────────────────────────────────────────
|
|
624
|
-
/**
|
|
625
|
-
* Inspect `git status --porcelain` output and return every dirty path that is
|
|
626
|
-
* NOT inside an akm-managed subtree. Used by `runUpstreamPush` to refuse
|
|
627
|
-
* pushing unrelated WIP when a writable stash shares its root with a project
|
|
628
|
-
* repo.
|
|
629
|
-
*
|
|
630
|
-
* Porcelain v1 format: `XY <path>` or `XY <orig> -> <new>` for renames. We
|
|
631
|
-
* key off the post-rename path (or the only path) — that is the working-tree
|
|
632
|
-
* file at risk of being staged by `git add -A`.
|
|
633
|
-
*/
|
|
634
|
-
function collectNonAkmDirtyPaths(porcelainOutput) {
|
|
635
|
-
const akmDirs = new Set(Object.values(TYPE_DIRS));
|
|
636
|
-
const result = [];
|
|
637
|
-
for (const rawLine of porcelainOutput.split("\n")) {
|
|
638
|
-
const line = rawLine.replace(/\r$/, "");
|
|
639
|
-
if (line.length === 0)
|
|
640
|
-
continue;
|
|
641
|
-
// Skip the 2-char status code + 1 space.
|
|
642
|
-
let p = line.length > 3 ? line.slice(3) : "";
|
|
643
|
-
// Renames / copies: `from -> to`. Stage decision applies to `to`.
|
|
644
|
-
const arrow = p.lastIndexOf(" -> ");
|
|
645
|
-
if (arrow !== -1) {
|
|
646
|
-
p = p.slice(arrow + 4);
|
|
647
|
-
}
|
|
648
|
-
// Strip surrounding quotes for paths with special chars.
|
|
649
|
-
if (p.startsWith('"') && p.endsWith('"') && p.length >= 2) {
|
|
650
|
-
p = p.slice(1, -1);
|
|
651
|
-
}
|
|
652
|
-
if (!p)
|
|
653
|
-
continue;
|
|
654
|
-
const segments = p.split("/");
|
|
655
|
-
const top = segments[0];
|
|
656
|
-
if (top === ".akm" || akmDirs.has(top))
|
|
657
|
-
continue;
|
|
658
|
-
result.push(p);
|
|
659
|
-
}
|
|
660
|
-
return result;
|
|
661
|
-
}
|
|
662
|
-
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
663
|
-
export { collectNonAkmDirtyPaths, ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl };
|
|
664
|
-
// resolveWritableOverride is exported at its declaration above.
|
|
4
|
+
// Barrel for the git source provider, split by responsibility:
|
|
5
|
+
// - git-provider.ts — the SourceProvider, repo-URL parsing, cache/mirror sync
|
|
6
|
+
// - git-install.ts — install/clone helpers (`akm add` / `akm update`)
|
|
7
|
+
// - git-stash.ts — stash save/commit/push
|
|
8
|
+
//
|
|
9
|
+
// Re-exported here so every consumer (and every `spyOn(gitProvider, …)` test)
|
|
10
|
+
// keeps importing from a single module namespace.
|
|
11
|
+
export { classifyCloneFailure, cloneRepo, syncRegistryGitRef } from "./git-install.js";
|
|
12
|
+
export { ensureGitMirror, GitSourceProvider, getCachePaths, parseGitRepoUrl, syncMirroredRepo, } from "./git-provider.js";
|
|
13
|
+
export { isGitBackedStash, resolveWritableOverride, saveGitStash, } from "./git-stash.js";
|