akm-cli 0.9.0-rc.0 → 0.9.0-rc.13
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 +1283 -22
- package/README.md +62 -37
- package/SECURITY.md +46 -31
- package/dist/akm +162 -38
- package/dist/akm-migrate +44 -0
- package/dist/assets/backends/schtasks-template.xml +2 -1
- package/dist/assets/hints/cli-hints-full.md +268 -118
- package/dist/assets/hints/cli-hints-short.md +87 -24
- package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
- package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
- package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
- package/dist/assets/improve-strategies/frequent.json +15 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
- package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
- package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
- package/dist/assets/improve-strategies/reflect-distill.json +30 -0
- package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +5 -5
- package/dist/assets/prompts/extract-session.md +2 -6
- package/dist/assets/prompts/memory-infer-user.md +2 -3
- package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
- package/dist/assets/prompts/reflect-output-repair.md +3 -0
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +38 -10
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
- package/dist/assets/tasks/core/extract.yml +3 -2
- package/dist/assets/tasks/core/improve.yml +2 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +2 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
- package/dist/assets/templates/html/health.html +5 -4
- package/dist/assets/workflows/workflow-template.md +31 -15
- package/dist/cli/invocation.js +279 -0
- package/dist/cli/parse-args.js +5 -90
- package/dist/cli/retired-commands.js +78 -0
- package/dist/cli/shared.js +158 -48
- package/dist/cli-node.mjs +2 -1
- package/dist/cli.js +747 -293
- package/dist/commands/agent/agent-dispatch.js +19 -18
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +43 -97
- package/dist/commands/completions.js +80 -23
- package/dist/commands/config-cli.js +44 -281
- package/dist/commands/env/env-binding.js +99 -0
- package/dist/commands/env/env-cli.js +84 -224
- package/dist/commands/env/env.js +12 -163
- package/dist/commands/env/marker-path.js +6 -0
- package/dist/commands/env/secret-cli.js +45 -61
- package/dist/commands/env/secret.js +32 -62
- package/dist/commands/feedback-cli.js +179 -85
- package/dist/commands/health/accept-rate.js +58 -0
- package/dist/commands/health/advisories.js +7 -8
- package/dist/commands/health/checks.js +279 -94
- package/dist/commands/health/html-report.js +197 -578
- package/dist/commands/health/improve-metrics.js +277 -246
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +16 -7
- package/dist/commands/health/metrics.js +67 -32
- package/dist/commands/health/renderers.js +47 -0
- package/dist/commands/health/report-view-model.js +508 -0
- package/dist/commands/health/stash-exposure.js +1 -1
- package/dist/commands/health/surfaces.js +16 -56
- package/dist/commands/health/task-runs.js +3 -67
- package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
- package/dist/commands/health/types-improve.js +29 -0
- package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
- package/dist/commands/health/types-result.js +7 -0
- package/dist/commands/health/types-runs.js +4 -0
- package/dist/commands/health/types-session-log.js +4 -0
- package/dist/commands/health/types-windows.js +4 -0
- package/dist/commands/health/types.js +26 -21
- package/dist/commands/health/windows.js +2 -3
- package/dist/commands/health.js +296 -167
- package/dist/commands/improve/anti-collapse.js +5 -5
- package/dist/commands/improve/autonomy-gate.js +68 -0
- package/dist/commands/improve/collapse-detector.js +65 -52
- package/dist/commands/improve/consolidate/chunking.js +9 -7
- package/dist/commands/improve/consolidate/eligibility.js +1 -23
- package/dist/commands/improve/consolidate/merge.js +4 -0
- package/dist/commands/improve/consolidate.js +454 -1354
- package/dist/commands/improve/content-hash.js +39 -0
- package/dist/commands/improve/distill/content-repair.js +4 -10
- package/dist/commands/improve/distill/promote-memory.js +89 -64
- package/dist/commands/improve/distill/quality-gate.js +118 -42
- package/dist/commands/improve/distill-guards.js +1 -1
- package/dist/commands/improve/distill-promotion-policy.js +33 -888
- package/dist/commands/improve/distill.js +607 -363
- package/dist/commands/improve/eligibility.js +165 -79
- package/dist/commands/improve/extract-cli.js +35 -126
- package/dist/commands/improve/extract-prompt.js +6 -35
- package/dist/commands/improve/extract.js +640 -391
- package/dist/commands/improve/feedback-valence.js +2 -12
- package/dist/commands/improve/improve-cli.js +134 -135
- package/dist/commands/improve/improve-result-file.js +30 -50
- package/dist/commands/improve/improve-run-types.js +4 -0
- package/dist/commands/improve/improve-strategies.js +135 -0
- package/dist/commands/improve/improve.js +904 -701
- package/dist/commands/improve/locks.js +64 -111
- package/dist/commands/improve/loop-stages.js +1110 -923
- package/dist/commands/improve/memory/derived-ref.js +124 -0
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
- package/dist/commands/improve/memory/memory-improve.js +25 -37
- package/dist/commands/improve/outcome-loop.js +25 -88
- package/dist/commands/improve/preparation.js +1034 -813
- package/dist/commands/improve/proactive-maintenance.js +34 -9
- package/dist/commands/improve/proposal-envelope.js +31 -0
- package/dist/commands/improve/reflect.js +983 -794
- package/dist/commands/improve/run-context.js +119 -0
- package/dist/commands/improve/salience.js +24 -127
- package/dist/commands/improve/session-asset.js +7 -3
- package/dist/commands/improve/shared.js +14 -34
- package/dist/commands/improve/source-identity.js +28 -0
- package/dist/commands/improve/triage.js +20 -17
- package/dist/commands/lint/base-linter.js +340 -313
- package/dist/commands/lint/env-key-rules.js +31 -47
- package/dist/commands/lint/index.js +185 -30
- package/dist/commands/{events.js → log.js} +28 -38
- package/dist/commands/migrate-cli.js +54 -0
- package/dist/commands/migration-tool.js +55 -0
- package/dist/commands/observability-cli.js +70 -208
- package/dist/commands/proposal/diff-format.js +50 -0
- package/dist/commands/proposal/drain-policies.js +0 -6
- package/dist/commands/proposal/drain.js +91 -40
- package/dist/commands/proposal/proposal-cli.js +134 -132
- package/dist/commands/proposal/proposal-types.js +56 -0
- package/dist/commands/proposal/proposal.js +83 -65
- package/dist/commands/proposal/propose-cli.js +88 -0
- package/dist/commands/proposal/propose.js +105 -88
- package/dist/commands/proposal/repository.js +1303 -278
- package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
- package/dist/commands/proposal/validators/proposal-validators.js +61 -12
- package/dist/commands/proposal/validators/proposals.js +6 -8
- package/dist/commands/read/curate.js +78 -73
- package/dist/commands/read/knowledge.js +510 -13
- package/dist/commands/read/registry-search.js +2 -2
- package/dist/commands/read/remember-cli.js +84 -15
- package/dist/commands/read/search-cli.js +203 -96
- package/dist/commands/read/search.js +126 -94
- package/dist/commands/read/show.js +226 -250
- package/dist/commands/registry-cli.js +34 -60
- package/dist/commands/remember.js +18 -57
- package/dist/commands/sources/add-cli.js +104 -49
- package/dist/commands/sources/bundle-cli.js +166 -0
- package/dist/commands/sources/bundle-config-ops.js +63 -0
- package/dist/commands/sources/info.js +27 -15
- package/dist/commands/sources/init.js +30 -40
- package/dist/commands/sources/installed-stashes.js +469 -172
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +10 -9
- package/dist/commands/sources/self-update.js +182 -121
- package/dist/commands/sources/source-add.js +169 -178
- package/dist/commands/sources/source-clone.js +144 -41
- package/dist/commands/sources/source-manage.js +94 -59
- package/dist/commands/sources/sources-cli.js +64 -205
- package/dist/commands/sources/stash-cli.js +91 -54
- package/dist/commands/sources/stash-skeleton.js +1 -1
- package/dist/commands/tasks/tasks-cli.js +106 -104
- package/dist/commands/tasks/tasks.js +445 -262
- package/dist/commands/workflow-cli.js +232 -121
- package/dist/core/action-contributors.js +1 -1
- package/dist/core/activation-policy.js +49 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
- package/dist/core/adapter/adapters/akm-adapter.js +528 -0
- package/dist/core/adapter/adapters/akm-lint.js +392 -0
- package/dist/core/adapter/adapters/akm-metadata.js +387 -0
- package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
- package/dist/core/adapter/adapters/claude-adapter.js +61 -0
- package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
- package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
- package/dist/core/adapter/adapters/index.js +80 -0
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
- package/dist/core/adapter/adapters/okf-adapter.js +391 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
- package/dist/core/adapter/adapters/shared.js +286 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
- package/dist/core/adapter/bundle-adapter.js +4 -0
- package/dist/core/adapter/detect-adapter.js +17 -0
- package/dist/core/adapter/recognize-match.js +44 -0
- package/dist/core/adapter/registry.js +56 -0
- package/dist/core/adapter/types.js +4 -0
- package/dist/core/asset/akm-markdown.js +30 -0
- package/dist/core/asset/asset-placement.js +243 -0
- package/dist/core/asset/asset-ref.js +110 -79
- package/dist/core/asset/asset-serialize.js +20 -0
- package/dist/core/asset/frontmatter.js +28 -12
- package/dist/core/asset/markdown.js +40 -51
- package/dist/core/asset/resolve-ref.js +274 -0
- package/dist/core/asset/stash-meta.js +2 -2
- package/dist/core/bundle-id.js +51 -0
- package/dist/core/common.js +281 -86
- package/dist/core/config/config-io.js +42 -128
- package/dist/core/config/config-schema.js +233 -834
- package/dist/core/config/config-sources.js +162 -39
- package/dist/core/config/config-types.js +16 -11
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +126 -37
- package/dist/core/config/config.js +154 -331
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +28 -0
- package/dist/core/config/experimental.js +21 -0
- package/dist/core/config/schema/embedding.js +38 -0
- package/dist/core/config/schema/engines.js +116 -0
- package/dist/core/config/schema/experimental.js +47 -0
- package/dist/core/config/schema/feedback.js +31 -0
- package/dist/core/config/schema/improve-processes.js +389 -0
- package/dist/core/config/schema/improve.js +94 -0
- package/dist/core/config/schema/index-config.js +176 -0
- package/dist/core/config/schema/output.js +18 -0
- package/dist/core/config/schema/primitives.js +94 -0
- package/dist/core/config/schema/search.js +30 -0
- package/dist/core/config/schema/setup.js +18 -0
- package/dist/core/config/schema/sources-bundles.js +169 -0
- package/dist/core/config/schema/workflow.js +29 -0
- package/dist/core/env-secret-ref.js +155 -20
- package/dist/core/errors.js +17 -15
- package/dist/core/events-types.js +4 -0
- package/dist/core/events.js +46 -128
- package/dist/core/extra-params.js +62 -0
- package/dist/core/file-change.js +17 -0
- package/dist/core/file-lock.js +202 -57
- package/dist/core/fs-txn.js +392 -0
- package/dist/core/git-message.js +59 -0
- package/dist/core/improve-result.js +167 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/lesson-lint.js +1 -17
- package/dist/core/logs-db.js +1 -1
- package/dist/core/maintenance-barrier.js +135 -0
- package/dist/core/migration-operation.js +44 -0
- package/dist/core/mutation-target.js +78 -0
- package/dist/core/paths.js +22 -25
- package/dist/core/platform.js +10 -0
- package/dist/core/recognition-util.js +128 -0
- package/dist/core/redaction.js +392 -0
- package/dist/core/standards/resolve-standards-context.js +36 -65
- package/dist/core/standards/resolve-stash-standards.js +2 -2
- package/dist/core/standards/resolve-type-conventions.js +5 -5
- package/dist/core/state/migrations.js +242 -11
- package/dist/core/state-db.js +98 -10
- package/dist/core/structured.js +1 -1
- package/dist/core/subprocess.js +303 -0
- package/dist/core/text-truncation.js +9 -5
- package/dist/core/time.js +20 -0
- package/dist/core/type-presentation.js +130 -0
- package/dist/core/warn.js +0 -3
- package/dist/core/write-source.js +834 -118
- package/dist/indexer/bundle-identity-guard.js +92 -0
- package/dist/indexer/db/graph-db.js +1 -25
- package/dist/indexer/db/llm-cache.js +1 -1
- package/dist/indexer/ensure-index.js +30 -9
- package/dist/indexer/graph/graph-boost.js +9 -30
- package/dist/indexer/graph/graph-extraction.js +41 -27
- package/dist/indexer/graph/graph-types.js +4 -0
- package/dist/indexer/index-writer-lock.js +93 -49
- package/dist/indexer/index-written-assets.js +100 -53
- package/dist/indexer/indexer.js +746 -329
- package/dist/indexer/init.js +18 -25
- package/dist/indexer/installations.js +142 -0
- package/dist/indexer/passes/dir-staleness.js +18 -10
- package/dist/indexer/passes/memory-inference.js +25 -15
- package/dist/indexer/passes/metadata.js +412 -243
- package/dist/indexer/scan/doc-to-entry.js +160 -0
- package/dist/indexer/scan/drain-dir.js +134 -0
- package/dist/indexer/search/db-search.js +292 -108
- package/dist/indexer/search/fts-query.js +64 -0
- package/dist/indexer/search/ranking-contributors.js +145 -25
- package/dist/indexer/search/ranking-types.js +4 -0
- package/dist/indexer/search/ranking.js +28 -71
- package/dist/indexer/search/search-attribution.js +67 -0
- package/dist/indexer/search/search-fields.js +18 -3
- package/dist/indexer/search/search-hit-enrichers.js +30 -40
- package/dist/indexer/search/search-source.js +157 -111
- package/dist/indexer/search/semantic-status.js +4 -1
- package/dist/indexer/usage/usage-events.js +10 -30
- package/dist/indexer/walk/file-context.js +3 -45
- package/dist/indexer/walk/matchers.js +42 -34
- package/dist/indexer/walk/path-resolver.js +11 -5
- package/dist/indexer/walk/walker.js +42 -14
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -56
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +231 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/prompts.js +46 -18
- package/dist/integrations/agent/runner-dispatch.js +93 -4
- package/dist/integrations/agent/runner.js +76 -208
- package/dist/integrations/agent/spawn.js +88 -196
- package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
- package/dist/integrations/harnesses/aider/index.js +48 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
- package/dist/integrations/harnesses/amazonq/index.js +45 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/config-import.js +1 -3
- package/dist/integrations/harnesses/claude/index.js +24 -35
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -75
- package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
- package/dist/integrations/harnesses/codex/index.js +52 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +48 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +48 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/ids.js +24 -0
- package/dist/integrations/harnesses/index.js +54 -34
- package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
- package/dist/integrations/harnesses/opencode/config-import.js +1 -3
- package/dist/integrations/harnesses/opencode/index.js +14 -32
- package/dist/integrations/harnesses/opencode/session-log.js +67 -125
- package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
- package/dist/integrations/harnesses/openhands/index.js +48 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
- package/dist/integrations/harnesses/pi/index.js +45 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/shared.js +17 -0
- package/dist/integrations/harnesses/types.js +43 -32
- package/dist/integrations/lockfile.js +211 -24
- package/dist/integrations/session-logs/index.js +36 -39
- package/dist/integrations/session-logs/provider-base.js +113 -0
- package/dist/llm/client.js +182 -110
- package/dist/llm/embedders/deterministic.js +2 -2
- package/dist/llm/embedders/remote.js +21 -9
- package/dist/llm/feature-gate.js +17 -57
- package/dist/llm/graph-extract.js +12 -13
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer.js +144 -1
- package/dist/llm/metadata-enhance.js +45 -30
- package/dist/llm/structured-call.js +16 -8
- package/dist/llm/usage-persist.js +30 -5
- package/dist/llm/usage-telemetry.js +59 -6
- package/dist/output/cli-hints.js +1 -2
- package/dist/output/command-registry.js +27 -0
- package/dist/output/context.js +22 -7
- package/dist/output/format-exempt.js +80 -0
- package/dist/output/generic-render.js +251 -0
- package/dist/output/html-render.js +11 -16
- package/dist/output/render-registry.js +57 -0
- package/dist/output/renderers.js +14 -279
- package/dist/output/shapes/curate.js +10 -1
- package/dist/output/shapes/events.js +12 -7
- package/dist/output/shapes/helpers.js +58 -84
- package/dist/output/shapes/passthrough.js +11 -39
- package/dist/output/shapes/proposal/producer.js +15 -7
- package/dist/output/shapes/registry.js +12 -6
- package/dist/output/shapes.js +0 -9
- package/dist/output/text/{init.js → bundle-create.js} +3 -1
- package/dist/output/text/bundle-show.js +7 -0
- package/dist/output/text/command-format.js +562 -0
- package/dist/output/text/env.js +1 -3
- package/dist/output/text/events.js +8 -7
- package/dist/output/text/helpers.js +15 -1164
- package/dist/output/text/proposal/producer.js +4 -2
- package/dist/output/text/proposal-format.js +202 -0
- package/dist/output/text/registry-commands.js +1 -2
- package/dist/output/text/registry.js +12 -6
- package/dist/output/text/show-directives.js +117 -0
- package/dist/output/text/show-format.js +103 -0
- package/dist/output/text/sync.js +5 -0
- package/dist/output/text/workflow-format.js +332 -0
- package/dist/output/text/workflow.js +3 -2
- package/dist/output/text.js +10 -19
- package/dist/registry/factory.js +4 -6
- package/dist/registry/origin-resolve.js +16 -27
- package/dist/registry/providers/skills-sh.js +3 -3
- package/dist/registry/providers/static-index.js +15 -25
- package/dist/registry/resolve.js +43 -94
- package/dist/registry/semver.js +43 -0
- package/dist/runtime.js +81 -12
- package/dist/scripts/akm-migrate.js +35529 -0
- package/dist/setup/detect.js +5 -7
- package/dist/setup/detected-engines.js +136 -0
- package/dist/setup/engine-config.js +100 -0
- package/dist/setup/registry-stash-loader.js +3 -3
- package/dist/setup/semantic-assets.js +12 -9
- package/dist/setup/setup.js +444 -208
- package/dist/setup/steps/connection-shared.js +120 -0
- package/dist/setup/steps/connection.js +108 -305
- package/dist/setup/steps/platforms.js +13 -12
- package/dist/setup/steps/semantic.js +15 -3
- package/dist/setup/steps/sources.js +21 -15
- package/dist/setup/steps/stashdir.js +6 -4
- package/dist/setup/steps/tasks.js +236 -119
- package/dist/setup/steps.js +3 -2
- package/dist/sources/freshness.js +39 -0
- package/dist/sources/provider-factory.js +11 -17
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +278 -34
- package/dist/sources/providers/git-provider.js +54 -56
- package/dist/sources/providers/git-stash.js +420 -91
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/providers/npm.js +16 -19
- package/dist/sources/providers/provider-utils.js +47 -22
- package/dist/sources/providers/sync-from-ref.js +3 -9
- package/dist/sources/providers/website.js +2 -2
- package/dist/sources/resolve.js +11 -10
- package/dist/sources/snapshot-fetchers/types.js +4 -0
- package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
- package/dist/storage/database.js +60 -4
- package/dist/storage/engines/sqlite-migrations.js +156 -5
- package/dist/storage/locations.js +1 -2
- package/dist/storage/repositories/canaries-repository.js +1 -1
- package/dist/storage/repositories/events-repository.js +51 -11
- package/dist/storage/repositories/improve-runs-repository.js +6 -32
- package/dist/storage/repositories/index-connection.js +79 -0
- package/dist/storage/repositories/index-db.js +4 -3
- package/dist/storage/repositories/index-entries-repository.js +863 -0
- package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
- package/dist/storage/repositories/index-entry-types.js +4 -0
- package/dist/storage/repositories/index-fts-repository.js +167 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
- package/dist/storage/repositories/index-meta-repository.js +49 -0
- package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
- package/dist/storage/repositories/index-sql.js +12 -0
- package/dist/storage/repositories/index-utility-repository.js +356 -0
- package/dist/storage/repositories/index-vec-repository.js +250 -0
- package/dist/storage/repositories/outcome-repository.js +119 -0
- package/dist/storage/repositories/proposals-repository.js +317 -75
- package/dist/storage/repositories/registry-cache.js +1 -1
- package/dist/storage/repositories/salience-repository.js +172 -0
- package/dist/storage/repositories/task-history-repository.js +110 -3
- package/dist/storage/repositories/workflow-runs-repository.js +240 -19
- package/dist/tasks/backends/cron.js +169 -46
- package/dist/tasks/backends/exec-utils.js +76 -3
- package/dist/tasks/backends/index.js +6 -9
- package/dist/tasks/backends/launchd.js +292 -55
- package/dist/tasks/backends/schtasks.js +557 -70
- package/dist/tasks/backends/types.js +4 -0
- package/dist/tasks/command-executable.js +93 -0
- package/dist/tasks/embedded.js +56 -38
- package/dist/tasks/parser.js +156 -64
- package/dist/tasks/resolve-akm-bin.js +144 -51
- package/dist/tasks/runner.js +377 -209
- package/dist/tasks/schedule.js +108 -19
- package/dist/tasks/scheduler-invocation.js +296 -0
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/task-id.js +35 -0
- package/dist/tasks/validator.js +30 -16
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/authoring/authoring.js +104 -43
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/cli.js +0 -16
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/exec/brief.js +450 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1038 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1460 -0
- package/dist/workflows/exec/run-workflow.js +602 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1190 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/workflow-engine-gate.js +67 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +246 -0
- package/dist/workflows/ir/freeze.js +233 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +540 -0
- package/dist/workflows/parser.js +878 -304
- package/dist/workflows/program/expressions.js +181 -0
- package/dist/workflows/program/schema.js +51 -0
- package/dist/workflows/renderer.js +100 -45
- package/dist/workflows/resource-limits.js +22 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/checkin.js +1 -1
- package/dist/workflows/runtime/plan-classifier.js +131 -0
- package/dist/workflows/runtime/runs.js +376 -119
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/unit-phases.js +20 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +2 -3
- package/dist/workflows/validator.js +52 -30
- package/docs/README.md +42 -78
- package/docs/migration/README.md +8 -0
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +9 -8
- package/docs/migration/release-notes/0.9.0.md +158 -14
- package/docs/migration/v0.7-to-v0.8.md +46 -47
- package/docs/migration/v0.8-to-v0.9.md +844 -0
- package/docs/reference/README.md +12 -0
- package/docs/reference/data-and-telemetry.md +333 -0
- package/package.json +21 -17
- package/schemas/akm-asset-envelope.json +93 -0
- package/schemas/akm-config.json +4636 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +373 -0
- package/dist/akm-migrate-storage +0 -38
- package/dist/assets/help/help-accept.md +0 -12
- package/dist/assets/help/help-improve.md +0 -84
- package/dist/assets/help/help-proposals.md +0 -17
- package/dist/assets/help/help-propose.md +0 -17
- package/dist/assets/help/help-reject.md +0 -11
- package/dist/assets/profiles/frequent.json +0 -13
- package/dist/assets/profiles/recombine-only.json +0 -21
- package/dist/assets/profiles/reflect-distill.json +0 -30
- package/dist/assets/profiles/synthesize.json +0 -15
- package/dist/assets/prompts/procedural-system.md +0 -44
- package/dist/assets/prompts/recombine-system.md +0 -40
- package/dist/assets/prompts/staleness-detect-system.md +0 -6
- package/dist/assets/tasks/core/backup.yml +0 -4
- package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
- package/dist/assets/templates/html/default.html +0 -78
- package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
- package/dist/assets/wiki/index-template.md +0 -12
- package/dist/assets/wiki/ingest-workflow-template.md +0 -83
- package/dist/assets/wiki/log-template.md +0 -8
- package/dist/assets/wiki/schema-template.md +0 -61
- package/dist/cli/config-migrate.js +0 -150
- package/dist/cli/config-validate.js +0 -39
- package/dist/commands/graph/graph-cli.js +0 -124
- package/dist/commands/graph/graph.js +0 -487
- package/dist/commands/improve/calibration.js +0 -161
- package/dist/commands/improve/dedup.js +0 -482
- package/dist/commands/improve/extract-watch.js +0 -140
- package/dist/commands/improve/hot-probation.js +0 -45
- package/dist/commands/improve/improve-auto-accept.js +0 -276
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/commands/improve/procedural.js +0 -398
- package/dist/commands/improve/recombine.js +0 -818
- package/dist/commands/improve/schema-similarity-gate.js +0 -168
- package/dist/commands/lint/agent-linter.js +0 -44
- package/dist/commands/lint/command-linter.js +0 -44
- package/dist/commands/lint/default-linter.js +0 -16
- package/dist/commands/lint/fact-linter.js +0 -39
- package/dist/commands/lint/knowledge-linter.js +0 -16
- package/dist/commands/lint/memory-linter.js +0 -61
- package/dist/commands/lint/registry.js +0 -41
- package/dist/commands/lint/skill-linter.js +0 -45
- package/dist/commands/lint/task-linter.js +0 -50
- package/dist/commands/lint/workflow-linter.js +0 -81
- package/dist/commands/proposal/legacy-import.js +0 -115
- package/dist/commands/sources/history.js +0 -196
- package/dist/commands/tasks/default-tasks.js +0 -186
- package/dist/commands/wiki-cli.js +0 -292
- package/dist/core/asset/asset-registry.js +0 -76
- package/dist/core/asset/asset-spec.js +0 -259
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/core/eval/rank-metrics.js +0 -113
- package/dist/core/ripgrep/install.js +0 -163
- package/dist/core/ripgrep/resolve.js +0 -81
- package/dist/indexer/db/db.js +0 -1413
- package/dist/indexer/manifest.js +0 -170
- package/dist/indexer/passes/metadata-contributors.js +0 -31
- package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
- package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
- package/dist/llm/call-ai.js +0 -62
- package/dist/llm/memory-infer-impl.js +0 -138
- package/dist/output/shapes/distill.js +0 -14
- package/dist/output/shapes/history.js +0 -11
- package/dist/output/text/distill.js +0 -6
- package/dist/output/text/enable-disable.js +0 -8
- package/dist/output/text/history.js +0 -6
- package/dist/output/text/wiki.js +0 -16
- package/dist/registry/build-index.js +0 -386
- package/dist/scripts/migrate-storage.js +0 -19108
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
- package/dist/scripts/migrations/v16-to-v17.js +0 -141
- package/dist/setup/legacy-config.js +0 -106
- package/dist/storage/repositories/consolidation-repository.js +0 -38
- package/dist/storage/repositories/recombine-repository.js +0 -213
- package/dist/wiki/wiki-templates.js +0 -15
- package/dist/wiki/wiki.js +0 -1012
- package/dist/workflows/db.js +0 -215
- package/docs/data-and-telemetry.md +0 -226
- /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
- /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* commit behaviour — they only ever touch the filesystem. Git-backed targets
|
|
14
14
|
* are committed in a SINGLE batch at the operation boundary via
|
|
15
15
|
* {@link commitWriteTargetBoundary} (which delegates to `saveGitStash`). This
|
|
16
|
-
*
|
|
16
|
+
* commits only operation-owned exact paths as one complete commit instead of
|
|
17
17
|
* one noisy, incomplete commit per asset.
|
|
18
18
|
*
|
|
19
19
|
* This module is still the **single dispatch point** for write/delete: callers
|
|
@@ -24,12 +24,19 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import fs from "node:fs";
|
|
26
26
|
import path from "node:path";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
27
|
+
import { withAssetMutationLeaseSync } from "../indexer/index-writer-lock.js";
|
|
28
|
+
import { lockContentRootFor } from "../integrations/lockfile.js";
|
|
29
|
+
import { GitStashPushError, getCachePaths, inspectGitUpstream, isGitBackedStash, parseGitRepoUrl, runGit, saveGitStash, } from "../sources/providers/git.js";
|
|
30
|
+
import { assertGitExactPathsClean, assertNoIgnoredExactPaths, listIgnoredExactPaths, reconcileGitExactPathIndex, } from "../sources/providers/git-stash.js";
|
|
31
|
+
import { detectAdapterId } from "./adapter/detect-adapter.js";
|
|
32
|
+
import { ensureAkmMarkdownType } from "./asset/akm-markdown.js";
|
|
33
|
+
import { assetPathForName, stashDirFor } from "./asset/asset-placement.js";
|
|
34
|
+
import { conceptIdFromTypeName, displayRef } from "./asset/resolve-ref.js";
|
|
35
|
+
import { deriveBundleId } from "./bundle-id.js";
|
|
30
36
|
import { isWithin, resolveStashDir } from "./common.js";
|
|
31
37
|
import { resolveConfiguredSources } from "./config/config.js";
|
|
32
38
|
import { ConfigError, UsageError } from "./errors.js";
|
|
39
|
+
import { sanitizeCommitMessage } from "./git-message.js";
|
|
33
40
|
import { warn } from "./warn.js";
|
|
34
41
|
/**
|
|
35
42
|
* Source kinds that the loader is allowed to mark `writable: true`. Anything
|
|
@@ -37,49 +44,210 @@ import { warn } from "./warn.js";
|
|
|
37
44
|
* {@link assertWritableAllowedForKind}.
|
|
38
45
|
*/
|
|
39
46
|
const REJECTED_WRITABLE_KINDS = new Set(["website", "npm"]);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
47
|
+
const pendingGitMutations = new Map();
|
|
48
|
+
const EMPTY_GIT_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
49
|
+
const GIT_PUSH_TIMEOUT_MS = 120_000;
|
|
50
|
+
function gitTargetKey(source) {
|
|
51
|
+
return `${path.resolve(source.repoPath ?? source.path)}\0${path.resolve(source.path)}`;
|
|
52
|
+
}
|
|
53
|
+
/** Record the exact post-mutation blob for the target's boundary commit. */
|
|
54
|
+
export function recordWriteTargetPath(source, filePath) {
|
|
55
|
+
if (source.kind !== "git")
|
|
56
|
+
return;
|
|
57
|
+
const repoDir = path.resolve(source.repoPath ?? source.path);
|
|
58
|
+
if (!isGitBackedStash(repoDir))
|
|
59
|
+
return;
|
|
60
|
+
const key = gitTargetKey(source);
|
|
61
|
+
const pending = pendingGitMutations.get(key) ?? {
|
|
62
|
+
baseHead: readOptionalGitHead(repoDir),
|
|
63
|
+
snapshots: {},
|
|
64
|
+
};
|
|
65
|
+
const snapshot = captureGitPathSnapshot({ source, config: { type: "git" } }, filePath);
|
|
66
|
+
pending.snapshots[snapshot.path] = snapshot.state;
|
|
67
|
+
pendingGitMutations.set(key, pending);
|
|
68
|
+
}
|
|
69
|
+
function repoRelativeGitPath(source, filePath) {
|
|
70
|
+
const repoDir = path.resolve(source.repoPath ?? source.path);
|
|
71
|
+
const absolutePath = path.resolve(filePath);
|
|
72
|
+
const [relativePath] = normalizePublicationPaths([path.relative(repoDir, absolutePath).replaceAll(path.sep, "/")]);
|
|
73
|
+
return relativePath;
|
|
74
|
+
}
|
|
75
|
+
/** Reject dirty or ignored exact paths before a direct transaction mutation. */
|
|
76
|
+
export function assertWriteTargetPathsClean(source, filePaths) {
|
|
77
|
+
if (source.kind !== "git")
|
|
78
|
+
return;
|
|
79
|
+
const repoDir = path.resolve(source.repoPath ?? source.path);
|
|
80
|
+
if (!isGitBackedStash(repoDir))
|
|
81
|
+
return;
|
|
82
|
+
const key = gitTargetKey(source);
|
|
83
|
+
const pending = pendingGitMutations.get(key);
|
|
84
|
+
for (const filePath of filePaths) {
|
|
85
|
+
const relativePath = repoRelativeGitPath(source, filePath);
|
|
86
|
+
if (pending && Object.hasOwn(pending.snapshots, relativePath)) {
|
|
87
|
+
preflightGitPathMutation(source, filePath);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
assertGitExactPathsClean(repoDir, [relativePath]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Preflight one operation's complete exact-path set before its first mutation. */
|
|
95
|
+
export function planWriteTargetPublication(target, filePaths, options) {
|
|
96
|
+
const paths = [...new Set(filePaths.map((filePath) => path.resolve(filePath)))];
|
|
97
|
+
assertNoWritePathDescendantSymlinks(target.source, paths);
|
|
98
|
+
if (target.source.kind !== "git")
|
|
99
|
+
return { target, paths, publish: false };
|
|
100
|
+
const repoDir = path.resolve(target.source.repoPath ?? target.source.path);
|
|
101
|
+
if (!isGitBackedStash(repoDir))
|
|
102
|
+
return { target, paths, publish: false };
|
|
103
|
+
const expectedBaseHead = readOptionalGitHead(repoDir);
|
|
104
|
+
const relativePaths = paths.map((filePath) => repoRelativeGitPath(target.source, filePath));
|
|
105
|
+
const ignored = new Set(listIgnoredExactPaths(repoDir, relativePaths));
|
|
106
|
+
if (ignored.size > 0) {
|
|
107
|
+
if (options.ignored === "reject") {
|
|
108
|
+
throw new UsageError(`Exact Git publication path is ignored: ${[...ignored][0]}. Update .gitignore or choose a tracked destination before writing.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const unignoredPaths = paths.filter((_, index) => !ignored.has(relativePaths[index]));
|
|
112
|
+
assertWriteTargetPathsClean(target.source, unignoredPaths);
|
|
113
|
+
assertWriteTargetPlanBase(target, expectedBaseHead);
|
|
114
|
+
return { target, paths, publish: ignored.size === 0, expectedBaseHead };
|
|
115
|
+
}
|
|
116
|
+
function assertNoWritePathDescendantSymlinks(source, filePaths) {
|
|
117
|
+
const lexicalRoot = path.resolve(source.path);
|
|
118
|
+
const canonicalRoot = fs.realpathSync(lexicalRoot);
|
|
119
|
+
for (const filePath of filePaths) {
|
|
120
|
+
const relative = path.relative(lexicalRoot, filePath);
|
|
121
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
122
|
+
throw new UsageError(`Write path resolves outside source "${source.name}".`, "PATH_ESCAPE_VIOLATION");
|
|
123
|
+
}
|
|
124
|
+
let current = canonicalRoot;
|
|
125
|
+
for (const segment of relative.split(path.sep)) {
|
|
126
|
+
current = path.join(current, segment);
|
|
127
|
+
try {
|
|
128
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
129
|
+
throw new UsageError(`Write path contains a symbolic link below source "${source.name}": ${relative}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error.code === "ENOENT")
|
|
134
|
+
break;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function assertWriteTargetPlanBase(target, expectedBaseHead) {
|
|
141
|
+
const repoDir = path.resolve(target.source.repoPath ?? target.source.path);
|
|
142
|
+
if (readOptionalGitHead(repoDir) !== expectedBaseHead) {
|
|
143
|
+
throw new UsageError(`Git target "${target.source.name}" advanced after exact-path preflight.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Revalidate a publication plan immediately before its first mutation. */
|
|
147
|
+
export function beginWriteTargetMutation(plan) {
|
|
148
|
+
if (plan.expectedBaseHead === undefined)
|
|
149
|
+
return;
|
|
150
|
+
assertWriteTargetPlanBase(plan.target, plan.expectedBaseHead);
|
|
151
|
+
}
|
|
152
|
+
/** Publish exactly the paths bound by {@link planWriteTargetPublication}. */
|
|
153
|
+
export function publishWriteTargetPlan(plan, message, expectedSnapshots) {
|
|
154
|
+
if (!plan.publish)
|
|
155
|
+
return;
|
|
156
|
+
if (plan.expectedBaseHead === undefined) {
|
|
157
|
+
throw new UsageError(`Git publication plan for "${plan.target.source.name}" has no preflight base.`);
|
|
158
|
+
}
|
|
159
|
+
assertWriteTargetPlanBase(plan.target, plan.expectedBaseHead);
|
|
160
|
+
for (const filePath of plan.paths)
|
|
161
|
+
recordWriteTargetPath(plan.target.source, filePath);
|
|
162
|
+
commitWriteTargetBoundary(plan.target, message, {
|
|
163
|
+
paths: [...plan.paths],
|
|
164
|
+
expectedBaseHead: plan.expectedBaseHead,
|
|
165
|
+
...(expectedSnapshots ? { expectedSnapshots } : {}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Hold the shared asset lease from exact-path preflight through publication. */
|
|
169
|
+
export function withWriteTargetMutation(target, paths, options, mutate) {
|
|
170
|
+
return withAssetMutationLeaseSync(options.purpose, () => {
|
|
171
|
+
const plan = planWriteTargetPublication(target, paths, { ignored: options.ignored });
|
|
172
|
+
beginWriteTargetMutation(plan);
|
|
173
|
+
const result = mutate();
|
|
174
|
+
const expectedSnapshots = captureIntendedWriteTargetState(plan);
|
|
175
|
+
publishWriteTargetPlan(plan, options.message, expectedSnapshots);
|
|
176
|
+
return result;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function captureIntendedWriteTargetState(plan) {
|
|
180
|
+
if (plan.expectedBaseHead === undefined)
|
|
181
|
+
return undefined;
|
|
182
|
+
const snapshots = {};
|
|
183
|
+
for (const filePath of plan.paths) {
|
|
184
|
+
const snapshot = captureGitPathSnapshot(plan.target, filePath);
|
|
185
|
+
snapshots[snapshot.path] = snapshot.state;
|
|
186
|
+
}
|
|
187
|
+
return snapshots;
|
|
188
|
+
}
|
|
189
|
+
function readOptionalGitHead(repoDir) {
|
|
190
|
+
const result = runGit(["-C", repoDir, "rev-parse", "--verify", "HEAD"]);
|
|
191
|
+
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
|
|
192
|
+
}
|
|
193
|
+
function preflightGitPathMutation(source, filePath) {
|
|
194
|
+
if (source.kind !== "git")
|
|
195
|
+
return undefined;
|
|
196
|
+
const repoDir = path.resolve(source.repoPath ?? source.path);
|
|
197
|
+
if (!isGitBackedStash(repoDir))
|
|
198
|
+
return undefined;
|
|
199
|
+
const relativePath = repoRelativeGitPath(source, filePath);
|
|
200
|
+
const key = gitTargetKey(source);
|
|
201
|
+
let pending = pendingGitMutations.get(key);
|
|
202
|
+
const created = pending === undefined;
|
|
203
|
+
if (!pending) {
|
|
204
|
+
pending = { baseHead: readOptionalGitHead(repoDir), snapshots: {} };
|
|
205
|
+
pendingGitMutations.set(key, pending);
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
if (readOptionalGitHead(repoDir) !== pending.baseHead) {
|
|
209
|
+
throw new UsageError(`Git target "${source.name}" advanced before its exact-path mutation.`);
|
|
210
|
+
}
|
|
211
|
+
if (!Object.hasOwn(pending.snapshots, relativePath)) {
|
|
212
|
+
assertGitExactPathsClean(repoDir, [relativePath]);
|
|
213
|
+
return { key, created };
|
|
214
|
+
}
|
|
215
|
+
assertNoIgnoredExactPaths(repoDir, [relativePath]);
|
|
216
|
+
const index = runGit([
|
|
217
|
+
"--literal-pathspecs",
|
|
218
|
+
"-C",
|
|
219
|
+
repoDir,
|
|
220
|
+
"diff",
|
|
221
|
+
"--cached",
|
|
222
|
+
"--quiet",
|
|
223
|
+
pending.baseHead ?? EMPTY_GIT_TREE,
|
|
224
|
+
"--",
|
|
225
|
+
relativePath,
|
|
226
|
+
]);
|
|
227
|
+
if (index.status === 1) {
|
|
228
|
+
throw new UsageError(`Exact Git operation path has staged work: ${relativePath}. Commit, stash, or discard that path before retrying.`);
|
|
229
|
+
}
|
|
230
|
+
if (index.status !== 0) {
|
|
231
|
+
throw new Error(`Cannot inspect Git index for exact operation path ${relativePath}: ${index.stderr.trim()}`);
|
|
232
|
+
}
|
|
233
|
+
const current = captureGitPathSnapshot({ source, config: { type: "git" } }, filePath);
|
|
234
|
+
if (!sameGitPathState(current.state, pending.snapshots[relativePath] ?? null)) {
|
|
235
|
+
throw new UsageError(`Exact Git operation path changed after AKM wrote it: ${relativePath}. Commit, stash, or discard that path before retrying.`);
|
|
236
|
+
}
|
|
237
|
+
return { key, created };
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (created && Object.keys(pending.snapshots).length === 0)
|
|
241
|
+
pendingGitMutations.delete(key);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function discardEmptyGitPreflight(preflight) {
|
|
246
|
+
if (!preflight?.created)
|
|
247
|
+
return;
|
|
248
|
+
const pending = pendingGitMutations.get(preflight.key);
|
|
249
|
+
if (pending && Object.keys(pending.snapshots).length === 0)
|
|
250
|
+
pendingGitMutations.delete(preflight.key);
|
|
83
251
|
}
|
|
84
252
|
// ── Portability advisory (review 13, D1) ────────────────────────────────────
|
|
85
253
|
/**
|
|
@@ -157,10 +325,20 @@ export function assertWritableAllowedForKind(entry) {
|
|
|
157
325
|
export async function writeAssetToSource(source, config, ref, content) {
|
|
158
326
|
ensureWritable(source, config);
|
|
159
327
|
assertSupportedKind(source);
|
|
328
|
+
assertAkmAssetWrite(source);
|
|
160
329
|
const filePath = resolveAssetFilePath(source, ref);
|
|
161
|
-
|
|
162
|
-
const normalized =
|
|
163
|
-
|
|
330
|
+
const authored = filePath.toLowerCase().endsWith(".md") ? ensureAkmMarkdownType(content, ref.type) : content;
|
|
331
|
+
const normalized = authored.endsWith("\n") ? authored : `${authored}\n`;
|
|
332
|
+
const preflight = preflightGitPathMutation(source, filePath);
|
|
333
|
+
try {
|
|
334
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
335
|
+
fs.writeFileSync(filePath, normalized, "utf8");
|
|
336
|
+
recordWriteTargetPath(source, filePath);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
discardEmptyGitPreflight(preflight);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
164
342
|
// Non-fatal portability advisory (review 13, D1): flag absolute host home
|
|
165
343
|
// paths in the written content. These make the stash non-portable and leak
|
|
166
344
|
// the local username. We warn AFTER the write so the advisory never blocks it.
|
|
@@ -169,7 +347,7 @@ export async function writeAssetToSource(source, config, ref, content) {
|
|
|
169
347
|
warn(`warning: asset "${formatRefForMessage(ref)}" embeds absolute host path(s): ${hostPaths.join(", ")}. ` +
|
|
170
348
|
"These make the stash non-portable and leak the local username — prefer $HOME or ~ relative references.");
|
|
171
349
|
}
|
|
172
|
-
return { path: filePath, ref:
|
|
350
|
+
return { path: filePath, ref: displayRef({ type: ref.type, name: ref.name, bundleId: ref.origin }) };
|
|
173
351
|
}
|
|
174
352
|
/**
|
|
175
353
|
* Delete the asset at `ref` from `source`. Symmetric to
|
|
@@ -180,12 +358,21 @@ export async function writeAssetToSource(source, config, ref, content) {
|
|
|
180
358
|
export async function deleteAssetFromSource(source, config, ref) {
|
|
181
359
|
ensureWritable(source, config);
|
|
182
360
|
assertSupportedKind(source);
|
|
361
|
+
assertAkmAssetWrite(source);
|
|
183
362
|
const filePath = resolveAssetFilePath(source, ref);
|
|
184
363
|
if (!fs.existsSync(filePath)) {
|
|
185
364
|
throw new UsageError(`Asset "${formatRefForMessage(ref)}" not found in source "${source.name}" (expected at ${filePath}).`, "MISSING_REQUIRED_ARGUMENT");
|
|
186
365
|
}
|
|
187
|
-
|
|
188
|
-
|
|
366
|
+
const preflight = preflightGitPathMutation(source, filePath);
|
|
367
|
+
try {
|
|
368
|
+
fs.unlinkSync(filePath);
|
|
369
|
+
recordWriteTargetPath(source, filePath);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
discardEmptyGitPreflight(preflight);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
return { path: filePath, ref: displayRef({ type: ref.type, name: ref.name, bundleId: ref.origin }) };
|
|
189
376
|
}
|
|
190
377
|
/**
|
|
191
378
|
* Fire the one-shot batch-at-boundary commit for a resolved write target.
|
|
@@ -196,67 +383,542 @@ export async function deleteAssetFromSource(source, config, ref) {
|
|
|
196
383
|
* primary stash stay non-committing here — the primary stash is committed by
|
|
197
384
|
* the existing improve auto-sync boundary).
|
|
198
385
|
*
|
|
199
|
-
* For a git target it delegates to `saveGitStash(name, message, writable, …)
|
|
200
|
-
*
|
|
201
|
-
* and pushes when the target is writable, has a
|
|
386
|
+
* For a git target it delegates to `saveGitStash(name, message, writable, …)`
|
|
387
|
+
* with the exact paths recorded by the write/delete helpers (plus any explicit
|
|
388
|
+
* caller paths), commits once, and pushes when the target is writable, has a
|
|
389
|
+
* remote, and `push !== false`.
|
|
202
390
|
*
|
|
203
|
-
* The push intent honours a deprecated `options.pushOnCommit` on the source
|
|
204
|
-
* config (mapped onto the batch push gate) when `push` is not explicitly set.
|
|
205
391
|
*/
|
|
206
392
|
export function commitWriteTargetBoundary(target, message, options) {
|
|
207
393
|
if (target.source.kind !== "git")
|
|
208
394
|
return;
|
|
209
|
-
|
|
210
|
-
// Map the deprecated per-asset `pushOnCommit` intent onto the batch push gate
|
|
211
|
-
// when the caller did not pass an explicit push toggle. `saveGitStash` still
|
|
212
|
-
// gates the actual push on writable + remote, so this only ever opts *in*.
|
|
213
|
-
const push = options?.push ?? (target.config.options?.pushOnCommit === true ? true : undefined);
|
|
395
|
+
const push = options?.push;
|
|
214
396
|
const writable = resolveWritable(target.config);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
397
|
+
const repoDir = path.resolve(target.source.repoPath ?? target.source.path);
|
|
398
|
+
const key = gitTargetKey(target.source);
|
|
399
|
+
const pending = pendingGitMutations.get(key);
|
|
400
|
+
const expectedBaseHead = options?.expectedBaseHead !== undefined
|
|
401
|
+
? options.expectedBaseHead
|
|
402
|
+
: pending
|
|
403
|
+
? pending.baseHead
|
|
404
|
+
: readOptionalGitHead(repoDir);
|
|
405
|
+
const normalizeBoundaryPath = (filePath) => {
|
|
406
|
+
const relativePath = path.isAbsolute(filePath)
|
|
407
|
+
? path.relative(repoDir, path.resolve(filePath)).replaceAll(path.sep, "/")
|
|
408
|
+
: filePath.replaceAll(path.sep, "/");
|
|
409
|
+
return normalizePublicationPaths([relativePath])[0];
|
|
410
|
+
};
|
|
411
|
+
const providedSnapshots = new Map(Object.entries(options?.expectedSnapshots ?? {}).map(([filePath, state]) => [
|
|
412
|
+
normalizeBoundaryPath(filePath),
|
|
413
|
+
state,
|
|
414
|
+
]));
|
|
415
|
+
const paths = normalizePublicationPaths([
|
|
416
|
+
...(options?.paths ?? []).map(normalizeBoundaryPath),
|
|
417
|
+
...Object.keys(pending?.snapshots ?? {}),
|
|
418
|
+
]);
|
|
419
|
+
const expectedSnapshots = {};
|
|
420
|
+
for (const filePath of paths) {
|
|
421
|
+
if (providedSnapshots.has(filePath)) {
|
|
422
|
+
expectedSnapshots[filePath] = providedSnapshots.get(filePath) ?? null;
|
|
423
|
+
}
|
|
424
|
+
else if (pending && Object.hasOwn(pending.snapshots, filePath)) {
|
|
425
|
+
expectedSnapshots[filePath] = pending.snapshots[filePath] ?? null;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
expectedSnapshots[filePath] = captureGitPathSnapshot(target, path.join(repoDir, filePath)).state;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (options?.expectedBaseHead !== undefined &&
|
|
432
|
+
pending !== undefined &&
|
|
433
|
+
options.expectedBaseHead !== pending.baseHead) {
|
|
434
|
+
throw new Error(`Git boundary base does not match the recorded exact-path mutation base.`);
|
|
435
|
+
}
|
|
436
|
+
// Assets may live under <repo>/content, but git synchronization always runs
|
|
437
|
+
// against the repository root.
|
|
438
|
+
try {
|
|
439
|
+
saveGitStash(undefined, message, writable, {
|
|
440
|
+
repoDir,
|
|
441
|
+
paths,
|
|
442
|
+
expectedSnapshots,
|
|
443
|
+
expectedBaseHead,
|
|
444
|
+
...(push === undefined ? {} : { push }),
|
|
445
|
+
...(options?.transactionId === undefined ? {} : { transactionId: options.transactionId }),
|
|
446
|
+
});
|
|
447
|
+
pendingGitMutations.delete(key);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
const currentHead = readOptionalGitHead(repoDir);
|
|
451
|
+
if (pending && currentHead !== pending.baseHead)
|
|
452
|
+
pendingGitMutations.delete(key);
|
|
453
|
+
if (error instanceof GitStashPushError) {
|
|
454
|
+
throw new Error(`Changes were committed as ${error.commit}, but publication failed: ${error.message}`, {
|
|
455
|
+
cause: error,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function sameGitPathState(left, right) {
|
|
462
|
+
return left === null ? right === null : right !== null && left.oid === right.oid && left.mode === right.mode;
|
|
463
|
+
}
|
|
464
|
+
/** Capture the exact checkout/upstream identity before a durable mutation starts. */
|
|
465
|
+
export function captureGitPublication(target) {
|
|
466
|
+
if (target.source.kind !== "git")
|
|
467
|
+
return undefined;
|
|
468
|
+
const identity = readGitPublicationIdentity(target);
|
|
469
|
+
const head = runGit(["-C", identity.repoPath, "rev-parse", "HEAD"]);
|
|
470
|
+
if (head.status !== 0 || !head.stdout.trim()) {
|
|
471
|
+
throw new Error(`Cannot read Git HEAD for target "${target.source.name}".`);
|
|
472
|
+
}
|
|
473
|
+
const baseHead = head.stdout.trim();
|
|
474
|
+
let upstreamHead;
|
|
475
|
+
if (identity.upstream) {
|
|
476
|
+
const upstream = runGit(["-C", identity.repoPath, "rev-parse", identity.upstream]);
|
|
477
|
+
if (upstream.status !== 0 || !upstream.stdout.trim()) {
|
|
478
|
+
throw new Error(`Cannot read Git upstream for target "${target.source.name}".`);
|
|
479
|
+
}
|
|
480
|
+
upstreamHead = upstream.stdout.trim();
|
|
481
|
+
if (baseHead !== upstreamHead) {
|
|
482
|
+
throw new Error(`Writable Git target "${target.source.name}" changed after mutation preflight.`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return { ...identity, baseHead, ...(upstreamHead ? { upstreamHead } : {}) };
|
|
486
|
+
}
|
|
487
|
+
/** Capture one repo-relative path exactly as Git will stage it. */
|
|
488
|
+
export function captureGitPathSnapshot(target, filePath) {
|
|
489
|
+
if (target.source.kind !== "git")
|
|
490
|
+
throw new Error(`Target "${target.source.name}" is not Git-backed.`);
|
|
491
|
+
const repoPath = path.resolve(target.source.repoPath ?? target.source.path);
|
|
492
|
+
const absolutePath = path.resolve(filePath);
|
|
493
|
+
const relativePath = path.relative(repoPath, absolutePath).replaceAll(path.sep, "/");
|
|
494
|
+
const [normalizedPath] = normalizePublicationPaths([relativePath]);
|
|
495
|
+
return {
|
|
496
|
+
path: normalizedPath,
|
|
497
|
+
state: captureGitPathState(repoPath, absolutePath, normalizedPath),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function captureGitPathState(repoPath, absolutePath, relativePath) {
|
|
501
|
+
let stat;
|
|
502
|
+
try {
|
|
503
|
+
stat = fs.lstatSync(absolutePath);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (error.code === "ENOENT")
|
|
507
|
+
return null;
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
let mode;
|
|
511
|
+
let oidResult;
|
|
512
|
+
if (stat.isSymbolicLink()) {
|
|
513
|
+
mode = "120000";
|
|
514
|
+
oidResult = runGit(["-C", repoPath, "hash-object", "--stdin"], { input: fs.readlinkSync(absolutePath) });
|
|
515
|
+
}
|
|
516
|
+
else if (stat.isFile()) {
|
|
517
|
+
mode = stat.mode & 0o111 ? "100755" : "100644";
|
|
518
|
+
oidResult = runGit(["-C", repoPath, "hash-object", `--path=${relativePath}`, "--", absolutePath]);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
throw new Error(`Git publication path is not a file: ${relativePath}`);
|
|
522
|
+
}
|
|
523
|
+
if (oidResult.status !== 0 || !oidResult.stdout.trim()) {
|
|
524
|
+
throw new Error(`Cannot snapshot Git publication path: ${relativePath}`);
|
|
525
|
+
}
|
|
526
|
+
return { oid: oidResult.stdout.trim(), mode };
|
|
527
|
+
}
|
|
528
|
+
/** Create or recover the one transaction-owned commit without pushing it. */
|
|
529
|
+
export function ensureGitTransactionCommit(target, publication, options) {
|
|
530
|
+
assertGitPublicationIdentity(target, publication);
|
|
531
|
+
const paths = normalizePublicationPaths(options.paths);
|
|
532
|
+
validateGitWorktreeSnapshots(target, paths, options.snapshots);
|
|
533
|
+
if (publication.commit !== undefined) {
|
|
534
|
+
if (publication.commit !== null) {
|
|
535
|
+
validateGitTransactionCommit(target, publication, publication.commit, options.transactionId, paths, options.snapshots);
|
|
536
|
+
reconcileGitExactPathIndex(publication.repoPath, publication.baseHead, publication.commit, paths);
|
|
537
|
+
}
|
|
538
|
+
return publication.commit;
|
|
539
|
+
}
|
|
540
|
+
const existing = findGitTransactionCommit(publication.repoPath, publication.baseHead, options.transactionId);
|
|
541
|
+
if (existing) {
|
|
542
|
+
validateGitTransactionCommit(target, publication, existing, options.transactionId, paths, options.snapshots);
|
|
543
|
+
reconcileGitExactPathIndex(publication.repoPath, publication.baseHead, existing, paths);
|
|
544
|
+
return existing;
|
|
545
|
+
}
|
|
546
|
+
const head = readGitHead(publication.repoPath, target.source.name);
|
|
547
|
+
if (head !== publication.baseHead) {
|
|
548
|
+
throw new Error(`Cannot publish Git transaction ${options.transactionId}: target "${target.source.name}" advanced before its commit was recorded.`);
|
|
549
|
+
}
|
|
550
|
+
commitWriteTargetBoundary(target, options.message, {
|
|
551
|
+
paths,
|
|
552
|
+
push: false,
|
|
553
|
+
transactionId: options.transactionId,
|
|
554
|
+
expectedBaseHead: publication.baseHead,
|
|
555
|
+
expectedSnapshots: options.snapshots,
|
|
222
556
|
});
|
|
557
|
+
const committed = findGitTransactionCommit(publication.repoPath, publication.baseHead, options.transactionId);
|
|
558
|
+
if (!committed) {
|
|
559
|
+
if (readGitHead(publication.repoPath, target.source.name) === publication.baseHead) {
|
|
560
|
+
validateGitCommitSnapshots(publication.repoPath, publication.baseHead, paths, options.snapshots);
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
throw new Error(`Cannot identify the Git commit for transaction ${options.transactionId}.`);
|
|
564
|
+
}
|
|
565
|
+
validateGitTransactionCommit(target, publication, committed, options.transactionId, paths, options.snapshots);
|
|
566
|
+
reconcileGitExactPathIndex(publication.repoPath, publication.baseHead, committed, paths);
|
|
567
|
+
return committed;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Kind-neutral snapshot capture for one mutated path.
|
|
571
|
+
*
|
|
572
|
+
* Returns `undefined` for kinds with no publication model, so command layers
|
|
573
|
+
* never branch on `source.kind` themselves — the same fail-soft contract
|
|
574
|
+
* {@link captureGitPublication} and {@link commitWriteTargetBoundary} already
|
|
575
|
+
* use. `captureGitPathSnapshot` throws for non-git targets, which is what
|
|
576
|
+
* forced callers to guard; this absorbs that guard.
|
|
577
|
+
*/
|
|
578
|
+
export function captureWriteTargetPathSnapshot(target, filePath) {
|
|
579
|
+
if (target.source.kind !== "git")
|
|
580
|
+
return undefined;
|
|
581
|
+
return captureGitPathSnapshot(target, filePath);
|
|
223
582
|
}
|
|
224
583
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
584
|
+
* Kind-neutral commit + publish for one transaction boundary.
|
|
585
|
+
*
|
|
586
|
+
* A no-op (returns `undefined`) for kinds with no publication model. For a
|
|
587
|
+
* publication-backed target this absorbs BOTH the kind test and the
|
|
588
|
+
* "transaction lacks durable publication identity" invariant, so callers hold
|
|
589
|
+
* no provider knowledge. `onCommitRecorded` fires between the ensure and the
|
|
590
|
+
* push, letting a caller persist the commit and advance its own journal phase
|
|
591
|
+
* without inspecting the target.
|
|
592
|
+
*
|
|
593
|
+
* `missingPublicationError` lets a caller keep its own error CLASS for the
|
|
594
|
+
* missing-identity case. The two call sites disagreed historically —
|
|
595
|
+
* consolidate threw `ConfigError` (exit 78), proposal a plain `Error`
|
|
596
|
+
* (exit 70) — and collapsing them here would silently change one command's
|
|
597
|
+
* exit code. The guard moves; the classification stays with the caller.
|
|
229
598
|
*/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
599
|
+
export function publishWriteTargetTransaction(target, publication, options) {
|
|
600
|
+
if (target.source.kind !== "git")
|
|
601
|
+
return undefined;
|
|
602
|
+
if (!publication) {
|
|
603
|
+
throw (options.missingPublicationError?.(target.source.name) ??
|
|
604
|
+
new Error(`Proposal transaction ${options.transactionId} has no Git publication identity.`));
|
|
605
|
+
}
|
|
606
|
+
const commit = ensureGitTransactionCommit(target, publication, {
|
|
607
|
+
transactionId: options.transactionId,
|
|
608
|
+
message: options.message,
|
|
609
|
+
paths: options.paths,
|
|
610
|
+
snapshots: options.snapshots,
|
|
611
|
+
});
|
|
612
|
+
options.onCommitRecorded?.(commit);
|
|
613
|
+
publishGitTransactionCommit(target, publication, options.transactionId, options.paths, options.snapshots);
|
|
614
|
+
return { commit };
|
|
615
|
+
}
|
|
616
|
+
/** Push only the recorded transaction commit, never later local descendants. */
|
|
617
|
+
export function publishGitTransactionCommit(target, publication, transactionId, paths, snapshots) {
|
|
618
|
+
assertGitPublicationIdentity(target, publication);
|
|
619
|
+
if (publication.commit === undefined) {
|
|
620
|
+
throw new Error(`Git transaction ${transactionId} has no recorded publication decision.`);
|
|
621
|
+
}
|
|
622
|
+
if (publication.commit === null)
|
|
623
|
+
return;
|
|
624
|
+
validateGitTransactionCommit(target, publication, publication.commit, transactionId, normalizePublicationPaths(paths), snapshots);
|
|
625
|
+
if (!publication.remote || !publication.mergeRef)
|
|
233
626
|
return;
|
|
234
|
-
if (
|
|
627
|
+
if (!publication.upstream)
|
|
628
|
+
throw new Error(`Git transaction ${transactionId} has no recorded upstream.`);
|
|
629
|
+
if (runGit(["-C", publication.repoPath, "merge-base", "--is-ancestor", publication.commit, publication.upstream])
|
|
630
|
+
.status === 0) {
|
|
235
631
|
return;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
632
|
+
}
|
|
633
|
+
if (runGit(["-C", publication.repoPath, "merge-base", "--is-ancestor", publication.upstream, publication.commit])
|
|
634
|
+
.status !== 0) {
|
|
635
|
+
throw new Error(`Cannot publish Git transaction ${transactionId}: upstream history diverged.`);
|
|
636
|
+
}
|
|
637
|
+
if (!publication.upstreamHead) {
|
|
638
|
+
throw new Error(`Git transaction ${transactionId} has no recorded upstream lease.`);
|
|
639
|
+
}
|
|
640
|
+
const pushed = runGit([
|
|
641
|
+
"-C",
|
|
642
|
+
publication.repoPath,
|
|
643
|
+
"push",
|
|
644
|
+
`--force-with-lease=${publication.mergeRef}:${publication.upstreamHead}`,
|
|
645
|
+
publication.remote,
|
|
646
|
+
`${publication.commit}:${publication.mergeRef}`,
|
|
647
|
+
], { timeout: GIT_PUSH_TIMEOUT_MS });
|
|
648
|
+
if (pushed.status !== 0) {
|
|
649
|
+
throw new Error(`git push failed for target "${target.source.name}": ${pushed.stderr.trim()}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function readGitPublicationIdentity(target) {
|
|
653
|
+
const repoPath = path.resolve(target.source.repoPath ?? target.source.path);
|
|
654
|
+
const branchResult = runGit(["-C", repoPath, "symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
655
|
+
const branch = branchResult.status === 0 ? branchResult.stdout.trim() : undefined;
|
|
656
|
+
const remotes = runGit(["-C", repoPath, "remote"]);
|
|
657
|
+
if (remotes.status !== 0)
|
|
658
|
+
throw new Error(`Cannot inspect Git remotes for target "${target.source.name}".`);
|
|
659
|
+
if (!remotes.stdout.trim())
|
|
660
|
+
return { repoPath, ...(branch ? { branch } : {}) };
|
|
661
|
+
if (!branch)
|
|
662
|
+
throw new Error(`Writable Git target "${target.source.name}" is detached from a branch.`);
|
|
663
|
+
const remoteResult = runGit(["-C", repoPath, "config", "--get", `branch.${branch}.remote`]);
|
|
664
|
+
const mergeResult = runGit(["-C", repoPath, "config", "--get", `branch.${branch}.merge`]);
|
|
665
|
+
if (remoteResult.status !== 0 || mergeResult.status !== 0) {
|
|
666
|
+
throw new Error(`Writable Git target "${target.source.name}" has no configured upstream branch.`);
|
|
667
|
+
}
|
|
668
|
+
const remote = remoteResult.stdout.trim();
|
|
669
|
+
const mergeRef = mergeResult.stdout.trim();
|
|
670
|
+
const upstreamResult = runGit(["-C", repoPath, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
671
|
+
if (upstreamResult.status !== 0 || !upstreamResult.stdout.trim()) {
|
|
672
|
+
throw new Error(`Cannot read Git upstream for target "${target.source.name}".`);
|
|
673
|
+
}
|
|
674
|
+
const urlResult = runGit(["-C", repoPath, "remote", "get-url", remote]);
|
|
675
|
+
if (urlResult.status !== 0 || !urlResult.stdout.trim()) {
|
|
676
|
+
throw new Error(`Cannot read Git remote "${remote}" for target "${target.source.name}".`);
|
|
677
|
+
}
|
|
678
|
+
const pushUrlsResult = runGit(["-C", repoPath, "remote", "get-url", "--push", "--all", remote]);
|
|
679
|
+
if (pushUrlsResult.status !== 0 || !pushUrlsResult.stdout.trim()) {
|
|
680
|
+
throw new Error(`Cannot read Git push URL for target "${target.source.name}".`);
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
repoPath,
|
|
684
|
+
branch,
|
|
685
|
+
remote,
|
|
686
|
+
mergeRef,
|
|
687
|
+
remoteUrl: urlResult.stdout.trim(),
|
|
688
|
+
pushUrls: pushUrlsResult.stdout
|
|
689
|
+
.split("\n")
|
|
690
|
+
.map((value) => value.trim())
|
|
691
|
+
.filter(Boolean),
|
|
692
|
+
upstream: upstreamResult.stdout.trim(),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function assertGitPublicationIdentity(target, publication) {
|
|
696
|
+
if (target.source.kind !== "git") {
|
|
697
|
+
throw new Error(`Git publication is bound to non-Git target "${target.source.name}".`);
|
|
698
|
+
}
|
|
699
|
+
const current = readGitPublicationIdentity(target);
|
|
700
|
+
for (const key of ["repoPath", "branch", "remote", "mergeRef", "remoteUrl", "upstream"]) {
|
|
701
|
+
if (current[key] !== publication[key]) {
|
|
702
|
+
throw new Error(`Git publication target identity changed at ${key} for "${target.source.name}".`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (JSON.stringify(current.pushUrls ?? []) !== JSON.stringify(publication.pushUrls ?? [])) {
|
|
706
|
+
throw new Error(`Git publication target identity changed at pushUrls for "${target.source.name}".`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function normalizePublicationPaths(paths) {
|
|
710
|
+
const normalized = new Set();
|
|
711
|
+
for (const filePath of paths) {
|
|
712
|
+
const candidate = filePath.replaceAll(path.sep, "/");
|
|
713
|
+
if (!candidate ||
|
|
714
|
+
candidate.includes("\0") ||
|
|
715
|
+
path.isAbsolute(filePath) ||
|
|
716
|
+
path.posix.isAbsolute(candidate) ||
|
|
717
|
+
candidate === "." ||
|
|
718
|
+
candidate === ".." ||
|
|
719
|
+
candidate.startsWith("../") ||
|
|
720
|
+
path.posix.normalize(candidate) !== candidate) {
|
|
721
|
+
throw new Error("Git publication contains an unsafe path.");
|
|
722
|
+
}
|
|
723
|
+
normalized.add(candidate);
|
|
724
|
+
}
|
|
725
|
+
return [...normalized];
|
|
726
|
+
}
|
|
727
|
+
function readGitHead(repoPath, targetName) {
|
|
728
|
+
const result = runGit(["-C", repoPath, "rev-parse", "HEAD"]);
|
|
729
|
+
if (result.status !== 0 || !result.stdout.trim())
|
|
730
|
+
throw new Error(`Cannot read Git HEAD for target "${targetName}".`);
|
|
731
|
+
return result.stdout.trim();
|
|
732
|
+
}
|
|
733
|
+
function findGitTransactionCommit(repoPath, baseHead, transactionId) {
|
|
734
|
+
const result = runGit([
|
|
735
|
+
"-C",
|
|
736
|
+
repoPath,
|
|
737
|
+
"log",
|
|
738
|
+
"--format=%H",
|
|
739
|
+
"--fixed-strings",
|
|
740
|
+
`--grep=AKM-Transaction: ${transactionId}`,
|
|
741
|
+
`${baseHead}..HEAD`,
|
|
742
|
+
]);
|
|
743
|
+
if (result.status !== 0)
|
|
744
|
+
throw new Error(`Cannot inspect Git transaction ${transactionId}.`);
|
|
745
|
+
const matches = result.stdout
|
|
746
|
+
.split("\n")
|
|
747
|
+
.map((value) => value.trim())
|
|
748
|
+
.filter(Boolean);
|
|
749
|
+
if (matches.length > 1)
|
|
750
|
+
throw new Error(`Git transaction ${transactionId} has multiple candidate commits.`);
|
|
751
|
+
return matches[0];
|
|
752
|
+
}
|
|
753
|
+
function validateGitTransactionCommit(target, publication, commit, transactionId, expectedPaths, snapshots) {
|
|
754
|
+
const parent = runGit(["-C", publication.repoPath, "rev-list", "--parents", "-n", "1", commit]);
|
|
755
|
+
const ancestry = parent.stdout.trim().split(/\s+/);
|
|
756
|
+
if (parent.status !== 0 || ancestry.length !== 2 || ancestry[1] !== publication.baseHead) {
|
|
757
|
+
throw new Error(`Git commit ${commit} is not the direct transaction commit for "${target.source.name}".`);
|
|
758
|
+
}
|
|
759
|
+
const message = runGit(["-C", publication.repoPath, "show", "-s", "--format=%B", commit]);
|
|
760
|
+
if (message.status !== 0 ||
|
|
761
|
+
!message.stdout.split(/\r?\n/).some((line) => line.trim() === `AKM-Transaction: ${transactionId}`)) {
|
|
762
|
+
throw new Error(`Git commit ${commit} is not owned by transaction ${transactionId}.`);
|
|
763
|
+
}
|
|
764
|
+
const changed = runGit([
|
|
765
|
+
"-C",
|
|
766
|
+
publication.repoPath,
|
|
767
|
+
"diff-tree",
|
|
768
|
+
"--no-commit-id",
|
|
769
|
+
"--name-only",
|
|
770
|
+
"-z",
|
|
771
|
+
"--no-renames",
|
|
772
|
+
"-r",
|
|
773
|
+
commit,
|
|
774
|
+
]);
|
|
775
|
+
const expected = new Set(expectedPaths);
|
|
776
|
+
const changedPaths = changed.stdout.split("\0").filter(Boolean);
|
|
777
|
+
if (changed.status !== 0 || changedPaths.length === 0 || changedPaths.some((filePath) => !expected.has(filePath))) {
|
|
778
|
+
throw new Error(`Git commit ${commit} contains paths outside transaction ${transactionId}.`);
|
|
779
|
+
}
|
|
780
|
+
validateGitCommitSnapshots(publication.repoPath, commit, expectedPaths, snapshots);
|
|
781
|
+
if (runGit(["-C", publication.repoPath, "merge-base", "--is-ancestor", commit, "HEAD"]).status !== 0) {
|
|
782
|
+
throw new Error(`Git commit ${commit} is no longer on the target branch.`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function validateGitWorktreeSnapshots(target, expectedPaths, snapshots) {
|
|
786
|
+
for (const expectedPath of expectedPaths) {
|
|
787
|
+
if (!Object.hasOwn(snapshots, expectedPath)) {
|
|
788
|
+
throw new Error(`Git publication lacks a snapshot for ${expectedPath}.`);
|
|
789
|
+
}
|
|
790
|
+
const current = captureGitPathSnapshot(target, path.join(target.source.repoPath ?? target.source.path, expectedPath));
|
|
791
|
+
if (!sameGitPathState(current.state, snapshots[expectedPath] ?? null)) {
|
|
792
|
+
throw new Error(`Git publication path diverged after mutation: ${expectedPath}.`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function validateGitCommitSnapshots(repoPath, commit, expectedPaths, snapshots) {
|
|
797
|
+
for (const expectedPath of expectedPaths) {
|
|
798
|
+
if (!Object.hasOwn(snapshots, expectedPath)) {
|
|
799
|
+
throw new Error(`Git publication lacks a snapshot for ${expectedPath}.`);
|
|
800
|
+
}
|
|
801
|
+
const expected = snapshots[expectedPath];
|
|
802
|
+
const tree = runGit(["-C", repoPath, "ls-tree", commit, "--", expectedPath]);
|
|
803
|
+
if (tree.status !== 0)
|
|
804
|
+
throw new Error(`Cannot inspect committed Git path: ${expectedPath}.`);
|
|
805
|
+
const match = tree.stdout.trim().match(/^(\d+)\s+blob\s+([0-9a-f]+)\t/);
|
|
806
|
+
if (expected === null) {
|
|
807
|
+
if (tree.stdout.trim())
|
|
808
|
+
throw new Error(`Git transaction unexpectedly retained ${expectedPath}.`);
|
|
809
|
+
}
|
|
810
|
+
else if (expected === undefined || !match || match[1] !== expected.mode || match[2] !== expected.oid) {
|
|
811
|
+
throw new Error(`Git transaction committed unexpected content for ${expectedPath}.`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Validate and normalize a write target before a command mutates it.
|
|
817
|
+
*
|
|
818
|
+
* Git bundle locks record the materialized content root, which can be a
|
|
819
|
+
* subdirectory of the checkout. Resolve the actual repository boundary from
|
|
820
|
+
* that root so scoped commits use repository-relative paths. Extracted or
|
|
821
|
+
* unmaterialized Git caches are not writable checkouts and must fail before a
|
|
822
|
+
* command writes files into them.
|
|
823
|
+
*/
|
|
824
|
+
export function prepareWriteTargetForMutation(target, options = {}) {
|
|
825
|
+
assertAkmAssetWrite(target.source, options.allowedAdapters);
|
|
826
|
+
if (target.source.kind !== "git")
|
|
827
|
+
return target;
|
|
828
|
+
const contentRoot = path.resolve(target.source.path);
|
|
829
|
+
let stat;
|
|
830
|
+
try {
|
|
831
|
+
stat = fs.statSync(contentRoot);
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
throw gitTargetNotMaterialized(target, contentRoot);
|
|
835
|
+
}
|
|
836
|
+
if (!stat.isDirectory())
|
|
837
|
+
throw gitTargetNotMaterialized(target, contentRoot);
|
|
838
|
+
const rootResult = runGit(["-C", contentRoot, "rev-parse", "--show-toplevel"]);
|
|
839
|
+
if (rootResult.status !== 0 || !rootResult.stdout.trim()) {
|
|
840
|
+
throw gitTargetNotMaterialized(target, contentRoot);
|
|
841
|
+
}
|
|
842
|
+
const repoPath = path.resolve(rootResult.stdout.trim());
|
|
843
|
+
const gitDirResult = runGit(["-C", repoPath, "rev-parse", "--git-dir"]);
|
|
844
|
+
if (gitDirResult.status !== 0 || !gitDirResult.stdout.trim()) {
|
|
845
|
+
throw gitTargetNotMaterialized(target, contentRoot);
|
|
846
|
+
}
|
|
847
|
+
const gitDir = path.resolve(repoPath, gitDirResult.stdout.trim());
|
|
848
|
+
let realContentRoot;
|
|
849
|
+
let realRepoPath;
|
|
850
|
+
try {
|
|
851
|
+
realContentRoot = fs.realpathSync(contentRoot);
|
|
852
|
+
realRepoPath = fs.realpathSync(repoPath);
|
|
853
|
+
fs.accessSync(realContentRoot, fs.constants.W_OK);
|
|
854
|
+
fs.accessSync(gitDir, fs.constants.W_OK);
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
throw new ConfigError(`Writable Git target "${target.source.name}" is not writable at ${contentRoot}.`, "INVALID_CONFIG_FILE", `Fix the checkout permissions or choose a different --target.`);
|
|
858
|
+
}
|
|
859
|
+
if (!isWithin(realContentRoot, realRepoPath)) {
|
|
860
|
+
throw new ConfigError(`Writable Git target "${target.source.name}" resolves outside its checkout: ${contentRoot}.`, "INVALID_CONFIG_FILE");
|
|
861
|
+
}
|
|
862
|
+
const statusResult = runGit(["-C", repoPath, "status", "--porcelain"]);
|
|
863
|
+
if (statusResult.status !== 0 || statusResult.error) {
|
|
864
|
+
throw gitTargetNotMaterialized(target, contentRoot);
|
|
865
|
+
}
|
|
866
|
+
const branchResult = runGit(["-C", repoPath, "symbolic-ref", "--quiet", "HEAD"]);
|
|
867
|
+
if (branchResult.status !== 0 || !branchResult.stdout.trim()) {
|
|
868
|
+
throw new UsageError(`Writable Git target "${target.source.name}" is detached from a branch.`, "INVALID_FLAG_VALUE");
|
|
869
|
+
}
|
|
870
|
+
const upstream = inspectGitUpstream(repoPath);
|
|
871
|
+
if (upstream.behind > 0) {
|
|
872
|
+
throw new UsageError(`Writable Git target "${target.source.name}" is behind ${upstream.upstream}; run \`akm bundle update ${target.source.name}\` before writing.`, "INVALID_FLAG_VALUE");
|
|
873
|
+
}
|
|
874
|
+
if (upstream.ahead > 0 && options.allowAhead !== true) {
|
|
875
|
+
throw new UsageError(`Writable Git target "${target.source.name}" has unpushed commits; push or reconcile them before AKM writes another commit.`, "INVALID_FLAG_VALUE");
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
...target,
|
|
879
|
+
source: { ...target.source, path: contentRoot, repoPath },
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function gitTargetNotMaterialized(target, contentRoot) {
|
|
883
|
+
return new ConfigError(`Writable Git target "${target.source.name}" is not materialized as a Git checkout at ${contentRoot}; refusing to write without a commit boundary.`, "INVALID_CONFIG_FILE", `Run \`akm bundle update ${target.source.name}\` to materialize it, or point the bundle at a writable Git checkout.`);
|
|
884
|
+
}
|
|
885
|
+
/** Enumerate enabled writable targets, deduplicated by materialized content root. */
|
|
886
|
+
export function resolveWritableTargets(akmConfig) {
|
|
887
|
+
const byRoot = new Map();
|
|
888
|
+
for (const runtime of resolveConfiguredSources(akmConfig)) {
|
|
889
|
+
if (runtime.enabled === false || !resolveWritable({ type: runtime.type, writable: runtime.writable }))
|
|
890
|
+
continue;
|
|
891
|
+
const target = adaptConfiguredSource(runtime);
|
|
892
|
+
const root = path.resolve(target.source.path);
|
|
893
|
+
const existing = byRoot.get(root);
|
|
894
|
+
if (!existing || target.source.name === akmConfig.defaultWriteTarget)
|
|
895
|
+
byRoot.set(root, target);
|
|
896
|
+
}
|
|
897
|
+
if (process.env.AKM_BUNDLE_DIR?.trim()) {
|
|
898
|
+
const target = resolveWorkingStashTarget(akmConfig);
|
|
899
|
+
byRoot.set(path.resolve(target.source.path), target);
|
|
900
|
+
}
|
|
901
|
+
return [...byRoot.values()];
|
|
241
902
|
}
|
|
242
903
|
/**
|
|
243
904
|
* Resolve the destination for a write per locked decision 3:
|
|
244
905
|
*
|
|
245
906
|
* 1. Explicit `--target <name>` (when supplied)
|
|
246
907
|
* 2. `config.defaultWriteTarget`
|
|
247
|
-
* 3. `config.
|
|
248
|
-
* 4. `ConfigError("no writable source configured; run `akm
|
|
908
|
+
* 3. `config.defaultBundle`'s path (the working stash created by `akm bundle create`)
|
|
909
|
+
* 4. `ConfigError("no writable source configured; run `akm bundle create`")`
|
|
249
910
|
*
|
|
250
911
|
* The legacy `first-writable-in-source-array-order` fallback is *not* used —
|
|
251
912
|
* see plan §6 decision 3 for the rationale.
|
|
252
913
|
*/
|
|
253
|
-
export function resolveWriteTarget(akmConfig, explicitTarget) {
|
|
914
|
+
export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
|
|
254
915
|
const configuredSources = resolveConfiguredSources(akmConfig);
|
|
916
|
+
const requireWritable = options.requireWritable !== false;
|
|
255
917
|
// 1. Explicit --target wins.
|
|
256
918
|
if (explicitTarget) {
|
|
257
919
|
const match = configuredSources.find((s) => s.name === explicitTarget);
|
|
258
920
|
if (!match) {
|
|
259
|
-
throw new UsageError(`--target must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
921
|
+
throw new UsageError(`--target must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
260
922
|
}
|
|
261
923
|
// Up-front writable check so an explicit --target fails fast with a
|
|
262
924
|
// ConfigError (rather than the generic UsageError ensureWritable would
|
|
@@ -264,7 +926,7 @@ export function resolveWriteTarget(akmConfig, explicitTarget) {
|
|
|
264
926
|
// effective writable flag (filesystem defaults to true; everything else
|
|
265
927
|
// defaults to false) so unset values are interpreted correctly.
|
|
266
928
|
const effectiveWritable = resolveWritable({ type: match.type, writable: match.writable });
|
|
267
|
-
if (!effectiveWritable) {
|
|
929
|
+
if (requireWritable && !effectiveWritable) {
|
|
268
930
|
throw new ConfigError(`source ${explicitTarget} is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${explicitTarget}" source in your config, or pass --target to a different source.`);
|
|
269
931
|
}
|
|
270
932
|
return adaptConfiguredSource(match);
|
|
@@ -279,34 +941,51 @@ export function resolveWriteTarget(akmConfig, explicitTarget) {
|
|
|
279
941
|
// ConfigError, rather than surfacing as a generic UsageError after
|
|
280
942
|
// path-building has already begun.
|
|
281
943
|
const effectiveWritable = resolveWritable({ type: match.type, writable: match.writable });
|
|
282
|
-
if (!effectiveWritable) {
|
|
944
|
+
if (requireWritable && !effectiveWritable) {
|
|
283
945
|
throw new ConfigError(`defaultWriteTarget "${akmConfig.defaultWriteTarget}" is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${akmConfig.defaultWriteTarget}" source in your config, or change \`defaultWriteTarget\` to a writable source.`);
|
|
284
946
|
}
|
|
285
947
|
return adaptConfiguredSource(match);
|
|
286
948
|
}
|
|
287
949
|
// Fall through if the named target no longer exists — surface a clear error.
|
|
288
|
-
throw new ConfigError(`defaultWriteTarget "${akmConfig.defaultWriteTarget}" does not match any configured source.`, "INVALID_CONFIG_FILE", "Update `defaultWriteTarget` in your config (run `akm config get defaultWriteTarget`) or run `akm list` to see configured sources.");
|
|
289
|
-
}
|
|
290
|
-
// 3.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
950
|
+
throw new ConfigError(`defaultWriteTarget "${akmConfig.defaultWriteTarget}" does not match any configured source.`, "INVALID_CONFIG_FILE", "Update `defaultWriteTarget` in your config (run `akm config get defaultWriteTarget`) or run `akm bundle list` to see configured sources.");
|
|
951
|
+
}
|
|
952
|
+
// 3. Configured default bundle.
|
|
953
|
+
return resolveWorkingStashTarget(akmConfig, options);
|
|
954
|
+
}
|
|
955
|
+
/** Resolve the implicit working stash without consulting `defaultWriteTarget`. */
|
|
956
|
+
export function resolveWorkingStashTarget(akmConfig, options = {}) {
|
|
957
|
+
const configuredSources = resolveConfiguredSources(akmConfig);
|
|
958
|
+
const requireWritable = options.requireWritable !== false;
|
|
959
|
+
if (process.env.AKM_BUNDLE_DIR?.trim()) {
|
|
960
|
+
const stashDir = resolveStashDir();
|
|
961
|
+
const configured = configuredSources.find((source) => {
|
|
962
|
+
const sourcePath = source.source.type === "filesystem" ? source.source.path : undefined;
|
|
963
|
+
return sourcePath !== undefined && path.resolve(sourcePath) === path.resolve(stashDir);
|
|
964
|
+
});
|
|
965
|
+
if (configured) {
|
|
966
|
+
const target = adaptConfiguredSource(configured);
|
|
967
|
+
if (requireWritable && !resolveWritable(target.config)) {
|
|
968
|
+
throw new ConfigError(`Bundle "${configured.name}" is not writable.`, "INVALID_CONFIG_FILE");
|
|
969
|
+
}
|
|
970
|
+
return { ...target, selector: undefined };
|
|
971
|
+
}
|
|
972
|
+
const bundleId = deriveBundleId(undefined, stashDir, new Set(Object.keys(akmConfig.bundles ?? {})));
|
|
300
973
|
return {
|
|
301
|
-
source: { kind: "filesystem", name:
|
|
302
|
-
config: { type: "filesystem",
|
|
974
|
+
source: { kind: "filesystem", name: bundleId, path: stashDir, adapterId: detectAdapterId(stashDir) },
|
|
975
|
+
config: { type: "filesystem", name: bundleId, path: stashDir, writable: true },
|
|
303
976
|
};
|
|
304
977
|
}
|
|
305
|
-
|
|
306
|
-
|
|
978
|
+
const defaultBundleSource = akmConfig.defaultBundle
|
|
979
|
+
? configuredSources.find((source) => source.name === akmConfig.defaultBundle)
|
|
980
|
+
: undefined;
|
|
981
|
+
if (!defaultBundleSource || !akmConfig.defaultBundle) {
|
|
982
|
+
throw new ConfigError("No default bundle is configured.", "INVALID_CONFIG_FILE");
|
|
983
|
+
}
|
|
984
|
+
const target = adaptConfiguredSource(defaultBundleSource);
|
|
985
|
+
if (requireWritable && !resolveWritable(target.config)) {
|
|
986
|
+
throw new ConfigError(`defaultBundle "${akmConfig.defaultBundle}" is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${akmConfig.defaultBundle}" bundle, or set \`defaultWriteTarget\` to a writable source.`);
|
|
307
987
|
}
|
|
308
|
-
|
|
309
|
-
throw new ConfigError("no writable source configured; run `akm init`", "STASH_DIR_NOT_FOUND", "Run `akm init` to create a working stash, or set `defaultWriteTarget` in your config.");
|
|
988
|
+
return { ...target, selector: undefined };
|
|
310
989
|
}
|
|
311
990
|
// ── Internals ───────────────────────────────────────────────────────────────
|
|
312
991
|
function ensureWritable(source, config) {
|
|
@@ -318,17 +997,26 @@ function ensureWritable(source, config) {
|
|
|
318
997
|
}
|
|
319
998
|
}
|
|
320
999
|
function resolveAssetFilePath(source, ref) {
|
|
321
|
-
const
|
|
1000
|
+
const basename = path.posix.basename(ref.name.replaceAll("\\", "/")).replace(/\.md$/i, "").toLowerCase();
|
|
1001
|
+
if (basename === "index" || basename === "log") {
|
|
1002
|
+
throw new UsageError(`Reserved concept name "${basename}" cannot be written.`, "INVALID_FLAG_VALUE");
|
|
1003
|
+
}
|
|
1004
|
+
const typeDir = stashDirFor(ref.type);
|
|
322
1005
|
if (!typeDir) {
|
|
323
1006
|
throw new UsageError(`Unknown asset type "${ref.type}". Cannot resolve a write path.`, "INVALID_FLAG_VALUE");
|
|
324
1007
|
}
|
|
325
1008
|
const typeRoot = path.join(source.path, typeDir);
|
|
326
|
-
const assetPath =
|
|
1009
|
+
const assetPath = assetPathForName(ref.type, typeRoot, ref.name);
|
|
327
1010
|
if (!isWithin(assetPath, typeRoot)) {
|
|
328
1011
|
throw new UsageError(`Resolved asset path escapes its source: "${ref.name}" in source "${source.name}".`, "PATH_ESCAPE_VIOLATION");
|
|
329
1012
|
}
|
|
330
1013
|
return assetPath;
|
|
331
1014
|
}
|
|
1015
|
+
export function assertAkmAssetWrite(source, allowedAdapters = ["akm"]) {
|
|
1016
|
+
if (!source.adapterId || allowedAdapters.includes(source.adapterId))
|
|
1017
|
+
return;
|
|
1018
|
+
throw new UsageError(`Bundle "${source.name}" uses adapter "${source.adapterId}", which does not support AKM asset writes.`, "INVALID_FLAG_VALUE");
|
|
1019
|
+
}
|
|
332
1020
|
/**
|
|
333
1021
|
* Reject any kind reaching the write/delete helpers other than the two
|
|
334
1022
|
* supported writable kinds. The config loader is the first line of defence
|
|
@@ -351,7 +1039,12 @@ export function formatRefForMessage(ref) {
|
|
|
351
1039
|
const origin = ref.origin ? sanitizeCommitMessage(ref.origin) : "";
|
|
352
1040
|
const type = sanitizeCommitMessage(ref.type);
|
|
353
1041
|
const name = sanitizeCommitMessage(ref.name);
|
|
354
|
-
|
|
1042
|
+
// 0.9.0 (Q-02): the retired `type:name` colon grammar is gone — emit the
|
|
1043
|
+
// slash conceptId (`workflows/name`), qualified with `origin//` when the
|
|
1044
|
+
// ref carries one. Mirrors the `displayRef`/`conceptIdFromTypeName` rule
|
|
1045
|
+
// used elsewhere in this file (see `resolveAssetFilePath` callers above).
|
|
1046
|
+
const conceptId = conceptIdFromTypeName(type, name);
|
|
1047
|
+
return origin ? `${origin}//${conceptId}` : conceptId;
|
|
355
1048
|
}
|
|
356
1049
|
/**
|
|
357
1050
|
* Derive a {@link WriteTargetSource} + persisted {@link SourceConfigEntry}
|
|
@@ -362,14 +1055,8 @@ export function formatRefForMessage(ref) {
|
|
|
362
1055
|
* ConfiguredSource.name → WriteTargetSource.name
|
|
363
1056
|
* ConfiguredSource.source.* → WriteTargetSource.path (via parseSourceSpec)
|
|
364
1057
|
*
|
|
365
|
-
* Legacy aliases (`context-hub`, `github`) have already been normalised to
|
|
366
|
-
* `git` by the config loader, so this mapping is straightforward.
|
|
367
1058
|
*/
|
|
368
1059
|
function adaptConfiguredSource(runtime) {
|
|
369
|
-
const writePath = pathFromConfiguredSource(runtime);
|
|
370
|
-
if (!writePath) {
|
|
371
|
-
throw new ConfigError(`Source "${runtime.name}" has no resolvable on-disk path; writes are unsupported for this entry.`, "INVALID_CONFIG_FILE");
|
|
372
|
-
}
|
|
373
1060
|
// Map the runtime kind to the write helper's `kind` discriminator. Only
|
|
374
1061
|
// filesystem and git produce writable sources at v1; any other kind
|
|
375
1062
|
// reaching this point is a config-loader bug (assertWritableAllowedForKind
|
|
@@ -380,18 +1067,47 @@ function adaptConfiguredSource(runtime) {
|
|
|
380
1067
|
"Writes are only defined for `filesystem` and `git` sources.", "INVALID_CONFIG_FILE", 'Use `kind: "filesystem"` or `kind: "git"` for writable sources.');
|
|
381
1068
|
}
|
|
382
1069
|
const kind = runtime.type;
|
|
1070
|
+
// §10.2 lock-first (BEHAVIOR FIX): a managed git bundle's resolved content
|
|
1071
|
+
// root lives in the lock (`localRoot`), NOT the desired config. Resolve there
|
|
1072
|
+
// FIRST — via the SAME shared resolver the indexer READ path uses — so a write
|
|
1073
|
+
// lands in exactly the directory a read walks; git sync/commit then runs
|
|
1074
|
+
// against that same root. Before the first lock row exists, fall back to the
|
|
1075
|
+
// derived cache repoDir + content/-subdir convention used by the read path.
|
|
1076
|
+
const lockRoot = kind === "git" ? lockContentRootFor(runtime.name, runtime.type) : undefined;
|
|
1077
|
+
const repoPath = lockRoot ?? pathFromConfiguredSource(runtime);
|
|
1078
|
+
if (!repoPath) {
|
|
1079
|
+
throw new ConfigError(`Source "${runtime.name}" has no resolvable on-disk path; writes are unsupported for this entry.`, "INVALID_CONFIG_FILE");
|
|
1080
|
+
}
|
|
1081
|
+
const contentRoot = kind === "git" ? (lockRoot ?? resolveGitContentRoot(repoPath)) : repoPath;
|
|
1082
|
+
const componentRoot = path.resolve(contentRoot, runtime.componentRoot ?? ".");
|
|
1083
|
+
if (!isWithin(componentRoot, contentRoot)) {
|
|
1084
|
+
throw new ConfigError(`Component root "${runtime.componentRoot}" escapes bundle "${runtime.name}".`, "INVALID_CONFIG_FILE");
|
|
1085
|
+
}
|
|
1086
|
+
const adapterId = runtime.adapterId ?? detectAdapterId(componentRoot);
|
|
383
1087
|
const config = {
|
|
384
1088
|
type: runtime.type,
|
|
385
1089
|
name: runtime.name,
|
|
386
|
-
|
|
1090
|
+
path: componentRoot,
|
|
387
1091
|
...(runtime.writable !== undefined ? { writable: runtime.writable } : {}),
|
|
388
1092
|
...(runtime.options ? { options: runtime.options } : {}),
|
|
389
1093
|
};
|
|
390
1094
|
return {
|
|
391
|
-
|
|
1095
|
+
selector: runtime.name,
|
|
1096
|
+
source: {
|
|
1097
|
+
kind,
|
|
1098
|
+
name: runtime.name,
|
|
1099
|
+
path: componentRoot,
|
|
1100
|
+
adapterId,
|
|
1101
|
+
...(kind === "git" ? { repoPath } : {}),
|
|
1102
|
+
},
|
|
392
1103
|
config,
|
|
393
1104
|
};
|
|
394
1105
|
}
|
|
1106
|
+
/** Resolve the asset root inside a git checkout while preserving root-layout repos. */
|
|
1107
|
+
export function resolveGitContentRoot(repoPath) {
|
|
1108
|
+
const contentPath = path.join(repoPath, "content");
|
|
1109
|
+
return fs.existsSync(contentPath) && fs.statSync(contentPath).isDirectory() ? contentPath : repoPath;
|
|
1110
|
+
}
|
|
395
1111
|
function pathFromConfiguredSource(runtime) {
|
|
396
1112
|
// ConfiguredSource.source is the parsed SourceSpec (filesystem|git|website|npm).
|
|
397
1113
|
// For writable kinds we only ever care about a local on-disk path: filesystem
|