akm-cli 0.9.0-rc.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1063 -44
- package/README.md +51 -25
- package/SECURITY.md +14 -1
- package/STABILITY.md +497 -0
- package/dist/akm +148 -35
- package/dist/{akm-migrate-storage → akm-migrate} +6 -9
- package/dist/assets/hints/cli-hints-full.md +223 -95
- package/dist/assets/hints/cli-hints-short.md +85 -22
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/prompts/memory-infer-user.md +2 -3
- package/dist/assets/stash-skeleton/README.md +6 -5
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
- package/dist/assets/tasks/core/extract.yml +1 -1
- package/dist/assets/tasks/core/version-check.yml +1 -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 +1 -3
- package/dist/assets/workflows/workflow-template.md +32 -15
- package/dist/cli/invocation.js +40 -15
- package/dist/cli/parse-args.js +0 -22
- package/dist/cli/retired-commands.js +121 -0
- package/dist/cli/shared.js +154 -22
- package/dist/cli/unknown-flags.js +236 -0
- package/dist/cli-node.mjs +2 -1
- package/dist/cli.js +696 -258
- package/dist/commands/agent/agent-dispatch.js +14 -3
- package/dist/commands/agent/contribute-cli.js +73 -88
- package/dist/commands/completions.js +79 -22
- package/dist/commands/config-cli.js +17 -150
- package/dist/commands/env/env-cli.js +59 -143
- package/dist/commands/env/env.js +12 -163
- package/dist/commands/env/marker-path.js +6 -0
- package/dist/commands/env/secret-cli.js +36 -66
- package/dist/commands/env/secret.js +24 -57
- package/dist/commands/feedback-cli.js +141 -87
- package/dist/commands/health/accept-rate.js +58 -0
- package/dist/commands/health/advisories.js +3 -4
- package/dist/commands/health/checks.js +85 -23
- package/dist/commands/health/html-report.js +7 -10
- package/dist/commands/health/improve-metrics.js +25 -83
- package/dist/commands/health/md-report.js +5 -9
- package/dist/commands/health/metrics.js +62 -20
- package/dist/commands/health/renderers.js +47 -0
- package/dist/commands/health/report-view-model.js +4 -5
- package/dist/commands/health/stash-exposure.js +1 -1
- package/dist/commands/health/surfaces.js +3 -48
- package/dist/commands/health/task-runs.js +3 -67
- package/dist/commands/health/types-improve.js +7 -0
- package/dist/commands/health.js +99 -28
- package/dist/commands/improve/anti-collapse.js +2 -2
- package/dist/commands/improve/autonomy-gate.js +68 -0
- package/dist/commands/improve/collapse-detector.js +41 -40
- package/dist/commands/improve/consolidate/eligibility.js +1 -23
- package/dist/commands/improve/consolidate/merge.js +4 -0
- package/dist/commands/improve/consolidate.js +140 -1000
- package/dist/commands/improve/distill/promote-memory.js +12 -12
- package/dist/commands/improve/distill/quality-gate.js +6 -6
- package/dist/commands/improve/distill.js +58 -69
- package/dist/commands/improve/eligibility.js +105 -57
- package/dist/commands/improve/extract-cli.js +14 -133
- package/dist/commands/improve/improve-cli.js +98 -114
- package/dist/commands/improve/improve-result-file.js +1 -28
- package/dist/commands/improve/improve-strategies.js +8 -5
- package/dist/commands/improve/improve.js +128 -91
- package/dist/commands/improve/loop-stages.js +182 -20
- package/dist/commands/improve/memory/derived-ref.js +45 -43
- package/dist/commands/improve/memory/memory-belief.js +1 -1
- package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
- package/dist/commands/improve/memory/memory-improve.js +6 -5
- package/dist/commands/improve/outcome-loop.js +22 -65
- package/dist/commands/improve/preparation.js +114 -123
- package/dist/commands/improve/proactive-maintenance.js +2 -5
- package/dist/commands/improve/reflect.js +56 -160
- package/dist/commands/improve/salience.js +11 -122
- package/dist/commands/improve/source-identity.js +10 -38
- package/dist/commands/lint/base-linter.js +20 -124
- package/dist/commands/lint/env-key-rules.js +31 -47
- package/dist/commands/lint/index.js +249 -43
- package/dist/commands/{events.js → log.js} +33 -38
- package/dist/commands/migrate-cli.js +92 -12
- package/dist/commands/migration-tool.js +46 -0
- package/dist/commands/observability-cli.js +70 -209
- package/dist/commands/proposal/drain.js +101 -29
- package/dist/commands/proposal/proposal-cli.js +76 -48
- package/dist/commands/proposal/proposal.js +54 -18
- package/dist/commands/proposal/propose-cli.js +88 -0
- package/dist/commands/proposal/propose.js +23 -15
- package/dist/commands/proposal/repository.js +701 -278
- package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
- package/dist/commands/proposal/validators/proposal-validators.js +55 -7
- package/dist/commands/proposal/validators/proposals.js +4 -7
- package/dist/commands/read/curate.js +34 -53
- package/dist/commands/read/knowledge.js +150 -95
- package/dist/commands/read/registry-search.js +2 -2
- package/dist/commands/read/remember-cli.js +42 -15
- package/dist/commands/read/search-cli.js +180 -78
- package/dist/commands/read/search.js +58 -43
- package/dist/commands/read/show.js +197 -141
- package/dist/commands/registry-cli.js +12 -51
- package/dist/commands/remember.js +14 -57
- package/dist/commands/sources/add-cli.js +100 -31
- package/dist/commands/sources/bundle-cli.js +166 -0
- package/dist/commands/sources/bundle-config-ops.js +7 -2
- package/dist/commands/sources/info.js +18 -5
- package/dist/commands/sources/init.js +12 -12
- package/dist/commands/sources/installed-stashes.js +382 -98
- package/dist/commands/sources/schema-repair.js +3 -2
- package/dist/commands/sources/self-update.js +131 -38
- package/dist/commands/sources/source-add.js +72 -17
- package/dist/commands/sources/source-clone.js +129 -45
- package/dist/commands/sources/source-manage.js +43 -23
- package/dist/commands/sources/sources-cli.js +57 -208
- package/dist/commands/sources/stash-cli.js +46 -53
- package/dist/commands/tasks/tasks-cli.js +91 -97
- package/dist/commands/tasks/tasks.js +276 -421
- package/dist/commands/workflow-cli.js +175 -450
- package/dist/core/adapter/adapters/akm-adapter.js +47 -28
- package/dist/core/adapter/adapters/akm-lint.js +42 -27
- package/dist/core/adapter/adapters/akm-metadata.js +15 -44
- package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
- package/dist/core/adapter/adapters/index.js +6 -6
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
- package/dist/core/adapter/adapters/okf-adapter.js +187 -19
- package/dist/core/adapter/adapters/shared.js +3 -19
- package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
- package/dist/core/adapter/detect-adapter.js +17 -0
- package/dist/core/adapter/recognize-match.js +6 -4
- package/dist/core/adapter/validate-context.js +214 -0
- package/dist/core/asset/akm-markdown.js +63 -0
- package/dist/core/asset/asset-placement.js +20 -6
- package/dist/core/asset/asset-ref.js +11 -9
- package/dist/core/asset/frontmatter-lint.js +30 -0
- package/dist/core/asset/frontmatter.js +37 -9
- package/dist/core/asset/markdown.js +40 -51
- package/dist/core/asset/resolve-ref.js +89 -18
- package/dist/core/asset/stash-meta.js +1 -1
- package/dist/core/bundle-id.js +51 -0
- package/dist/core/common.js +152 -38
- package/dist/core/config/config-io.js +12 -1
- package/dist/core/config/config-schema.js +35 -8
- package/dist/core/config/config-sources.js +55 -11
- package/dist/core/config/config-walker.js +25 -9
- package/dist/core/config/config.js +9 -48
- package/dist/core/config/experimental.js +21 -0
- package/dist/core/config/schema/embedding.js +5 -1
- package/dist/core/config/schema/experimental.js +30 -0
- package/dist/core/config/schema/improve-processes.js +0 -6
- package/dist/core/config/schema/improve.js +21 -3
- package/dist/core/config/schema/index-config.js +8 -15
- package/dist/core/config/schema/output.js +4 -1
- package/dist/core/config/schema/setup.js +9 -18
- package/dist/core/config/schema/sources-bundles.js +49 -33
- package/dist/core/config/schema/workflow.js +3 -3
- package/dist/core/env-secret-ref.js +76 -46
- package/dist/core/errors.js +18 -12
- package/dist/core/events.js +46 -128
- package/dist/core/file-change.js +6 -5
- package/dist/core/fs-txn.js +83 -7
- package/dist/core/git-message.js +2 -2
- package/dist/core/improve-result.js +1 -100
- package/dist/core/lesson-lint.js +1 -17
- package/dist/core/logs-db.js +2 -1
- package/dist/core/migration-operation.js +16 -0
- package/dist/core/mutation-target.js +78 -0
- package/dist/core/parse.js +4 -1
- package/dist/core/paths.js +17 -20
- package/dist/core/recognition-util.js +12 -14
- package/dist/core/redaction.js +34 -0
- package/dist/core/standards/resolve-standards-context.js +2 -14
- package/dist/core/standards/resolve-stash-standards.js +2 -2
- package/dist/core/standards/resolve-type-conventions.js +2 -2
- package/dist/core/state/migrations.js +41 -18
- package/dist/core/state-db.js +5 -14
- package/dist/core/structured.js +1 -1
- package/dist/core/subprocess.js +6 -4
- package/dist/core/text-truncation.js +9 -5
- package/dist/core/type-presentation.js +3 -3
- package/dist/core/warn.js +0 -3
- package/dist/core/write-source.js +771 -95
- package/dist/indexer/bundle-identity-guard.js +3 -2
- package/dist/indexer/db/graph-db.js +0 -24
- package/dist/indexer/ensure-index.js +1 -0
- package/dist/indexer/graph/graph-boost.js +9 -34
- package/dist/indexer/graph/graph-extraction.js +8 -5
- package/dist/indexer/index-writer-lock.js +53 -17
- package/dist/indexer/index-written-assets.js +16 -22
- package/dist/indexer/indexer.js +497 -239
- package/dist/indexer/installations.js +14 -96
- package/dist/indexer/passes/dir-staleness.js +16 -9
- package/dist/indexer/passes/memory-inference.js +11 -9
- package/dist/indexer/passes/metadata.js +113 -47
- package/dist/indexer/scan/doc-to-entry.js +38 -1
- package/dist/indexer/scan/drain-dir.js +13 -23
- package/dist/indexer/search/db-search.js +99 -54
- package/dist/indexer/search/fts-query.js +47 -24
- package/dist/indexer/search/ranking-contributors.js +42 -20
- package/dist/indexer/search/ranking.js +18 -99
- package/dist/indexer/search/search-fields.js +7 -2
- package/dist/indexer/search/search-source.js +82 -93
- package/dist/indexer/usage/usage-events.js +0 -89
- package/dist/indexer/walk/file-context.js +2 -1
- package/dist/indexer/walk/matchers.js +30 -43
- package/dist/indexer/walk/path-resolver.js +7 -2
- package/dist/indexer/walk/walker.js +38 -12
- package/dist/integrations/agent/builders.js +0 -6
- package/dist/integrations/agent/config.js +2 -2
- package/dist/integrations/agent/detect.js +49 -19
- package/dist/integrations/agent/engine-fallback.js +76 -0
- package/dist/integrations/agent/profiles.js +14 -0
- package/dist/integrations/agent/prompts.js +12 -8
- package/dist/integrations/agent/runner-dispatch.js +4 -2
- package/dist/integrations/agent/runner.js +0 -1
- package/dist/integrations/agent/spawn.js +5 -6
- package/dist/integrations/github.js +1 -1
- package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
- package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
- package/dist/integrations/harnesses/claude/session-log.js +0 -10
- package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
- package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
- package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
- package/dist/integrations/harnesses/index.js +3 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
- package/dist/integrations/harnesses/opencode/session-log.js +0 -15
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
- package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
- package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
- package/dist/integrations/lockfile.js +101 -6
- package/dist/integrations/session-logs/index.js +3 -28
- package/dist/llm/client.js +136 -100
- package/dist/llm/embedders/remote.js +13 -5
- package/dist/llm/feature-gate.js +4 -12
- package/dist/llm/graph-extract.js +5 -11
- package/dist/llm/memory-infer.js +144 -1
- package/dist/llm/metadata-enhance.js +5 -7
- package/dist/llm/structured-call.js +1 -1
- package/dist/llm/usage-persist.js +26 -5
- package/dist/llm/usage-telemetry.js +25 -2
- package/dist/output/cli-hints.js +1 -2
- package/dist/output/context.js +22 -7
- package/dist/output/format-exempt.js +80 -0
- package/dist/output/generic-render.js +259 -0
- package/dist/output/render-registry.js +57 -0
- package/dist/output/renderers.js +14 -36
- package/dist/output/shapes/curate.js +10 -1
- package/dist/output/shapes/events.js +12 -7
- package/dist/output/shapes/helpers.js +56 -83
- package/dist/output/shapes/migrate.js +8 -0
- package/dist/output/shapes/passthrough.js +7 -41
- package/dist/output/shapes/proposal/producer.js +15 -7
- package/dist/output/shapes.js +2 -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 +164 -96
- package/dist/output/text/env.js +1 -3
- package/dist/output/text/events.js +8 -7
- package/dist/output/text/health-format.js +103 -0
- package/dist/output/text/health.js +7 -0
- package/dist/output/text/helpers.js +10 -8
- package/dist/output/text/lint-format.js +43 -0
- package/dist/output/text/{save.js → lint.js} +2 -2
- package/dist/output/text/migrate.js +88 -0
- package/dist/output/text/proposal/producer.js +4 -2
- package/dist/output/text/proposal-format.js +44 -72
- package/dist/output/text/registry-commands.js +1 -2
- package/dist/output/text/show-directives.js +15 -7
- package/dist/output/text/status-list.js +32 -0
- package/dist/output/text/sync.js +5 -0
- package/dist/output/text/workflow-format.js +24 -203
- package/dist/output/text/workflow.js +1 -7
- package/dist/output/text.js +16 -17
- 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 +13 -23
- package/dist/registry/resolve.js +42 -7
- package/dist/registry/semver.js +34 -84
- package/dist/runtime.js +2 -23
- package/dist/scripts/akm-migrate-node.js +60290 -0
- package/dist/scripts/akm-migrate.js +59628 -0
- package/dist/setup/detect.js +42 -15
- package/dist/setup/registry-stash-loader.js +2 -2
- package/dist/setup/setup.js +236 -136
- package/dist/setup/steps/connection.js +7 -9
- package/dist/setup/steps/platforms.js +9 -9
- package/dist/setup/steps/semantic.js +15 -3
- package/dist/setup/steps/sources.js +12 -13
- package/dist/setup/steps/stashdir.js +2 -3
- package/dist/setup/steps/tasks.js +237 -120
- package/dist/sources/freshness.js +1 -1
- 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 +25 -23
- package/dist/sources/providers/git-stash.js +395 -106
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/providers/npm.js +16 -19
- package/dist/sources/providers/provider-utils.js +7 -4
- package/dist/sources/providers/sync-from-ref.js +3 -9
- package/dist/sources/providers/website.js +6 -1
- package/dist/sources/resolve.js +6 -5
- package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
- package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
- package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
- package/dist/sources/snapshot-fetchers/github.js +100 -0
- package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
- package/dist/sources/snapshot-fetchers/registry.js +17 -1
- package/dist/sources/snapshot-fetchers/robots.js +348 -0
- package/dist/sources/snapshot-fetchers/rss.js +282 -0
- package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
- package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
- package/dist/sources/snapshot-fetchers/x.js +910 -0
- package/dist/storage/database.js +7 -0
- package/dist/storage/engines/sqlite-migrations.js +23 -111
- package/dist/storage/managed-db.js +2 -2
- package/dist/storage/repositories/canaries-repository.js +1 -1
- package/dist/storage/repositories/events-repository.js +27 -11
- package/dist/storage/repositories/improve-runs-repository.js +6 -12
- package/dist/storage/repositories/index-connection.js +17 -6
- package/dist/storage/repositories/index-entries-repository.js +151 -240
- package/dist/storage/repositories/index-entry-mapper.js +15 -11
- package/dist/storage/repositories/index-fts-repository.js +5 -2
- package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
- package/dist/storage/repositories/index-meta-repository.js +2 -3
- package/dist/storage/repositories/index-schema.js +10 -25
- package/dist/storage/repositories/index-utility-repository.js +15 -28
- package/dist/storage/repositories/index-vec-repository.js +6 -1
- package/dist/storage/repositories/outcome-repository.js +119 -0
- package/dist/storage/repositories/proposals-repository.js +296 -59
- package/dist/storage/repositories/registry-cache.js +19 -0
- package/dist/storage/repositories/salience-repository.js +172 -0
- package/dist/storage/repositories/task-history-repository.js +15 -13
- package/dist/storage/repositories/workflow-runs-repository.js +52 -40
- package/dist/tasks/backends/cron.js +105 -15
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +85 -38
- package/dist/tasks/backends/schtasks.js +135 -15
- package/dist/tasks/embedded.js +56 -40
- package/dist/tasks/parser.js +7 -157
- package/dist/tasks/resolve-akm-bin.js +137 -59
- package/dist/tasks/runner.js +79 -42
- package/dist/tasks/scheduler-invocation.js +220 -10
- package/dist/tasks/schema.js +24 -1
- package/dist/tasks/task-id.js +1 -3
- package/dist/tasks/validator.js +20 -6
- package/dist/workflows/authoring/authoring.js +94 -143
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/exec/frozen-judge.js +28 -2
- package/dist/workflows/exec/native-executor.js +77 -57
- package/dist/workflows/exec/param-secrets.js +9 -9
- package/dist/workflows/exec/run-workflow.js +133 -79
- package/dist/workflows/exec/step-work.js +219 -346
- package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
- package/dist/workflows/ir/compile.js +141 -270
- package/dist/workflows/ir/freeze.js +40 -30
- package/dist/workflows/ir/params.js +135 -11
- package/dist/workflows/ir/plan-hash.js +1 -1
- package/dist/workflows/ir/schema.js +25 -26
- package/dist/workflows/parser.js +872 -307
- package/dist/workflows/program/expressions.js +20 -208
- package/dist/workflows/program/schema.js +7 -10
- package/dist/workflows/renderer.js +95 -68
- package/dist/workflows/resource-limits.js +2 -0
- package/dist/workflows/runtime/checkin.js +3 -3
- package/dist/workflows/runtime/plan-classifier.js +16 -75
- package/dist/workflows/runtime/runs.js +186 -127
- package/dist/workflows/runtime/unit-checkin.js +1 -1
- package/dist/workflows/runtime/unit-phases.js +2 -2
- package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +30 -36
- package/dist/workflows/validator.js +21 -62
- package/docs/README.md +68 -0
- package/docs/migration/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +11 -11
- package/docs/migration/release-notes/0.9.0.md +208 -27
- package/docs/migration/v0.7-to-v0.8.md +46 -47
- package/docs/migration/v0.8-to-v0.9.md +564 -208
- package/docs/migration/v0.9.0-troubleshooting.md +561 -0
- package/docs/reference/README.md +12 -0
- package/docs/reference/cli.md +2253 -0
- package/docs/reference/configuration.md +358 -0
- package/docs/reference/data-and-telemetry.md +105 -42
- package/docs/reference/workflows.md +647 -0
- package/package.json +22 -11
- package/schemas/akm-asset-envelope.json +93 -0
- package/schemas/akm-config.json +81 -128
- package/schemas/akm-workflow.json +74 -73
- package/dist/assets/tasks/core/backup.yml +0 -5
- package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
- package/dist/cli/config-migrate.js +0 -1806
- package/dist/cli/config-validate.js +0 -41
- package/dist/commands/backup-cli.js +0 -56
- package/dist/commands/bundle/bundle-cli.js +0 -68
- package/dist/commands/bundle/bundle.js +0 -219
- package/dist/commands/graph/graph-cli.js +0 -124
- package/dist/commands/graph/graph.js +0 -489
- package/dist/commands/improve/extract-watch.js +0 -140
- package/dist/commands/mv-cli.js +0 -1221
- package/dist/commands/sources/history.js +0 -201
- package/dist/commands/tasks/default-tasks.js +0 -186
- package/dist/core/migration-backup.js +0 -1234
- package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
- package/dist/llm/memory-infer-impl.js +0 -138
- package/dist/migrate/legacy/config-source-migration.js +0 -223
- package/dist/migrate/legacy/content-migration.js +0 -305
- package/dist/migrate/legacy/legacy-layout.js +0 -779
- package/dist/migrate/legacy/legacy-paths.js +0 -25
- package/dist/migrate/legacy/legacy-stash-json.js +0 -72
- package/dist/migrate/legacy/proposal-fs-import.js +0 -168
- package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
- package/dist/migrate/legacy/three-db-cutover.js +0 -841
- package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
- package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
- package/dist/migrate/legacy-ref-grammar.js +0 -214
- 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/registry/build-index.js +0 -382
- package/dist/schemas/akm-config.json +0 -4704
- package/dist/schemas/akm-task.json +0 -87
- package/dist/schemas/akm-workflow.json +0 -372
- package/dist/scripts/migrate-storage.js +0 -3816
- package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
- package/dist/workflows/cli.js +0 -53
- package/dist/workflows/exec/brief.js +0 -481
- package/dist/workflows/exec/report.js +0 -1460
- package/dist/workflows/exec/watch.js +0 -116
- package/dist/workflows/program/parser.js +0 -813
- package/dist/workflows/program/project.js +0 -104
|
@@ -13,8 +13,8 @@
|
|
|
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
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* commits only operation-owned exact paths as one complete commit instead of
|
|
17
|
+
* one noisy, incomplete commit per asset.
|
|
18
18
|
*
|
|
19
19
|
* This module is still the **single dispatch point** for write/delete: callers
|
|
20
20
|
* (remember, import, source-add, etc.) MUST go through `writeAssetToSource` /
|
|
@@ -24,49 +24,230 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import fs from "node:fs";
|
|
26
26
|
import path from "node:path";
|
|
27
|
+
import { withAssetMutationLeaseSync } from "../indexer/index-writer-lock.js";
|
|
27
28
|
import { lockContentRootFor } from "../integrations/lockfile.js";
|
|
28
|
-
import { getCachePaths,
|
|
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";
|
|
29
33
|
import { assetPathForName, stashDirFor } from "./asset/asset-placement.js";
|
|
30
|
-
import { displayRef } from "./asset/resolve-ref.js";
|
|
34
|
+
import { conceptIdFromTypeName, displayRef } from "./asset/resolve-ref.js";
|
|
35
|
+
import { deriveBundleId } from "./bundle-id.js";
|
|
31
36
|
import { isWithin, resolveStashDir } from "./common.js";
|
|
32
37
|
import { resolveConfiguredSources } from "./config/config.js";
|
|
33
38
|
import { ConfigError, UsageError } from "./errors.js";
|
|
34
39
|
import { sanitizeCommitMessage } from "./git-message.js";
|
|
35
40
|
import { warn } from "./warn.js";
|
|
36
|
-
// Re-exported so existing `import { sanitizeCommitMessage } from
|
|
37
|
-
// "./core/write-source"` sites are unaffected by the KILL 6 sever (the
|
|
38
|
-
// helper moved to git-message.ts to break the write-source.ts → git.ts →
|
|
39
|
-
// git-stash.ts → write-source.ts 3-file import cycle).
|
|
40
|
-
export { sanitizeCommitMessage };
|
|
41
41
|
/**
|
|
42
42
|
* Source kinds that the loader is allowed to mark `writable: true`. Anything
|
|
43
43
|
* else is rejected at config load (per locked decision 4) — see
|
|
44
44
|
* {@link assertWritableAllowedForKind}.
|
|
45
45
|
*/
|
|
46
46
|
const REJECTED_WRITABLE_KINDS = new Set(["website", "npm"]);
|
|
47
|
-
const
|
|
47
|
+
const pendingGitMutations = new Map();
|
|
48
|
+
const EMPTY_GIT_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
49
|
+
const GIT_PUSH_TIMEOUT_MS = 120_000;
|
|
48
50
|
function gitTargetKey(source) {
|
|
49
51
|
return `${path.resolve(source.repoPath ?? source.path)}\0${path.resolve(source.path)}`;
|
|
50
52
|
}
|
|
51
|
-
/**
|
|
53
|
+
/** Record the exact post-mutation blob for the target's boundary commit. */
|
|
52
54
|
export function recordWriteTargetPath(source, filePath) {
|
|
53
55
|
if (source.kind !== "git")
|
|
54
56
|
return;
|
|
57
|
+
const repoDir = path.resolve(source.repoPath ?? source.path);
|
|
58
|
+
if (!isGitBackedStash(repoDir))
|
|
59
|
+
return;
|
|
55
60
|
const key = gitTargetKey(source);
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
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);
|
|
59
68
|
}
|
|
60
|
-
function
|
|
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;
|
|
61
82
|
const key = gitTargetKey(source);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
66
196
|
const repoDir = path.resolve(source.repoPath ?? source.path);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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);
|
|
70
251
|
}
|
|
71
252
|
// ── Portability advisory (review 13, D1) ────────────────────────────────────
|
|
72
253
|
/**
|
|
@@ -144,11 +325,20 @@ export function assertWritableAllowedForKind(entry) {
|
|
|
144
325
|
export async function writeAssetToSource(source, config, ref, content) {
|
|
145
326
|
ensureWritable(source, config);
|
|
146
327
|
assertSupportedKind(source);
|
|
328
|
+
assertAkmAssetWrite(source);
|
|
147
329
|
const filePath = resolveAssetFilePath(source, ref);
|
|
148
|
-
|
|
149
|
-
const normalized =
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
}
|
|
152
342
|
// Non-fatal portability advisory (review 13, D1): flag absolute host home
|
|
153
343
|
// paths in the written content. These make the stash non-portable and leak
|
|
154
344
|
// the local username. We warn AFTER the write so the advisory never blocks it.
|
|
@@ -168,12 +358,20 @@ export async function writeAssetToSource(source, config, ref, content) {
|
|
|
168
358
|
export async function deleteAssetFromSource(source, config, ref) {
|
|
169
359
|
ensureWritable(source, config);
|
|
170
360
|
assertSupportedKind(source);
|
|
361
|
+
assertAkmAssetWrite(source);
|
|
171
362
|
const filePath = resolveAssetFilePath(source, ref);
|
|
172
363
|
if (!fs.existsSync(filePath)) {
|
|
173
364
|
throw new UsageError(`Asset "${formatRefForMessage(ref)}" not found in source "${source.name}" (expected at ${filePath}).`, "MISSING_REQUIRED_ARGUMENT");
|
|
174
365
|
}
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}
|
|
177
375
|
return { path: filePath, ref: displayRef({ type: ref.type, name: ref.name, bundleId: ref.origin }) };
|
|
178
376
|
}
|
|
179
377
|
/**
|
|
@@ -190,48 +388,499 @@ export async function deleteAssetFromSource(source, config, ref) {
|
|
|
190
388
|
* caller paths), commits once, and pushes when the target is writable, has a
|
|
191
389
|
* remote, and `push !== false`.
|
|
192
390
|
*
|
|
193
|
-
* The deprecated `options.pushOnCommit` on the source config is now fully
|
|
194
|
-
* IGNORED (Decision 6, WI-9.6b): it neither sets nor suppresses `push`. Only a
|
|
195
|
-
* one-time deprecation warning remains (see {@link warnIfPushOnCommit}).
|
|
196
391
|
*/
|
|
197
392
|
export function commitWriteTargetBoundary(target, message, options) {
|
|
198
393
|
if (target.source.kind !== "git")
|
|
199
394
|
return;
|
|
200
|
-
warnIfPushOnCommit(target.config);
|
|
201
395
|
const push = options?.push;
|
|
202
396
|
const writable = resolveWritable(target.config);
|
|
203
|
-
const repoDir = target.source.repoPath ?? target.source.path;
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
}
|
|
208
436
|
// Assets may live under <repo>/content, but git synchronization always runs
|
|
209
437
|
// against the repository root.
|
|
210
|
-
|
|
211
|
-
|
|
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, {
|
|
212
551
|
paths,
|
|
213
|
-
|
|
552
|
+
push: false,
|
|
553
|
+
transactionId: options.transactionId,
|
|
554
|
+
expectedBaseHead: publication.baseHead,
|
|
555
|
+
expectedSnapshots: options.snapshots,
|
|
214
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);
|
|
215
582
|
}
|
|
216
583
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
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.
|
|
222
598
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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)
|
|
226
626
|
return;
|
|
227
|
-
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) {
|
|
228
631
|
return;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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.`);
|
|
235
884
|
}
|
|
236
885
|
/** Enumerate enabled writable targets, deduplicated by materialized content root. */
|
|
237
886
|
export function resolveWritableTargets(akmConfig) {
|
|
@@ -245,17 +894,9 @@ export function resolveWritableTargets(akmConfig) {
|
|
|
245
894
|
if (!existing || target.source.name === akmConfig.defaultWriteTarget)
|
|
246
895
|
byRoot.set(root, target);
|
|
247
896
|
}
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
byRoot.set(path.resolve(stashDir), {
|
|
252
|
-
source: { kind: "filesystem", name: "stash", path: stashDir },
|
|
253
|
-
config: { type: "filesystem", path: stashDir, name: "stash", writable: true },
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
// No active working stash; configured writable targets above are complete.
|
|
258
|
-
}
|
|
897
|
+
if (process.env.AKM_BUNDLE_DIR?.trim()) {
|
|
898
|
+
const target = resolveWorkingStashTarget(akmConfig);
|
|
899
|
+
byRoot.set(path.resolve(target.source.path), target);
|
|
259
900
|
}
|
|
260
901
|
return [...byRoot.values()];
|
|
261
902
|
}
|
|
@@ -264,8 +905,8 @@ export function resolveWritableTargets(akmConfig) {
|
|
|
264
905
|
*
|
|
265
906
|
* 1. Explicit `--target <name>` (when supplied)
|
|
266
907
|
* 2. `config.defaultWriteTarget`
|
|
267
|
-
* 3. `config.
|
|
268
|
-
* 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`")`
|
|
269
910
|
*
|
|
270
911
|
* The legacy `first-writable-in-source-array-order` fallback is *not* used —
|
|
271
912
|
* see plan §6 decision 3 for the rationale.
|
|
@@ -277,7 +918,7 @@ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
|
|
|
277
918
|
if (explicitTarget) {
|
|
278
919
|
const match = configuredSources.find((s) => s.name === explicitTarget);
|
|
279
920
|
if (!match) {
|
|
280
|
-
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");
|
|
281
922
|
}
|
|
282
923
|
// Up-front writable check so an explicit --target fails fast with a
|
|
283
924
|
// ConfigError (rather than the generic UsageError ensureWritable would
|
|
@@ -306,28 +947,45 @@ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
|
|
|
306
947
|
return adaptConfiguredSource(match);
|
|
307
948
|
}
|
|
308
949
|
// Fall through if the named target no longer exists — surface a clear error.
|
|
309
|
-
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.");
|
|
310
|
-
}
|
|
311
|
-
// 3.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
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 ?? {})));
|
|
321
973
|
return {
|
|
322
|
-
source: { kind: "filesystem", name:
|
|
323
|
-
config: { type: "filesystem",
|
|
974
|
+
source: { kind: "filesystem", name: bundleId, path: stashDir, adapterId: detectAdapterId(stashDir) },
|
|
975
|
+
config: { type: "filesystem", name: bundleId, path: stashDir, writable: true },
|
|
324
976
|
};
|
|
325
977
|
}
|
|
326
|
-
|
|
327
|
-
|
|
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");
|
|
328
983
|
}
|
|
329
|
-
|
|
330
|
-
|
|
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.`);
|
|
987
|
+
}
|
|
988
|
+
return { ...target, selector: undefined };
|
|
331
989
|
}
|
|
332
990
|
// ── Internals ───────────────────────────────────────────────────────────────
|
|
333
991
|
function ensureWritable(source, config) {
|
|
@@ -339,6 +997,10 @@ function ensureWritable(source, config) {
|
|
|
339
997
|
}
|
|
340
998
|
}
|
|
341
999
|
function resolveAssetFilePath(source, ref) {
|
|
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
|
+
}
|
|
342
1004
|
const typeDir = stashDirFor(ref.type);
|
|
343
1005
|
if (!typeDir) {
|
|
344
1006
|
throw new UsageError(`Unknown asset type "${ref.type}". Cannot resolve a write path.`, "INVALID_FLAG_VALUE");
|
|
@@ -350,6 +1012,11 @@ function resolveAssetFilePath(source, ref) {
|
|
|
350
1012
|
}
|
|
351
1013
|
return assetPath;
|
|
352
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
|
+
}
|
|
353
1020
|
/**
|
|
354
1021
|
* Reject any kind reaching the write/delete helpers other than the two
|
|
355
1022
|
* supported writable kinds. The config loader is the first line of defence
|
|
@@ -372,7 +1039,12 @@ export function formatRefForMessage(ref) {
|
|
|
372
1039
|
const origin = ref.origin ? sanitizeCommitMessage(ref.origin) : "";
|
|
373
1040
|
const type = sanitizeCommitMessage(ref.type);
|
|
374
1041
|
const name = sanitizeCommitMessage(ref.name);
|
|
375
|
-
|
|
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;
|
|
376
1048
|
}
|
|
377
1049
|
/**
|
|
378
1050
|
* Derive a {@link WriteTargetSource} + persisted {@link SourceConfigEntry}
|
|
@@ -383,8 +1055,6 @@ export function formatRefForMessage(ref) {
|
|
|
383
1055
|
* ConfiguredSource.name → WriteTargetSource.name
|
|
384
1056
|
* ConfiguredSource.source.* → WriteTargetSource.path (via parseSourceSpec)
|
|
385
1057
|
*
|
|
386
|
-
* Legacy aliases (`context-hub`, `github`) have already been normalised to
|
|
387
|
-
* `git` by the config loader, so this mapping is straightforward.
|
|
388
1058
|
*/
|
|
389
1059
|
function adaptConfiguredSource(runtime) {
|
|
390
1060
|
// Map the runtime kind to the write helper's `kind` discriminator. Only
|
|
@@ -401,18 +1071,23 @@ function adaptConfiguredSource(runtime) {
|
|
|
401
1071
|
// root lives in the lock (`localRoot`), NOT the desired config. Resolve there
|
|
402
1072
|
// FIRST — via the SAME shared resolver the indexer READ path uses — so a write
|
|
403
1073
|
// lands in exactly the directory a read walks; git sync/commit then runs
|
|
404
|
-
// against that same root.
|
|
405
|
-
//
|
|
406
|
-
// content/-subdir convention — the identical chain the read path applies.
|
|
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.
|
|
407
1076
|
const lockRoot = kind === "git" ? lockContentRootFor(runtime.name, runtime.type) : undefined;
|
|
408
1077
|
const repoPath = lockRoot ?? pathFromConfiguredSource(runtime);
|
|
409
1078
|
if (!repoPath) {
|
|
410
1079
|
throw new ConfigError(`Source "${runtime.name}" has no resolvable on-disk path; writes are unsupported for this entry.`, "INVALID_CONFIG_FILE");
|
|
411
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);
|
|
412
1087
|
const config = {
|
|
413
1088
|
type: runtime.type,
|
|
414
1089
|
name: runtime.name,
|
|
415
|
-
path:
|
|
1090
|
+
path: componentRoot,
|
|
416
1091
|
...(runtime.writable !== undefined ? { writable: runtime.writable } : {}),
|
|
417
1092
|
...(runtime.options ? { options: runtime.options } : {}),
|
|
418
1093
|
};
|
|
@@ -421,7 +1096,8 @@ function adaptConfiguredSource(runtime) {
|
|
|
421
1096
|
source: {
|
|
422
1097
|
kind,
|
|
423
1098
|
name: runtime.name,
|
|
424
|
-
path:
|
|
1099
|
+
path: componentRoot,
|
|
1100
|
+
adapterId,
|
|
425
1101
|
...(kind === "git" ? { repoPath } : {}),
|
|
426
1102
|
},
|
|
427
1103
|
config,
|