akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4
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 +756 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +2 -7
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +0 -17
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +363 -679
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +87 -406
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -21
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/config/retired-experimental-keys-shim.js +0 -62
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -78
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -13,76 +13,31 @@
|
|
|
13
13
|
* a lock entry is "managed" (installed from a registry and overwritten on
|
|
14
14
|
* `akm bundle update`); a bundle with no lock is a plain filesystem/git/website source.
|
|
15
15
|
*/
|
|
16
|
-
import { createHash } from "node:crypto";
|
|
17
16
|
import fs from "node:fs";
|
|
18
17
|
import path from "node:path";
|
|
19
18
|
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
20
19
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
21
|
-
import {
|
|
20
|
+
import { bundleComponentConfig, getSources, loadConfig, resolveSecret } from "../../core/config/config.js";
|
|
22
21
|
import { AkmError, ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
23
22
|
import { isPathAbsent } from "../../core/path-access.js";
|
|
24
23
|
import { getDbPath, getRegistryCacheDir } from "../../core/paths.js";
|
|
25
|
-
import { beginImmediateTransaction, getStateDbPath, openStateDatabase } from "../../core/state-db.js";
|
|
26
24
|
import { warn } from "../../core/warn.js";
|
|
27
25
|
import { resolveGitContentRoot } from "../../core/write-source.js";
|
|
28
26
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
29
|
-
import { akmIndex
|
|
30
|
-
import {
|
|
27
|
+
import { akmIndex } from "../../indexer/indexer.js";
|
|
28
|
+
import { readLockfile, upsertLockEntry } from "../../integrations/lockfile.js";
|
|
31
29
|
import { parseRegistryRef } from "../../registry/resolve.js";
|
|
32
|
-
import { sha256Hex } from "../../runtime.js";
|
|
33
30
|
import { getCachePaths, parseGitRepoUrl, runGit, syncMirroredRepo } from "../../sources/providers/git.js";
|
|
31
|
+
import { replaceDirectory } from "../../sources/providers/git-install.js";
|
|
34
32
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
35
33
|
import { createWebsiteProvider } from "../../sources/providers/website.js";
|
|
36
34
|
import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
37
35
|
import { ensureWebsiteMirror } from "../../sources/snapshot-fetchers/website-ingest.js";
|
|
38
|
-
import {
|
|
39
|
-
import { closeDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
|
|
36
|
+
import { closeDatabase, openReadonlyExistingDatabase } from "../../storage/repositories/index-connection.js";
|
|
40
37
|
import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
|
|
41
|
-
import { auditStashForDangerousKeys
|
|
38
|
+
import { auditStashForDangerousKeys } from "./dangerous-env-audit.js";
|
|
42
39
|
import { removeInstalledRegistryEntry } from "./source-add.js";
|
|
43
40
|
import { removeStash } from "./source-manage.js";
|
|
44
|
-
import { cleanupStagingParent, createStagingParent, prepareDirectoryPublication, prepareWritableGitPublication, } from "./update-transaction.js";
|
|
45
|
-
function canonicalJson(value) {
|
|
46
|
-
if (Array.isArray(value))
|
|
47
|
-
return `[${value.map(canonicalJson).join(",")}]`;
|
|
48
|
-
if (value && typeof value === "object") {
|
|
49
|
-
const entries = Object.entries(value)
|
|
50
|
-
.filter(([, entry]) => entry !== undefined)
|
|
51
|
-
// Codepoint order, NOT localeCompare: this string feeds sha256Hex for the
|
|
52
|
-
// bundle-audit generation, and localeCompare is ICU/locale-dependent — the
|
|
53
|
-
// same object could hash differently on two machines. Matches the other
|
|
54
|
-
// canonicalJson implementations (workflows/ir/plan-hash.ts).
|
|
55
|
-
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
56
|
-
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
57
|
-
}
|
|
58
|
-
return JSON.stringify(value);
|
|
59
|
-
}
|
|
60
|
-
function bundleAuditGeneration(entry) {
|
|
61
|
-
return sha256Hex(canonicalJson(entry ?? null));
|
|
62
|
-
}
|
|
63
|
-
function bundleAuditFence(config, id) {
|
|
64
|
-
const bundle = config.bundles?.[id];
|
|
65
|
-
if (!bundle)
|
|
66
|
-
throw new ConfigError(`Source "${id}" disappeared before update staging.`);
|
|
67
|
-
return {
|
|
68
|
-
componentRoot: bundleComponentConfig(bundle)?.root ?? ".",
|
|
69
|
-
generation: bundleAuditGeneration(bundle),
|
|
70
|
-
writable: bundleComponentConfig(bundle)?.writable ?? bundle.writable === true,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function assertBundleAuditFence(config, id, expectedGeneration) {
|
|
74
|
-
const current = config.bundles?.[id];
|
|
75
|
-
if (bundleAuditGeneration(current) !== expectedGeneration) {
|
|
76
|
-
throw new ConfigError(`Source "${id}" config changed after its staged bytes were audited; refusing to publish without re-staging and re-auditing.`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
function currentPlainSource(config, id, type) {
|
|
80
|
-
const source = getSources(config).find((candidate) => candidate.name === id && candidate.type === type);
|
|
81
|
-
if (!source) {
|
|
82
|
-
throw new ConfigError(`Source "${id}" changed or disappeared before update staging; retry to re-resolve it.`);
|
|
83
|
-
}
|
|
84
|
-
return source;
|
|
85
|
-
}
|
|
86
41
|
/** Enumerate the registry-managed installs (lock-backed bundles) in a config. */
|
|
87
42
|
function listManagedInstalls(config) {
|
|
88
43
|
const bundles = config.bundles ?? {};
|
|
@@ -105,7 +60,6 @@ function listManagedInstalls(config) {
|
|
|
105
60
|
writable: componentWritable ?? bundle.writable === true,
|
|
106
61
|
componentRoot,
|
|
107
62
|
requiredRoots: lock.localRoot ? [path.resolve(lock.localRoot, componentRoot)] : [],
|
|
108
|
-
auditConfigGeneration: bundleAuditGeneration(bundle),
|
|
109
63
|
...(bundle.credential !== undefined ? { credential: bundle.credential } : {}),
|
|
110
64
|
});
|
|
111
65
|
}
|
|
@@ -169,8 +123,8 @@ function describeComponents(entry, bundleRoot) {
|
|
|
169
123
|
const names = Object.keys(configuredComponents);
|
|
170
124
|
if (names.length === 0) {
|
|
171
125
|
// No explicit component at all — the implicit single component every
|
|
172
|
-
// bundle gets (spec §1.2 rule 5; `deriveInstallations
|
|
173
|
-
//
|
|
126
|
+
// bundle gets (spec §1.2 rule 5; `deriveInstallations` applies the same
|
|
127
|
+
// "main"-shaped default at index time).
|
|
174
128
|
const adapter = bundleRoot !== undefined ? detectAdapterId(bundleRoot) : "akm";
|
|
175
129
|
return [{ name: "main", adapter, detected: true }];
|
|
176
130
|
}
|
|
@@ -415,237 +369,19 @@ function incompleteFilesystemOutcome(id) {
|
|
|
415
369
|
reason: `Filesystem source "${id}" was not scanned completely; preserving its last-known-good index rows.`,
|
|
416
370
|
};
|
|
417
371
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
updateTransactionHookForTests = hook;
|
|
422
|
-
}
|
|
423
|
-
function updateTransactionHook(point, id, context) {
|
|
424
|
-
updateTransactionHookForTests?.(point, id, context);
|
|
425
|
-
}
|
|
426
|
-
function gitHead(repoDir) {
|
|
427
|
-
const result = runGit(["-C", repoDir, "rev-parse", "HEAD"]);
|
|
428
|
-
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : undefined;
|
|
429
|
-
}
|
|
430
|
-
function invalidWritableGitPublication(repoDir) {
|
|
431
|
-
return {
|
|
432
|
-
publish() {
|
|
433
|
-
throw new ConfigError(`Writable Git source at ${repoDir} is not a valid checkout; refusing to replace its live directory.`);
|
|
434
|
-
},
|
|
435
|
-
rollback() { },
|
|
436
|
-
commit() { },
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
function writableGitPublication(opts) {
|
|
440
|
-
const auditedTargetHead = opts.auditedTargetHead ?? gitHead(opts.stagedRepo);
|
|
441
|
-
if (!opts.expectedOldHead || !auditedTargetHead)
|
|
442
|
-
return invalidWritableGitPublication(opts.liveRepo);
|
|
443
|
-
return prepareWritableGitPublication({
|
|
444
|
-
stagedRepo: opts.stagedRepo,
|
|
445
|
-
liveRepo: opts.liveRepo,
|
|
446
|
-
expectedOldHead: opts.expectedOldHead,
|
|
447
|
-
auditedTargetHead,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
const UPDATE_STATE_SCHEMA = "akm_update_state";
|
|
451
|
-
function sqliteStringLiteral(value) {
|
|
452
|
-
return `'${value.replaceAll("'", "''")}'`;
|
|
453
|
-
}
|
|
454
|
-
function openUnifiedUpdateTransaction() {
|
|
455
|
-
// Keep canonical state activity registered for the entire attached
|
|
456
|
-
// transaction so maintenance/migration cannot replace state.db underneath
|
|
457
|
-
// the borrowed index connection.
|
|
458
|
-
const stateActivity = openStateDatabase();
|
|
459
|
-
try {
|
|
460
|
-
const statePath = getStateDbPath();
|
|
461
|
-
const db = openIndexDatabase(undefined, {
|
|
462
|
-
beforeSchema: (candidate) => {
|
|
463
|
-
candidate.exec(`ATTACH DATABASE ${sqliteStringLiteral(statePath)} AS "${UPDATE_STATE_SCHEMA}"`);
|
|
464
|
-
// openIndexDatabase invokes this before ensureSchema, so the outer
|
|
465
|
-
// transaction begins before the first update-owned index mutation.
|
|
466
|
-
beginImmediateTransaction(candidate);
|
|
467
|
-
},
|
|
468
|
-
});
|
|
469
|
-
return {
|
|
470
|
-
db,
|
|
471
|
-
stateActivity,
|
|
472
|
-
deferred: { db, stateSchema: UPDATE_STATE_SCHEMA },
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
catch (error) {
|
|
476
|
-
stateActivity.close();
|
|
477
|
-
throw error;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
function rollbackUnifiedUpdateTransaction(transaction) {
|
|
481
|
-
if (!transaction.db.inTransaction)
|
|
482
|
-
return undefined;
|
|
483
|
-
try {
|
|
484
|
-
transaction.db.exec("ROLLBACK");
|
|
485
|
-
return undefined;
|
|
486
|
-
}
|
|
487
|
-
catch (error) {
|
|
488
|
-
return error;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
function commitUnifiedUpdateTransaction(transaction) {
|
|
492
|
-
if (!transaction.db.inTransaction) {
|
|
493
|
-
throw new Error("Source update index returned without its active unified index/state transaction.");
|
|
494
|
-
}
|
|
495
|
-
transaction.db.exec("COMMIT");
|
|
496
|
-
}
|
|
497
|
-
function closeUnifiedUpdateTransaction(transaction, committed) {
|
|
498
|
-
let closeError = rollbackUnifiedUpdateTransaction(transaction);
|
|
499
|
-
try {
|
|
500
|
-
transaction.db.close();
|
|
501
|
-
}
|
|
502
|
-
catch (error) {
|
|
503
|
-
closeError ??= error;
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
transaction.stateActivity.close();
|
|
507
|
-
}
|
|
508
|
-
catch (error) {
|
|
509
|
-
closeError ??= error;
|
|
510
|
-
}
|
|
511
|
-
if (closeError) {
|
|
512
|
-
warn(committed
|
|
513
|
-
? `[akm bundle update] committed, but closing its database handles failed: ${String(closeError)}`
|
|
514
|
-
: `[akm bundle update] rolled back, but closing its database handles failed: ${String(closeError)}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Run the embedding phase AFTER the coordinator's atomic commit, on its own
|
|
519
|
-
* fresh connection (#954). Before this, `akmUpdate` called
|
|
520
|
-
* `akmIndex()` for its embedding phase too, INSIDE this same unified
|
|
521
|
-
* `BEGIN IMMEDIATE` — so every per-batch commit the materializer opened
|
|
522
|
-
* nested as an unobservable SAVEPOINT, and a SIGKILL mid-run lost every
|
|
523
|
-
* embedding of the run rather than just the one in flight. The
|
|
524
|
-
* ambient-transaction drift guard (#954) now rejects that outright, so
|
|
525
|
-
* `akmIndex`'s own embedding phase is skipped for a deferred update
|
|
526
|
-
* transaction and this runs instead, once content/lock/index/state are
|
|
527
|
-
* already durably committed.
|
|
528
|
-
*
|
|
529
|
-
* A failing pass (provider down, timeout) does NOT fail the update: the
|
|
530
|
-
* bundle content and index are already committed successfully, exactly like
|
|
531
|
-
* a plain `akm index` whose embedding phase fails — only the reported
|
|
532
|
-
* `verification` reflects the shortfall (`semanticStatus: "blocked"`).
|
|
533
|
-
*/
|
|
534
|
-
async function runPostCommitEmbeddingPass(index) {
|
|
535
|
-
const config = loadConfig();
|
|
536
|
-
let db;
|
|
537
|
-
try {
|
|
538
|
-
const embeddingDim = config.embedding?.dimension;
|
|
539
|
-
db = openIndexDatabase(getDbPath(), embeddingDim ? { embeddingDim } : undefined);
|
|
540
|
-
const { verification } = await runEmbeddingPass({ db, config, onProgress: () => { } });
|
|
541
|
-
return { ...index, verification };
|
|
542
|
-
}
|
|
543
|
-
catch (error) {
|
|
544
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
545
|
-
warn(`[akm bundle update] post-commit embedding pass failed: ${message}`);
|
|
546
|
-
return {
|
|
547
|
-
...index,
|
|
548
|
-
verification: { ...index.verification, ok: false, semanticStatus: "blocked", message },
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
finally {
|
|
552
|
-
if (db)
|
|
553
|
-
closeDatabase(db);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
function pathAtOrBelow(candidate, root) {
|
|
557
|
-
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
558
|
-
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
559
|
-
}
|
|
560
|
-
function remapStagedPath(candidate, stagedRoot, liveRoot) {
|
|
561
|
-
if (!pathAtOrBelow(candidate, stagedRoot))
|
|
562
|
-
return candidate;
|
|
563
|
-
return path.join(liveRoot, path.relative(stagedRoot, candidate));
|
|
372
|
+
function createStagingParent(parent) {
|
|
373
|
+
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
374
|
+
return fs.mkdtempSync(path.join(parent, ".akm-update-stage-"));
|
|
564
375
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
async function contentGeneration(root) {
|
|
571
|
-
const hash = createHash("sha256");
|
|
572
|
-
const resolvedRoot = path.resolve(root);
|
|
573
|
-
const walk = async (directory) => {
|
|
574
|
-
const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
575
|
-
for (const entry of entries) {
|
|
576
|
-
if (entry.name === ".git")
|
|
577
|
-
continue;
|
|
578
|
-
const absolute = path.join(directory, entry.name);
|
|
579
|
-
const relative = path.relative(resolvedRoot, absolute).replaceAll(path.sep, "/");
|
|
580
|
-
const before = fs.lstatSync(absolute);
|
|
581
|
-
if (before.isDirectory()) {
|
|
582
|
-
hash.update(`D\0${relative}\0${before.mode & 0o777}\0`);
|
|
583
|
-
await walk(absolute);
|
|
584
|
-
}
|
|
585
|
-
else if (before.isSymbolicLink()) {
|
|
586
|
-
hash.update(`L\0${relative}\0${fs.readlinkSync(absolute)}\0`);
|
|
587
|
-
}
|
|
588
|
-
else if (before.isFile()) {
|
|
589
|
-
hash.update(`F\0${relative}\0${before.mode & 0o777}\0${before.size}\0`);
|
|
590
|
-
for await (const chunk of fs.createReadStream(absolute))
|
|
591
|
-
hash.update(chunk);
|
|
592
|
-
const after = fs.lstatSync(absolute);
|
|
593
|
-
if (after.ino !== before.ino || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
|
|
594
|
-
throw new ConfigError(`Source content changed while ${relative} was being audited; retry the update.`);
|
|
595
|
-
}
|
|
596
|
-
hash.update("\0");
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
throw new ConfigError(`Unsupported filesystem entry ${absolute}; refusing to audit an unstable source tree.`);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
if (!fs.statSync(resolvedRoot).isDirectory())
|
|
604
|
-
throw new ConfigError(`Expected ${resolvedRoot} to be a directory.`);
|
|
605
|
-
await walk(resolvedRoot);
|
|
606
|
-
return hash.digest("hex");
|
|
607
|
-
}
|
|
608
|
-
async function verifyPublishedAudit(prepared, id) {
|
|
609
|
-
const liveRoot = prepared.publishedAuditRoot;
|
|
610
|
-
if (!liveRoot)
|
|
611
|
-
return;
|
|
612
|
-
const resolveLiveAuditRoot = () => {
|
|
613
|
-
const physicalRoot = canonicalWritablePath(liveRoot, `published component root for "${id}"`);
|
|
614
|
-
const containmentRoot = prepared.publishedAuditContainmentRoot;
|
|
615
|
-
if (containmentRoot && !pathAtOrBelow(physicalRoot, containmentRoot)) {
|
|
616
|
-
throw new ConfigError(`Published component root ${liveRoot} resolves outside its physical checkout at ${containmentRoot}.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
617
|
-
}
|
|
618
|
-
const expected = prepared.publishedAuditExpectedPhysicalRoot;
|
|
619
|
-
if (expected && path.resolve(physicalRoot) !== path.resolve(expected)) {
|
|
620
|
-
throw new ConfigError(`Published component root ${liveRoot} resolves to ${physicalRoot}, not its audited staged target ${expected}.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
621
|
-
}
|
|
622
|
-
return physicalRoot;
|
|
623
|
-
};
|
|
624
|
-
const beforeRoot = resolveLiveAuditRoot();
|
|
625
|
-
const beforeAudit = await contentGeneration(beforeRoot);
|
|
626
|
-
let findings;
|
|
627
|
-
try {
|
|
628
|
-
const scanRoot = resolveLiveAuditRoot();
|
|
629
|
-
if (scanRoot !== beforeRoot)
|
|
630
|
-
throw new Error("configured component symlink changed during re-audit");
|
|
631
|
-
findings = scanStashForDangerousKeys(scanRoot);
|
|
632
|
-
}
|
|
633
|
-
catch (error) {
|
|
634
|
-
throw new ConfigError(`Dangerous environment-key re-audit failed after materializing "${id}"; rolling the update back. ${error instanceof Error ? error.message : String(error)}`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
635
|
-
}
|
|
636
|
-
const afterRoot = resolveLiveAuditRoot();
|
|
637
|
-
const afterAudit = await contentGeneration(afterRoot);
|
|
638
|
-
if (afterRoot !== beforeRoot ||
|
|
639
|
-
beforeAudit !== prepared.auditedContentGeneration ||
|
|
640
|
-
afterAudit !== prepared.auditedContentGeneration ||
|
|
641
|
-
dangerousFindingGeneration(findings) !== prepared.approvedDangerousFindings) {
|
|
642
|
-
throw new ConfigError(`Writable source "${id}" changed while its audited Git target was materialized; refusing to activate unaudited bytes.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
643
|
-
}
|
|
376
|
+
/** A bundle's single component root and writability. */
|
|
377
|
+
function bundleComponent(config, id) {
|
|
378
|
+
const bundle = config.bundles?.[id];
|
|
379
|
+
const component = bundleComponentConfig(bundle);
|
|
380
|
+
return { root: component?.root ?? ".", writable: component?.writable ?? bundle?.writable === true };
|
|
644
381
|
}
|
|
645
|
-
async function
|
|
646
|
-
updateTransactionHook("staged", id);
|
|
382
|
+
async function auditStagedUpdate(auditRoot, ref, allowDangerousEnvKeys) {
|
|
647
383
|
const decision = await auditStashForDangerousKeys({
|
|
648
|
-
stashRoot:
|
|
384
|
+
stashRoot: auditRoot,
|
|
649
385
|
ref,
|
|
650
386
|
allowDangerousKeys: allowDangerousEnvKeys,
|
|
651
387
|
isTTY: process.stdin.isTTY === true,
|
|
@@ -654,347 +390,179 @@ async function auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys) {
|
|
|
654
390
|
});
|
|
655
391
|
if (decision.blocked)
|
|
656
392
|
throw new NotFoundError(decision.error, decision.code);
|
|
657
|
-
prepared.approvedDangerousFindings = dangerousFindingGeneration(decision.findings);
|
|
658
|
-
prepared.auditedContentGeneration = await contentGeneration(prepared.auditRoot);
|
|
659
|
-
updateTransactionHook("audited", id);
|
|
660
393
|
}
|
|
661
|
-
function
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
catch (error) {
|
|
666
|
-
throw new ConfigError(`Cannot resolve ${description} at ${candidate}; refusing writable Git update. ${error instanceof Error ? error.message : String(error)}`);
|
|
394
|
+
function gitOrThrow(repoDir, args) {
|
|
395
|
+
const result = runGit(["-C", repoDir, ...args], { timeout: 120_000 });
|
|
396
|
+
if (result.status !== 0) {
|
|
397
|
+
throw new UsageError(`git ${args[0]} failed in ${repoDir}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
|
|
667
398
|
}
|
|
399
|
+
return result.stdout.trim();
|
|
668
400
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Publish an audited writable checkout: fast-forward the live checkout to
|
|
403
|
+
* exactly the staged commit. Git refuses a non-fast-forward or a merge that
|
|
404
|
+
* would overwrite local work, and changes nothing in that case.
|
|
405
|
+
*/
|
|
406
|
+
function fastForwardCheckout(liveRepo, stagedRepo) {
|
|
407
|
+
const head = gitOrThrow(stagedRepo, ["rev-parse", "HEAD"]);
|
|
408
|
+
gitOrThrow(liveRepo, ["fetch", "--no-tags", stagedRepo, head]);
|
|
409
|
+
gitOrThrow(liveRepo, ["merge", "--ff-only", "--no-overwrite-ignore", head]);
|
|
675
410
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
// content root. Resolving it to today's symlink target here would pin the old
|
|
690
|
-
// target and miss an upstream commit that retargets the symlink.
|
|
691
|
-
const lexicalLocalRoot = path.resolve(managed.localRoot);
|
|
692
|
-
const configuredLiveRoots = managed.requiredRoots.map((root) => {
|
|
693
|
-
const lexicalRoot = path.resolve(root);
|
|
694
|
-
if (!pathAtOrBelow(lexicalRoot, lexicalLocalRoot)) {
|
|
695
|
-
throw new ConfigError(`Configured writable Git component root ${lexicalRoot} escapes content root ${lexicalLocalRoot}.`);
|
|
696
|
-
}
|
|
697
|
-
return path.resolve(physicalLocalRoot, path.relative(lexicalLocalRoot, lexicalRoot));
|
|
698
|
-
});
|
|
699
|
-
const physicalRequiredRoots = configuredLiveRoots.map((root) => containedWritablePath(root, liveRepo, `Configured component root for "${managed.installId}"`));
|
|
700
|
-
// The Git provider must preserve ordinary component directories, but not a
|
|
701
|
-
// symlink's current physical target. The configured lexical path is audited
|
|
702
|
-
// again after sync, so retaining the old target here would reject a valid
|
|
703
|
-
// upstream retarget before the new target can reach that audit.
|
|
704
|
-
const preservedPhysicalRequiredRoots = physicalRequiredRoots.filter((root, index) => configuredLiveRoots[index] === root);
|
|
705
|
-
const expectedOldHead = gitHead(liveRepo);
|
|
706
|
-
const stagingParent = createStagingParent(path.dirname(liveRepo));
|
|
707
|
-
const stagedRepo = path.join(stagingParent, path.basename(liveRepo));
|
|
708
|
-
fs.cpSync(liveRepo, stagedRepo, { recursive: true, preserveTimestamps: true, verbatimSymlinks: true });
|
|
709
|
-
const stagedContentRoot = remapStagedPath(physicalLocalRoot, liveRepo, stagedRepo);
|
|
710
|
-
const stagedPhysicalRequiredRoots = preservedPhysicalRequiredRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
|
|
711
|
-
const stagedConfiguredRoots = configuredLiveRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
|
|
712
|
-
return syncFromRef(managed.ref, {
|
|
713
|
-
force,
|
|
714
|
-
writable: true,
|
|
715
|
-
writableRoot: stagedContentRoot,
|
|
716
|
-
...(stagedPhysicalRequiredRoots.length > 0 ? { writableRequiredRoots: stagedPhysicalRequiredRoots } : {}),
|
|
717
|
-
...(managed.credential ? { credential: resolveSecret(managed.credential, storeSecretResolver.resolveSecret) } : {}),
|
|
718
|
-
})
|
|
719
|
-
.then((staged) => {
|
|
720
|
-
const auditedTargetHead = gitHead(stagedRepo);
|
|
721
|
-
const physicalStagedRepo = canonicalWritablePath(stagedRepo, "staged managed Git checkout");
|
|
722
|
-
const auditedConfiguredRoots = stagedConfiguredRoots.map((root) => containedWritablePath(root, physicalStagedRepo, `Post-sync component root for "${managed.installId}"`));
|
|
723
|
-
const expectedPublishedRoots = auditedConfiguredRoots.map((root) => path.join(liveRepo, path.relative(physicalStagedRepo, root)));
|
|
724
|
-
const synced = {
|
|
725
|
-
...staged,
|
|
726
|
-
resolvedRevision: auditedTargetHead ?? staged.resolvedRevision,
|
|
727
|
-
contentDir: remapStagedPath(staged.contentDir, stagedRepo, liveRepo),
|
|
728
|
-
cacheDir: remapStagedPath(staged.cacheDir, stagedRepo, liveRepo),
|
|
729
|
-
extractedDir: remapStagedPath(staged.extractedDir, stagedRepo, liveRepo),
|
|
730
|
-
};
|
|
731
|
-
return {
|
|
732
|
-
synced,
|
|
733
|
-
auditRoot: auditedConfiguredRoots[0] ?? staged.contentDir,
|
|
734
|
-
publishedAuditRoot: configuredLiveRoots[0] ?? synced.contentDir,
|
|
735
|
-
publishedAuditContainmentRoot: liveRepo,
|
|
736
|
-
publishedAuditExpectedPhysicalRoot: expectedPublishedRoots[0] ?? synced.contentDir,
|
|
737
|
-
publication: writableGitPublication({ stagedRepo, liveRepo, expectedOldHead, auditedTargetHead }),
|
|
738
|
-
cleanup: () => cleanupStagingParent(stagingParent),
|
|
739
|
-
};
|
|
740
|
-
})
|
|
741
|
-
.catch((error) => {
|
|
742
|
-
cleanupStagingParent(stagingParent);
|
|
743
|
-
throw error;
|
|
411
|
+
/**
|
|
412
|
+
* Reindex after an update. Sources are not re-fetched (the update just did
|
|
413
|
+
* that) and detected adapters are not written to config.json — an update
|
|
414
|
+
* leaves config alone; the next `akm index` records them.
|
|
415
|
+
*/
|
|
416
|
+
function reindexAfterUpdate(stashDir) {
|
|
417
|
+
return akmIndex({ stashDir, full: true, hydrateSources: false, persistDetectedAdapters: false });
|
|
418
|
+
}
|
|
419
|
+
/** Publish, then reindex, holding the asset-mutation lease so no other writer interleaves. */
|
|
420
|
+
function publishAndIndex(stashDir, publish) {
|
|
421
|
+
return withAssetMutationLease("source-update", async () => {
|
|
422
|
+
await publish();
|
|
423
|
+
return reindexAfterUpdate(stashDir);
|
|
744
424
|
});
|
|
745
425
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
426
|
+
/** Fetch a registry-managed install into staging without touching its live content. */
|
|
427
|
+
async function stageManagedUpdate(managed, force) {
|
|
428
|
+
const credential = managed.credential
|
|
429
|
+
? resolveSecret(managed.credential, storeSecretResolver.resolveSecret)
|
|
430
|
+
: undefined;
|
|
431
|
+
if (managed.writable && managed.localRoot) {
|
|
432
|
+
// A writable install is a checkout that may carry local commits: the
|
|
433
|
+
// provider fast-forwards a copy, the copy is audited, and the live
|
|
434
|
+
// checkout is then fast-forwarded to exactly the audited commit.
|
|
435
|
+
const topLevel = runGit(["-C", managed.localRoot, "rev-parse", "--show-toplevel"]);
|
|
436
|
+
if (topLevel.status !== 0 || !topLevel.stdout.trim()) {
|
|
437
|
+
throw new UsageError(`Writable Git install at ${managed.localRoot} is not a checkout; refusing to update it.`);
|
|
438
|
+
}
|
|
439
|
+
const liveRepo = topLevel.stdout.trim();
|
|
440
|
+
const stagingParent = createStagingParent(path.dirname(liveRepo));
|
|
441
|
+
try {
|
|
442
|
+
const stagedRepo = path.join(stagingParent, path.basename(liveRepo));
|
|
443
|
+
fs.cpSync(liveRepo, stagedRepo, { recursive: true, preserveTimestamps: true, verbatimSymlinks: true });
|
|
444
|
+
const stagedRoot = path.join(stagedRepo, path.relative(liveRepo, fs.realpathSync(managed.localRoot)));
|
|
445
|
+
const fetched = await syncFromRef(managed.ref, {
|
|
446
|
+
force,
|
|
447
|
+
writable: true,
|
|
448
|
+
writableRoot: stagedRoot,
|
|
449
|
+
writableRequiredRoots: managed.requiredRoots.map((root) => path.join(stagedRoot, path.relative(managed.localRoot, root))),
|
|
450
|
+
...(credential ? { credential } : {}),
|
|
451
|
+
});
|
|
452
|
+
return {
|
|
453
|
+
synced: { ...fetched, contentDir: managed.localRoot, cacheDir: liveRepo, extractedDir: liveRepo },
|
|
454
|
+
staged: {
|
|
455
|
+
auditRoot: path.resolve(fetched.contentDir, managed.componentRoot),
|
|
456
|
+
publish: () => fastForwardCheckout(liveRepo, stagedRepo),
|
|
457
|
+
cleanup: () => cleanupDirectoryBestEffort(stagingParent, "bundle update"),
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
cleanupDirectoryBestEffort(stagingParent, "bundle update");
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
749
466
|
if (managed.source === "local") {
|
|
467
|
+
// The user's own directory: nothing to fetch or publish.
|
|
750
468
|
const synced = await syncFromRef(managed.ref, { force, writable: managed.writable });
|
|
751
469
|
return {
|
|
752
470
|
synced,
|
|
753
|
-
auditRoot:
|
|
754
|
-
cleanup: () => { },
|
|
471
|
+
staged: { auditRoot: path.resolve(synced.contentDir, managed.componentRoot), publish() { }, cleanup() { } },
|
|
755
472
|
};
|
|
756
473
|
}
|
|
757
|
-
const
|
|
758
|
-
const stagingParent = createStagingParent(
|
|
474
|
+
const cacheRoot = getRegistryCacheDir();
|
|
475
|
+
const stagingParent = createStagingParent(cacheRoot);
|
|
759
476
|
try {
|
|
760
|
-
const
|
|
477
|
+
const fetched = await syncFromRef(managed.ref, {
|
|
761
478
|
force,
|
|
762
479
|
writable: false,
|
|
763
480
|
cacheRootDir: stagingParent,
|
|
764
|
-
...(
|
|
765
|
-
? { credential: resolveSecret(managed.credential, storeSecretResolver.resolveSecret) }
|
|
766
|
-
: {}),
|
|
481
|
+
...(credential ? { credential } : {}),
|
|
767
482
|
});
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
// invisible until the lock points at it, so no directory promotion is needed.
|
|
771
|
-
return {
|
|
772
|
-
synced: staged,
|
|
773
|
-
auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
|
|
774
|
-
cleanup: () => cleanupStagingParent(stagingParent),
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
const liveCacheDir = path.join(canonicalCacheRoot, path.relative(stagingParent, staged.cacheDir));
|
|
778
|
-
const synced = {
|
|
779
|
-
...staged,
|
|
780
|
-
contentDir: remapStagedPath(staged.contentDir, staged.cacheDir, liveCacheDir),
|
|
781
|
-
cacheDir: liveCacheDir,
|
|
782
|
-
extractedDir: remapStagedPath(staged.extractedDir, staged.cacheDir, liveCacheDir),
|
|
783
|
-
};
|
|
483
|
+
const liveCacheDir = path.join(cacheRoot, path.relative(stagingParent, fetched.cacheDir));
|
|
484
|
+
const toLive = (candidate) => path.join(liveCacheDir, path.relative(fetched.cacheDir, candidate));
|
|
784
485
|
return {
|
|
785
|
-
synced
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
486
|
+
synced: {
|
|
487
|
+
...fetched,
|
|
488
|
+
cacheDir: liveCacheDir,
|
|
489
|
+
contentDir: toLive(fetched.contentDir),
|
|
490
|
+
extractedDir: toLive(fetched.extractedDir),
|
|
491
|
+
},
|
|
492
|
+
staged: {
|
|
493
|
+
auditRoot: path.resolve(fetched.contentDir, managed.componentRoot),
|
|
494
|
+
publish: () => replaceDirectory(fetched.cacheDir, liveCacheDir),
|
|
495
|
+
cleanup: () => cleanupDirectoryBestEffort(stagingParent, "bundle update"),
|
|
496
|
+
},
|
|
789
497
|
};
|
|
790
498
|
}
|
|
791
499
|
catch (error) {
|
|
792
|
-
|
|
500
|
+
cleanupDirectoryBestEffort(stagingParent, "bundle update");
|
|
793
501
|
throw error;
|
|
794
502
|
}
|
|
795
503
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
return resolved;
|
|
802
|
-
}
|
|
803
|
-
async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, full, expectedConfigGeneration) {
|
|
804
|
-
try {
|
|
805
|
-
await auditPreparedUpdate(prepared, id, ref, allowDangerousEnvKeys);
|
|
806
|
-
return await withAssetMutationLease("source-update", async () => {
|
|
807
|
-
let transaction;
|
|
808
|
-
let releaseConfigFence;
|
|
809
|
-
let contentPublished = false;
|
|
810
|
-
let committed = false;
|
|
811
|
-
try {
|
|
812
|
-
const configFence = acquireConfigReadFence();
|
|
813
|
-
releaseConfigFence = configFence.release;
|
|
814
|
-
assertBundleAuditFence(configFence.config, id, expectedConfigGeneration);
|
|
815
|
-
updateTransactionHook("fenced", id);
|
|
816
|
-
if (prepared.publication) {
|
|
817
|
-
prepared.publication.publish();
|
|
818
|
-
contentPublished = true;
|
|
819
|
-
}
|
|
820
|
-
await verifyPublishedAudit(prepared, id);
|
|
821
|
-
updateTransactionHook("published", id);
|
|
822
|
-
updateTransactionHook("before-index", id);
|
|
823
|
-
transaction = openUnifiedUpdateTransaction();
|
|
824
|
-
const index = await akmIndex({
|
|
825
|
-
stashDir,
|
|
826
|
-
...(full ? { full: true } : {}),
|
|
827
|
-
hydrateSources: false,
|
|
828
|
-
persistDetectedAdapters: false,
|
|
829
|
-
deferredUpdateTransaction: transaction.deferred,
|
|
830
|
-
});
|
|
831
|
-
updateTransactionHook("indexed", id, { db: transaction.db });
|
|
832
|
-
// Re-fence the exact published worktree after the potentially long
|
|
833
|
-
// index pass and immediately before the coordinator commit. This
|
|
834
|
-
// catches cooperating or observable external writes that land after
|
|
835
|
-
// the first post-materialization audit.
|
|
836
|
-
await verifyPublishedAudit(prepared, id);
|
|
837
|
-
updateTransactionHook("before-commit", id, { db: transaction.db });
|
|
838
|
-
commitUnifiedUpdateTransaction(transaction);
|
|
839
|
-
committed = true;
|
|
840
|
-
prepared.publication?.commit();
|
|
841
|
-
return await runPostCommitEmbeddingPass(index);
|
|
842
|
-
}
|
|
843
|
-
catch (error) {
|
|
844
|
-
let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
|
|
845
|
-
try {
|
|
846
|
-
if (contentPublished)
|
|
847
|
-
prepared.publication?.rollback();
|
|
848
|
-
}
|
|
849
|
-
catch (rollbackError) {
|
|
850
|
-
recoveryError ??= rollbackError;
|
|
851
|
-
}
|
|
852
|
-
if (recoveryError)
|
|
853
|
-
throw recoveryError;
|
|
854
|
-
throw error;
|
|
855
|
-
}
|
|
856
|
-
finally {
|
|
857
|
-
try {
|
|
858
|
-
if (transaction)
|
|
859
|
-
closeUnifiedUpdateTransaction(transaction, committed);
|
|
860
|
-
}
|
|
861
|
-
finally {
|
|
862
|
-
releaseConfigFence?.();
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
finally {
|
|
868
|
-
prepared.cleanup();
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
|
|
504
|
+
/** Fetch a plain git source into staging, audit it, publish it, and reindex. */
|
|
505
|
+
async function syncGitPlainSource(config, gitSource, stashDir, allowDangerousEnvKeys) {
|
|
506
|
+
const id = gitSource.name ?? gitSource.url ?? "";
|
|
507
|
+
const ref = gitSource.url ?? "";
|
|
872
508
|
if (!gitSource.url)
|
|
873
|
-
throw new ConfigError(`Git source "${
|
|
874
|
-
const
|
|
875
|
-
const
|
|
509
|
+
throw new ConfigError(`Git source "${id}" has no URL.`);
|
|
510
|
+
const component = bundleComponent(config, id);
|
|
511
|
+
const canonicalUrl = parseGitRepoUrl(gitSource.url).canonicalUrl;
|
|
512
|
+
const livePaths = getCachePaths(canonicalUrl);
|
|
876
513
|
const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
|
|
877
|
-
const stagedPaths = getCachePaths(repo.canonicalUrl, stagingParent);
|
|
878
514
|
try {
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
515
|
+
const stagedPaths = getCachePaths(canonicalUrl, stagingParent);
|
|
516
|
+
// A writable mirror may carry local commits: pull into a copy, then
|
|
517
|
+
// fast-forward the live checkout once the copy passes the audit.
|
|
518
|
+
const liveCheckout = component.writable && fs.existsSync(livePaths.repoDir);
|
|
519
|
+
if (liveCheckout)
|
|
882
520
|
fs.cpSync(livePaths.rootDir, stagedPaths.rootDir, { recursive: true, preserveTimestamps: true });
|
|
883
|
-
|
|
884
|
-
const staged = await syncMirroredRepo(gitSource, {
|
|
521
|
+
await syncMirroredRepo(gitSource, {
|
|
885
522
|
force: true,
|
|
886
|
-
writable,
|
|
523
|
+
writable: component.writable,
|
|
887
524
|
cacheRootDir: stagingParent,
|
|
888
525
|
...(gitSource.credential
|
|
889
526
|
? { credential: resolveSecret(gitSource.credential, storeSecretResolver.resolveSecret) }
|
|
890
527
|
: {}),
|
|
891
528
|
});
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
// Audit it, but do not move an unowned directory into the canonical
|
|
899
|
-
// cache. Real providers honor cacheRootDir and use the promotion below.
|
|
900
|
-
return {
|
|
901
|
-
auditRoot: configuredStagedAuditRoot,
|
|
902
|
-
cleanup: () => cleanupStagingParent(stagingParent),
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
let auditRoot = configuredStagedAuditRoot;
|
|
906
|
-
let publishedAuditRoot;
|
|
907
|
-
let publishedAuditContainmentRoot;
|
|
908
|
-
let publishedAuditExpectedPhysicalRoot;
|
|
909
|
-
if (writable) {
|
|
910
|
-
const physicalStagedRepo = canonicalWritablePath(stagedPaths.repoDir, "staged plain Git checkout");
|
|
911
|
-
auditRoot = containedWritablePath(configuredStagedAuditRoot, physicalStagedRepo, `Post-sync component root for "${gitSource.name ?? "git"}"`);
|
|
912
|
-
publishedAuditRoot = remapStagedPath(configuredStagedAuditRoot, staged.cacheDir, livePaths.rootDir);
|
|
913
|
-
if (hasLiveCheckout) {
|
|
914
|
-
publishedAuditContainmentRoot = canonicalWritablePath(livePaths.repoDir, "plain Git checkout");
|
|
915
|
-
}
|
|
916
|
-
else {
|
|
917
|
-
const physicalCacheParent = canonicalWritablePath(path.dirname(livePaths.rootDir), "plain Git cache parent");
|
|
918
|
-
publishedAuditContainmentRoot = path.join(physicalCacheParent, path.basename(livePaths.rootDir), path.relative(livePaths.rootDir, livePaths.repoDir));
|
|
919
|
-
}
|
|
920
|
-
publishedAuditExpectedPhysicalRoot = path.join(publishedAuditContainmentRoot, path.relative(physicalStagedRepo, auditRoot));
|
|
921
|
-
}
|
|
922
|
-
return {
|
|
923
|
-
auditRoot,
|
|
924
|
-
...(publishedAuditRoot ? { publishedAuditRoot } : {}),
|
|
925
|
-
...(publishedAuditContainmentRoot ? { publishedAuditContainmentRoot } : {}),
|
|
926
|
-
...(publishedAuditExpectedPhysicalRoot ? { publishedAuditExpectedPhysicalRoot } : {}),
|
|
927
|
-
publication: writable && hasLiveCheckout
|
|
928
|
-
? writableGitPublication({
|
|
929
|
-
stagedRepo: stagedPaths.repoDir,
|
|
930
|
-
liveRepo: livePaths.repoDir,
|
|
931
|
-
expectedOldHead,
|
|
932
|
-
})
|
|
933
|
-
: prepareDirectoryPublication(staged.cacheDir, livePaths.rootDir),
|
|
934
|
-
cleanup: () => cleanupStagingParent(stagingParent),
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
catch (error) {
|
|
938
|
-
cleanupStagingParent(stagingParent);
|
|
939
|
-
throw error;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
async function syncGitPlainSource(gitSource, stashDir, allowDangerousEnvKeys) {
|
|
943
|
-
const id = gitSource.name ?? gitSource.url ?? "";
|
|
944
|
-
const currentConfig = loadConfig();
|
|
945
|
-
const currentSource = currentPlainSource(currentConfig, id, "git");
|
|
946
|
-
const ref = currentSource.url ?? "";
|
|
947
|
-
const fence = bundleAuditFence(currentConfig, id);
|
|
948
|
-
const prepared = await prepareGitPlainUpdate(currentSource, fence.componentRoot, fence.writable);
|
|
949
|
-
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowDangerousEnvKeys, true, fence.generation);
|
|
950
|
-
return { item: { id, kind: "git", ref }, index };
|
|
951
|
-
}
|
|
952
|
-
async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
|
|
953
|
-
if (!websiteSource.url)
|
|
954
|
-
throw new ConfigError(`Website source "${websiteSource.name ?? "website"}" has no URL.`);
|
|
955
|
-
const livePaths = getWebsiteCachePaths(websiteSource.url);
|
|
956
|
-
const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
|
|
957
|
-
const stagedPaths = getWebsiteCachePaths(websiteSource.url, stagingParent);
|
|
958
|
-
try {
|
|
959
|
-
const provider = createWebsiteProvider(websiteSource);
|
|
960
|
-
await provider.sync({
|
|
961
|
-
force: true,
|
|
962
|
-
secrets: storeSecretResolver,
|
|
963
|
-
ensureWebsiteMirror: (config, options) => ensureWebsiteMirror(config, {
|
|
964
|
-
...options,
|
|
965
|
-
resolveSecret: options?.resolveSecret ?? storeSecretResolver.resolveSecret,
|
|
966
|
-
cacheRootDir: stagingParent,
|
|
967
|
-
}),
|
|
529
|
+
await auditStagedUpdate(path.resolve(resolveGitContentRoot(stagedPaths.repoDir), component.root), ref, allowDangerousEnvKeys);
|
|
530
|
+
const index = await publishAndIndex(stashDir, () => {
|
|
531
|
+
if (liveCheckout)
|
|
532
|
+
fastForwardCheckout(livePaths.repoDir, stagedPaths.repoDir);
|
|
533
|
+
else
|
|
534
|
+
replaceDirectory(stagedPaths.rootDir, livePaths.rootDir);
|
|
968
535
|
});
|
|
969
|
-
return {
|
|
970
|
-
auditRoot: resolveComponentAuditRoot(stagedPaths.stashDir, componentRoot, websiteSource.name ?? "website"),
|
|
971
|
-
publication: prepareDirectoryPublication(stagedPaths.rootDir, livePaths.rootDir),
|
|
972
|
-
cleanup: () => cleanupStagingParent(stagingParent),
|
|
973
|
-
};
|
|
536
|
+
return { item: { id, kind: "git", ref }, index };
|
|
974
537
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
throw error;
|
|
538
|
+
finally {
|
|
539
|
+
cleanupDirectoryBestEffort(stagingParent, "bundle update");
|
|
978
540
|
}
|
|
979
541
|
}
|
|
980
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Re-crawl a website source and reindex. The website provider already builds
|
|
544
|
+
* its snapshot in a staging directory and swaps it in, so a killed refresh
|
|
545
|
+
* keeps the previous mirror; a snapshot holds only knowledge pages, so there
|
|
546
|
+
* is no env file to audit.
|
|
547
|
+
*/
|
|
548
|
+
async function syncWebsitePlainSource(websiteSource, stashDir) {
|
|
981
549
|
const id = websiteSource.name ?? websiteSource.url ?? "";
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const index = await
|
|
988
|
-
return { item: { id, kind: "website", ref }, index };
|
|
550
|
+
await createWebsiteProvider(websiteSource).sync({
|
|
551
|
+
force: true,
|
|
552
|
+
secrets: storeSecretResolver,
|
|
553
|
+
ensureWebsiteMirror,
|
|
554
|
+
});
|
|
555
|
+
const index = await reindexAfterUpdate(stashDir);
|
|
556
|
+
return { item: { id, kind: "website", ref: websiteSource.url ?? "" }, index };
|
|
989
557
|
}
|
|
990
558
|
/** Sync a git-mirrored (plain) source and return an UpdateResponse (single-target path). */
|
|
991
|
-
async function updateGitSource(stashDir, target, all, gitSource, allowDangerousEnvKeys) {
|
|
992
|
-
const synced = await syncGitPlainSource(gitSource, stashDir, allowDangerousEnvKeys);
|
|
559
|
+
async function updateGitSource(config, stashDir, target, all, gitSource, allowDangerousEnvKeys) {
|
|
560
|
+
const synced = await syncGitPlainSource(config, gitSource, stashDir, allowDangerousEnvKeys);
|
|
993
561
|
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
994
562
|
}
|
|
995
563
|
/** Re-crawl a website (plain) source and return an UpdateResponse (single-target path). */
|
|
996
|
-
async function updateWebsiteSource(stashDir, target, all, websiteSource
|
|
997
|
-
const synced = await syncWebsitePlainSource(websiteSource, stashDir
|
|
564
|
+
async function updateWebsiteSource(stashDir, target, all, websiteSource) {
|
|
565
|
+
const synced = await syncWebsitePlainSource(websiteSource, stashDir);
|
|
998
566
|
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
999
567
|
}
|
|
1000
568
|
/** Reconcile a filesystem source's current bytes without provider hydration. */
|
|
@@ -1025,29 +593,23 @@ async function updateFilesystemSource(stashDir, target, all, filesystemSource) {
|
|
|
1025
593
|
* {@link updateManagedInstall} verbatim rather than duplicating its lock/config
|
|
1026
594
|
* bookkeeping.
|
|
1027
595
|
*/
|
|
1028
|
-
function managedInstallViewOfPlainNpm(npmSource) {
|
|
596
|
+
function managedInstallViewOfPlainNpm(config, npmSource) {
|
|
1029
597
|
const id = npmSource.name ?? npmSource.path ?? "";
|
|
1030
|
-
const
|
|
1031
|
-
const currentSource = currentPlainSource(config, id, "npm");
|
|
1032
|
-
const spec = currentSource.path ?? "";
|
|
1033
|
-
const ref = spec.startsWith("npm:") ? spec : `npm:${spec}`;
|
|
1034
|
-
const fence = bundleAuditFence(config, id);
|
|
598
|
+
const spec = npmSource.path ?? "";
|
|
1035
599
|
return {
|
|
1036
600
|
bundleKey: id,
|
|
1037
601
|
installId: id,
|
|
1038
602
|
source: "npm",
|
|
1039
|
-
ref
|
|
603
|
+
ref: spec.startsWith("npm:") ? spec : `npm:${spec}`,
|
|
1040
604
|
localRoot: "",
|
|
1041
|
-
resolvedVersion: undefined,
|
|
1042
|
-
resolvedRevision: undefined,
|
|
1043
605
|
writable: false,
|
|
1044
|
-
componentRoot:
|
|
606
|
+
componentRoot: bundleComponent(config, id).root,
|
|
1045
607
|
requiredRoots: [],
|
|
1046
|
-
auditConfigGeneration: fence.generation,
|
|
1047
608
|
};
|
|
1048
609
|
}
|
|
1049
610
|
/**
|
|
1050
|
-
* Sync a single registry-managed install and return the processed record
|
|
611
|
+
* Sync a single registry-managed install and return the processed record:
|
|
612
|
+
* fetch into staging, audit, publish, write its lock entry, reindex.
|
|
1051
613
|
*
|
|
1052
614
|
* `yes` gates ONLY the destructive branch below (deleting a previous
|
|
1053
615
|
* `localRoot` whose content moved) — it has no effect on the sync itself. A
|
|
@@ -1058,16 +620,9 @@ function managedInstallViewOfPlainNpm(npmSource) {
|
|
|
1058
620
|
* command).
|
|
1059
621
|
*/
|
|
1060
622
|
async function updateManagedInstall(managed, force, yes, stashDir, allowDangerousEnvKeys) {
|
|
1061
|
-
|
|
1062
|
-
// re-materialize staging-first (git clones into a `.tmp-*` sibling and swaps
|
|
1063
|
-
// only on success), so destroying `managed.localRoot` BEFORE `syncFromRef`
|
|
1064
|
-
// succeeds would turn any sync failure (network down, bad ref) into losing a
|
|
1065
|
-
// previously-working install. The old root is cleaned up below only after
|
|
1066
|
-
// config/lock publication and reindexing both succeed.
|
|
1067
|
-
const prepared = await prepareManagedUpdate(managed, force);
|
|
623
|
+
const { synced, staged } = await stageManagedUpdate(managed, force);
|
|
1068
624
|
try {
|
|
1069
|
-
await
|
|
1070
|
-
const synced = prepared.synced;
|
|
625
|
+
await auditStagedUpdate(staged.auditRoot, managed.ref, allowDangerousEnvKeys);
|
|
1071
626
|
const installedEntry = {
|
|
1072
627
|
id: managed.installId,
|
|
1073
628
|
// Preserve the original source classification. syncFromRef() re-derives the
|
|
@@ -1093,10 +648,9 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowDangerou
|
|
|
1093
648
|
if (!confirmed)
|
|
1094
649
|
throw new UsageError(`Update cancelled for "${managed.installId}"; no configuration was changed.`);
|
|
1095
650
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const desiredLock = {
|
|
651
|
+
const index = await publishAndIndex(stashDir, async () => {
|
|
652
|
+
staged.publish();
|
|
653
|
+
await upsertLockEntry({
|
|
1100
654
|
id: managed.bundleKey,
|
|
1101
655
|
// Preserve the STORED install kind: a `github:`-ref entry recorded as
|
|
1102
656
|
// source "git" must not be reclassified by the sync flow's re-derivation.
|
|
@@ -1107,142 +661,42 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowDangerou
|
|
|
1107
661
|
integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
|
|
1108
662
|
localRoot: synced.contentDir,
|
|
1109
663
|
installedAt: synced.syncedAt,
|
|
1110
|
-
};
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
let contentPublished = false;
|
|
1116
|
-
let lockPublished = false;
|
|
1117
|
-
let publishedLockSnapshot;
|
|
1118
|
-
let committed = false;
|
|
1119
|
-
let index;
|
|
1120
|
-
try {
|
|
1121
|
-
const configFence = acquireConfigReadFence();
|
|
1122
|
-
releaseConfigFence = configFence.release;
|
|
1123
|
-
assertBundleAuditFence(configFence.config, managed.bundleKey, managed.auditConfigGeneration);
|
|
1124
|
-
updateTransactionHook("fenced", managed.installId);
|
|
1125
|
-
if (prepared.publication) {
|
|
1126
|
-
prepared.publication.publish();
|
|
1127
|
-
contentPublished = true;
|
|
1128
|
-
}
|
|
1129
|
-
await verifyPublishedAudit(prepared, managed.installId);
|
|
1130
|
-
publishedLockSnapshot = await publishLockfileUpdate(oldLockSnapshot, desiredLocks);
|
|
1131
|
-
if (!publishedLockSnapshot)
|
|
1132
|
-
throw concurrentGenerationError(managed);
|
|
1133
|
-
lockPublished = true;
|
|
1134
|
-
updateTransactionHook("published", managed.installId);
|
|
1135
|
-
updateTransactionHook("before-index", managed.installId);
|
|
1136
|
-
transaction = openUnifiedUpdateTransaction();
|
|
1137
|
-
index = await akmIndex({
|
|
1138
|
-
stashDir,
|
|
1139
|
-
full: true,
|
|
1140
|
-
hydrateSources: false,
|
|
1141
|
-
persistDetectedAdapters: false,
|
|
1142
|
-
deferredUpdateTransaction: transaction.deferred,
|
|
1143
|
-
});
|
|
1144
|
-
const currentLockSnapshot = readLockfileForUpdate();
|
|
1145
|
-
if (generationHash(currentLockSnapshot.entries) !== desiredLockGeneration ||
|
|
1146
|
-
currentLockSnapshot.raw !== publishedLockSnapshot.raw ||
|
|
1147
|
-
currentLockSnapshot.mode !== publishedLockSnapshot.mode) {
|
|
1148
|
-
throw concurrentGenerationError(managed);
|
|
1149
|
-
}
|
|
1150
|
-
updateTransactionHook("indexed", managed.installId, { db: transaction.db });
|
|
1151
|
-
// Indexing can be long enough for a local writer to race the first
|
|
1152
|
-
// post-materialization audit. Re-verify the exact bytes/findings at the
|
|
1153
|
-
// final cross-store commit boundary while every AKM fence is held.
|
|
1154
|
-
await verifyPublishedAudit(prepared, managed.installId);
|
|
1155
|
-
updateTransactionHook("before-commit", managed.installId, { db: transaction.db });
|
|
1156
|
-
commitUnifiedUpdateTransaction(transaction);
|
|
1157
|
-
committed = true;
|
|
1158
|
-
}
|
|
1159
|
-
catch (error) {
|
|
1160
|
-
let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
|
|
1161
|
-
let concurrentGeneration = false;
|
|
1162
|
-
let concurrentGenerationOwnsPublishedRoot = false;
|
|
1163
|
-
if (lockPublished) {
|
|
1164
|
-
try {
|
|
1165
|
-
if (!publishedLockSnapshot ||
|
|
1166
|
-
!(await compareAndSwapLockfileSnapshot(publishedLockSnapshot, oldLockSnapshot))) {
|
|
1167
|
-
concurrentGeneration = true;
|
|
1168
|
-
const concurrentLocks = readLockfileForUpdate().entries;
|
|
1169
|
-
const concurrentEntry = concurrentLocks.find((entry) => entry.id === desiredLock.id);
|
|
1170
|
-
concurrentGenerationOwnsPublishedRoot =
|
|
1171
|
-
prepared.publication !== undefined &&
|
|
1172
|
-
concurrentEntry?.localRoot !== undefined &&
|
|
1173
|
-
path.resolve(concurrentEntry.localRoot) === path.resolve(synced.contentDir);
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
catch (compensationError) {
|
|
1177
|
-
recoveryError ??= compensationError;
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
if (!concurrentGenerationOwnsPublishedRoot) {
|
|
1181
|
-
try {
|
|
1182
|
-
if (contentPublished)
|
|
1183
|
-
prepared.publication?.rollback();
|
|
1184
|
-
}
|
|
1185
|
-
catch (rollbackError) {
|
|
1186
|
-
recoveryError ??= rollbackError;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
if (recoveryError)
|
|
1190
|
-
throw recoveryError;
|
|
1191
|
-
if (concurrentGeneration)
|
|
1192
|
-
throw concurrentGenerationError(managed);
|
|
1193
|
-
throw error;
|
|
1194
|
-
}
|
|
1195
|
-
finally {
|
|
1196
|
-
try {
|
|
1197
|
-
if (transaction)
|
|
1198
|
-
closeUnifiedUpdateTransaction(transaction, committed);
|
|
1199
|
-
}
|
|
1200
|
-
finally {
|
|
1201
|
-
releaseConfigFence?.();
|
|
1202
|
-
}
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
if (movedRoot) {
|
|
667
|
+
if (referencesRoot(managed.localRoot, loadConfig(), readLockfile())) {
|
|
668
|
+
warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
|
|
1203
669
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (movedRoot) {
|
|
1207
|
-
const currentConfig = loadConfig();
|
|
1208
|
-
const currentLocks = readLockfile();
|
|
1209
|
-
if (referencesRoot(managed.localRoot, currentConfig, currentLocks)) {
|
|
1210
|
-
warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
|
|
1211
|
-
}
|
|
1212
|
-
else {
|
|
1213
|
-
cleanupDirectoryBestEffort(managed.localRoot, "update");
|
|
1214
|
-
}
|
|
670
|
+
else {
|
|
671
|
+
cleanupDirectoryBestEffort(managed.localRoot, "update");
|
|
1215
672
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
installed: { ...installedEntry, extractedDir: synced.extractedDir },
|
|
1230
|
-
changed: {
|
|
1231
|
-
version: versionChanged,
|
|
1232
|
-
revision: revisionChanged,
|
|
1233
|
-
any: versionChanged || revisionChanged,
|
|
1234
|
-
},
|
|
673
|
+
}
|
|
674
|
+
const versionChanged = (managed.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
|
|
675
|
+
const revisionChanged = (managed.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
|
|
676
|
+
return {
|
|
677
|
+
index,
|
|
678
|
+
item: {
|
|
679
|
+
id: managed.installId,
|
|
680
|
+
source: managed.source,
|
|
681
|
+
ref: managed.ref,
|
|
682
|
+
previous: {
|
|
683
|
+
resolvedVersion: managed.resolvedVersion,
|
|
684
|
+
resolvedRevision: managed.resolvedRevision,
|
|
685
|
+
cacheDir: managed.localRoot,
|
|
1235
686
|
},
|
|
1236
|
-
|
|
1237
|
-
|
|
687
|
+
installed: { ...installedEntry, extractedDir: synced.extractedDir },
|
|
688
|
+
changed: {
|
|
689
|
+
version: versionChanged,
|
|
690
|
+
revision: revisionChanged,
|
|
691
|
+
any: versionChanged || revisionChanged,
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
};
|
|
1238
695
|
}
|
|
1239
696
|
finally {
|
|
1240
|
-
|
|
697
|
+
staged.cleanup();
|
|
1241
698
|
}
|
|
1242
699
|
}
|
|
1243
|
-
function generationHash(value) {
|
|
1244
|
-
return sha256Hex(JSON.stringify(value));
|
|
1245
|
-
}
|
|
1246
700
|
function referencesRoot(target, config, locks) {
|
|
1247
701
|
const resolvedTarget = path.resolve(target);
|
|
1248
702
|
const locksById = new Map(locks.map((entry) => [entry.id, entry]));
|
|
@@ -1264,9 +718,6 @@ function isRootAtOrBelow(candidate, root) {
|
|
|
1264
718
|
const lexicallyWithin = relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
1265
719
|
return lexicallyWithin || isWithin(candidate, root);
|
|
1266
720
|
}
|
|
1267
|
-
function concurrentGenerationError(managed) {
|
|
1268
|
-
return new ConfigError(`Managed source "${managed.installId}" changed concurrently; retained both materialized roots and deleted nothing.`, "INVALID_CONFIG_FILE", "Inspect `akm bundle list` and retry the update after the concurrent source operation completes.");
|
|
1269
|
-
}
|
|
1270
721
|
// ── akmUpdate dispatcher ─────────────────────────────────────────────────────
|
|
1271
722
|
export async function akmUpdate(input) {
|
|
1272
723
|
const stashDir = input?.stashDir ?? resolveStashDir();
|
|
@@ -1310,7 +761,7 @@ export async function akmUpdate(input) {
|
|
|
1310
761
|
return false;
|
|
1311
762
|
});
|
|
1312
763
|
if (gitMatch)
|
|
1313
|
-
return updateGitSource(stashDir, target, all, gitMatch, allowDangerousEnvKeys);
|
|
764
|
+
return updateGitSource(config, stashDir, target, all, gitMatch, allowDangerousEnvKeys);
|
|
1314
765
|
const websiteMatch = stashes.find((s) => {
|
|
1315
766
|
if (s.type !== "website")
|
|
1316
767
|
return false;
|
|
@@ -1323,7 +774,7 @@ export async function akmUpdate(input) {
|
|
|
1323
774
|
return false;
|
|
1324
775
|
});
|
|
1325
776
|
if (websiteMatch)
|
|
1326
|
-
return updateWebsiteSource(stashDir, target, all, websiteMatch
|
|
777
|
+
return updateWebsiteSource(stashDir, target, all, websiteMatch);
|
|
1327
778
|
// Plain npm source (bundle without a lock) — sync via the registry
|
|
1328
779
|
// pipeline and promote to a managed install (see
|
|
1329
780
|
// managedInstallViewOfPlainNpm's doc comment for why npm can't stay
|
|
@@ -1338,7 +789,7 @@ export async function akmUpdate(input) {
|
|
|
1338
789
|
return false;
|
|
1339
790
|
});
|
|
1340
791
|
if (npmMatch) {
|
|
1341
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir, allowDangerousEnvKeys);
|
|
792
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, npmMatch), force, yes, stashDir, allowDangerousEnvKeys);
|
|
1342
793
|
return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
|
|
1343
794
|
}
|
|
1344
795
|
const filesystemMatch = stashes.find((source) => {
|
|
@@ -1374,10 +825,10 @@ export async function akmUpdate(input) {
|
|
|
1374
825
|
// `--all` must account for EVERY configured source, not only the
|
|
1375
826
|
// registry-managed (lock-backed) ones (R-015) — `selectManagedTargets`
|
|
1376
827
|
// above returns only `installs` for `all`, so plain sources were
|
|
1377
|
-
// previously never even looked at. Git/npm plain sources are synced
|
|
1378
|
-
// too (
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
828
|
+
// previously never even looked at. Git/website/npm plain sources are synced
|
|
829
|
+
// here too (npm is promoted to managed, same as the single-target path
|
|
830
|
+
// above). Filesystem sources reflect their files in place and are reported
|
|
831
|
+
// as intentionally skipped.
|
|
1381
832
|
if (all) {
|
|
1382
833
|
const managedKeys = new Set(managedInstalls.map((m) => m.bundleKey));
|
|
1383
834
|
const plainSources = getSources(config).filter((source) => source.enabled !== false && !managedKeys.has(source.name ?? ""));
|
|
@@ -1386,17 +837,17 @@ export async function akmUpdate(input) {
|
|
|
1386
837
|
const id = plain.name ?? plain.path ?? plain.url ?? "";
|
|
1387
838
|
try {
|
|
1388
839
|
if (plain.type === "git") {
|
|
1389
|
-
const updated = await syncGitPlainSource(plain, stashDir, allowDangerousEnvKeys);
|
|
840
|
+
const updated = await syncGitPlainSource(config, plain, stashDir, allowDangerousEnvKeys);
|
|
1390
841
|
plainSynced.push(updated.item);
|
|
1391
842
|
latestIndex = updated.index;
|
|
1392
843
|
}
|
|
1393
844
|
else if (plain.type === "npm") {
|
|
1394
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir, allowDangerousEnvKeys);
|
|
845
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(config, plain), force, yes, stashDir, allowDangerousEnvKeys);
|
|
1395
846
|
processed.push(updated.item);
|
|
1396
847
|
latestIndex = updated.index;
|
|
1397
848
|
}
|
|
1398
849
|
else if (plain.type === "website") {
|
|
1399
|
-
const updated = await syncWebsitePlainSource(plain, stashDir
|
|
850
|
+
const updated = await syncWebsitePlainSource(plain, stashDir);
|
|
1400
851
|
plainSynced.push(updated.item);
|
|
1401
852
|
latestIndex = updated.index;
|
|
1402
853
|
}
|