akm-cli 0.9.1 → 0.9.2-alpha.1
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 +88 -0
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract.js +521 -342
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +10 -0
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +835 -152
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +83 -34
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +21 -20
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +65 -34
- package/dist/indexer/indexer.js +443 -222
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/scan/doc-to-entry.js +7 -9
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +98 -22
- package/dist/indexer/search/search-fields.js +1 -1
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +28 -32
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19122 -52218
- package/dist/scripts/akm-migrate.js +19064 -51567
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +167 -207
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +10 -10
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +71 -342
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +1 -1
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +153 -27
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -216
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -13,26 +13,71 @@
|
|
|
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";
|
|
16
17
|
import fs from "node:fs";
|
|
17
18
|
import path from "node:path";
|
|
18
19
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
19
|
-
import { getSources, loadConfig } from "../../core/config/config.js";
|
|
20
|
-
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
20
|
+
import { acquireConfigReadFence, bundleComponentConfig, getSources, loadConfig } from "../../core/config/config.js";
|
|
21
|
+
import { AkmError, ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
21
22
|
import { isPathAbsent } from "../../core/path-access.js";
|
|
22
|
-
import { getDbPath } from "../../core/paths.js";
|
|
23
|
+
import { getDbPath, getRegistryCacheDir } from "../../core/paths.js";
|
|
24
|
+
import { beginImmediateTransaction, getStateDbPath, openStateDatabase } from "../../core/state-db.js";
|
|
23
25
|
import { warn } from "../../core/warn.js";
|
|
26
|
+
import { resolveGitContentRoot } from "../../core/write-source.js";
|
|
24
27
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
25
28
|
import { akmIndex } from "../../indexer/indexer.js";
|
|
26
|
-
import {
|
|
29
|
+
import { compareAndSwapLockfileSnapshot, publishLockfileUpdate, readLockfile, readLockfileForUpdate, } from "../../integrations/lockfile.js";
|
|
27
30
|
import { parseRegistryRef } from "../../registry/resolve.js";
|
|
28
31
|
import { sha256Hex } from "../../runtime.js";
|
|
29
|
-
import { parseGitRepoUrl, syncMirroredRepo } from "../../sources/providers/git.js";
|
|
32
|
+
import { getCachePaths, parseGitRepoUrl, runGit, syncMirroredRepo } from "../../sources/providers/git.js";
|
|
30
33
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
34
|
+
import { createWebsiteProvider } from "../../sources/providers/website.js";
|
|
35
|
+
import { storeSecretResolver } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
36
|
+
import { ensureWebsiteMirror } from "../../sources/snapshot-fetchers/website-ingest.js";
|
|
37
|
+
import { getWebsiteCachePaths } from "../../sources/website-url.js";
|
|
38
|
+
import { closeDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
|
|
33
39
|
import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
|
|
40
|
+
import { auditStashForDangerousKeys, scanStashForDangerousKeys } from "./dangerous-env-audit.js";
|
|
34
41
|
import { removeInstalledRegistryEntry } from "./source-add.js";
|
|
35
42
|
import { removeStash } from "./source-manage.js";
|
|
43
|
+
import { cleanupStagingParent, createStagingParent, prepareDirectoryPublication, prepareWritableGitPublication, } from "./update-transaction.js";
|
|
44
|
+
function canonicalJson(value) {
|
|
45
|
+
if (Array.isArray(value))
|
|
46
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
47
|
+
if (value && typeof value === "object") {
|
|
48
|
+
const entries = Object.entries(value)
|
|
49
|
+
.filter(([, entry]) => entry !== undefined)
|
|
50
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
51
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
function bundleAuditGeneration(entry) {
|
|
56
|
+
return sha256Hex(canonicalJson(entry ?? null));
|
|
57
|
+
}
|
|
58
|
+
function bundleAuditFence(config, id) {
|
|
59
|
+
const bundle = config.bundles?.[id];
|
|
60
|
+
if (!bundle)
|
|
61
|
+
throw new ConfigError(`Source "${id}" disappeared before update staging.`);
|
|
62
|
+
return {
|
|
63
|
+
componentRoot: bundleComponentConfig(bundle)?.root ?? ".",
|
|
64
|
+
generation: bundleAuditGeneration(bundle),
|
|
65
|
+
writable: bundleComponentConfig(bundle)?.writable ?? bundle.writable === true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function assertBundleAuditFence(config, id, expectedGeneration) {
|
|
69
|
+
const current = config.bundles?.[id];
|
|
70
|
+
if (bundleAuditGeneration(current) !== expectedGeneration) {
|
|
71
|
+
throw new ConfigError(`Source "${id}" config changed after its staged bytes were audited; refusing to publish without re-staging and re-auditing.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function currentPlainSource(config, id, type) {
|
|
75
|
+
const source = getSources(config).find((candidate) => candidate.name === id && candidate.type === type);
|
|
76
|
+
if (!source) {
|
|
77
|
+
throw new ConfigError(`Source "${id}" changed or disappeared before update staging; retry to re-resolve it.`);
|
|
78
|
+
}
|
|
79
|
+
return source;
|
|
80
|
+
}
|
|
36
81
|
/** Enumerate the registry-managed installs (lock-backed bundles) in a config. */
|
|
37
82
|
function listManagedInstalls(config) {
|
|
38
83
|
const bundles = config.bundles ?? {};
|
|
@@ -43,6 +88,7 @@ function listManagedInstalls(config) {
|
|
|
43
88
|
if (!lock)
|
|
44
89
|
continue; // only lock-backed bundles are registry-managed
|
|
45
90
|
const componentWritable = Object.values(bundle.components ?? {})[0]?.writable;
|
|
91
|
+
const componentRoot = bundleComponentConfig(bundle)?.root ?? ".";
|
|
46
92
|
out.push({
|
|
47
93
|
bundleKey: key,
|
|
48
94
|
installId: bundle.registryId ?? key,
|
|
@@ -52,9 +98,9 @@ function listManagedInstalls(config) {
|
|
|
52
98
|
resolvedVersion: lock.resolvedVersion,
|
|
53
99
|
resolvedRevision: lock.resolvedRevision,
|
|
54
100
|
writable: componentWritable ?? bundle.writable === true,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
101
|
+
componentRoot,
|
|
102
|
+
requiredRoots: lock.localRoot ? [path.resolve(lock.localRoot, componentRoot)] : [],
|
|
103
|
+
auditConfigGeneration: bundleAuditGeneration(bundle),
|
|
58
104
|
});
|
|
59
105
|
}
|
|
60
106
|
return out;
|
|
@@ -272,10 +318,26 @@ export async function akmRemove(input) {
|
|
|
272
318
|
},
|
|
273
319
|
};
|
|
274
320
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
321
|
+
/** Read the current index generation without creating or hydrating anything. */
|
|
322
|
+
function readCurrentIndexSummary() {
|
|
323
|
+
const db = openReadonlyExistingDatabase(getDbPath());
|
|
324
|
+
if (!db)
|
|
325
|
+
return { mode: "incremental", totalEntries: 0, directoriesScanned: 0, directoriesSkipped: 0 };
|
|
326
|
+
try {
|
|
327
|
+
return {
|
|
328
|
+
mode: "incremental",
|
|
329
|
+
totalEntries: getAllEntries(db).length,
|
|
330
|
+
directoriesScanned: 0,
|
|
331
|
+
directoriesSkipped: 0,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
closeDatabase(db);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Build a standard UpdateResponse summary block from a committed or unchanged index generation. */
|
|
339
|
+
function buildUpdateResponse(stashDir, target, all, processed, opts) {
|
|
340
|
+
const index = opts.index;
|
|
279
341
|
const finalConfig = loadConfig();
|
|
280
342
|
return {
|
|
281
343
|
schemaVersion: 1,
|
|
@@ -296,34 +358,540 @@ async function buildUpdateResponse(stashDir, target, all, processed, opts) {
|
|
|
296
358
|
},
|
|
297
359
|
};
|
|
298
360
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
361
|
+
let updateTransactionHookForTests;
|
|
362
|
+
/** TEST-ONLY. Inject faults at source-update transaction boundaries. */
|
|
363
|
+
export function _setUpdateTransactionHookForTests(hook) {
|
|
364
|
+
updateTransactionHookForTests = hook;
|
|
365
|
+
}
|
|
366
|
+
function updateTransactionHook(point, id, context) {
|
|
367
|
+
updateTransactionHookForTests?.(point, id, context);
|
|
368
|
+
}
|
|
369
|
+
function gitHead(repoDir) {
|
|
370
|
+
const result = runGit(["-C", repoDir, "rev-parse", "HEAD"]);
|
|
371
|
+
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : undefined;
|
|
372
|
+
}
|
|
373
|
+
function invalidWritableGitPublication(repoDir) {
|
|
374
|
+
return {
|
|
375
|
+
publish() {
|
|
376
|
+
throw new ConfigError(`Writable Git source at ${repoDir} is not a valid checkout; refusing to replace its live directory.`);
|
|
377
|
+
},
|
|
378
|
+
rollback() { },
|
|
379
|
+
commit() { },
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function writableGitPublication(opts) {
|
|
383
|
+
const auditedTargetHead = opts.auditedTargetHead ?? gitHead(opts.stagedRepo);
|
|
384
|
+
if (!opts.expectedOldHead || !auditedTargetHead)
|
|
385
|
+
return invalidWritableGitPublication(opts.liveRepo);
|
|
386
|
+
return prepareWritableGitPublication({
|
|
387
|
+
stagedRepo: opts.stagedRepo,
|
|
388
|
+
liveRepo: opts.liveRepo,
|
|
389
|
+
expectedOldHead: opts.expectedOldHead,
|
|
390
|
+
auditedTargetHead,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const UPDATE_STATE_SCHEMA = "akm_update_state";
|
|
394
|
+
function sqliteStringLiteral(value) {
|
|
395
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
396
|
+
}
|
|
397
|
+
function openUnifiedUpdateTransaction() {
|
|
398
|
+
// Keep canonical state activity registered for the entire attached
|
|
399
|
+
// transaction so maintenance/migration cannot replace state.db underneath
|
|
400
|
+
// the borrowed index connection.
|
|
401
|
+
const stateActivity = openStateDatabase();
|
|
402
|
+
try {
|
|
403
|
+
const statePath = getStateDbPath();
|
|
404
|
+
const db = openIndexDatabase(undefined, {
|
|
405
|
+
beforeSchema: (candidate) => {
|
|
406
|
+
candidate.exec(`ATTACH DATABASE ${sqliteStringLiteral(statePath)} AS "${UPDATE_STATE_SCHEMA}"`);
|
|
407
|
+
// openIndexDatabase invokes this before ensureSchema, so the outer
|
|
408
|
+
// transaction begins before the first update-owned index mutation.
|
|
409
|
+
beginImmediateTransaction(candidate);
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
db,
|
|
414
|
+
stateActivity,
|
|
415
|
+
deferred: { db, stateSchema: UPDATE_STATE_SCHEMA },
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
stateActivity.close();
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function rollbackUnifiedUpdateTransaction(transaction) {
|
|
424
|
+
if (!transaction.db.inTransaction)
|
|
425
|
+
return undefined;
|
|
426
|
+
try {
|
|
427
|
+
transaction.db.exec("ROLLBACK");
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
return error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function commitUnifiedUpdateTransaction(transaction) {
|
|
435
|
+
if (!transaction.db.inTransaction) {
|
|
436
|
+
throw new Error("Source update index returned without its active unified index/state transaction.");
|
|
437
|
+
}
|
|
438
|
+
transaction.db.exec("COMMIT");
|
|
439
|
+
}
|
|
440
|
+
function closeUnifiedUpdateTransaction(transaction, committed) {
|
|
441
|
+
let closeError = rollbackUnifiedUpdateTransaction(transaction);
|
|
442
|
+
try {
|
|
443
|
+
transaction.db.close();
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
closeError ??= error;
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
transaction.stateActivity.close();
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
closeError ??= error;
|
|
453
|
+
}
|
|
454
|
+
if (closeError) {
|
|
455
|
+
warn(committed
|
|
456
|
+
? `[akm bundle update] committed, but closing its database handles failed: ${String(closeError)}`
|
|
457
|
+
: `[akm bundle update] rolled back, but closing its database handles failed: ${String(closeError)}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function pathAtOrBelow(candidate, root) {
|
|
461
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
462
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
463
|
+
}
|
|
464
|
+
function remapStagedPath(candidate, stagedRoot, liveRoot) {
|
|
465
|
+
if (!pathAtOrBelow(candidate, stagedRoot))
|
|
466
|
+
return candidate;
|
|
467
|
+
return path.join(liveRoot, path.relative(stagedRoot, candidate));
|
|
468
|
+
}
|
|
469
|
+
function dangerousFindingGeneration(findings) {
|
|
470
|
+
return JSON.stringify(findings
|
|
471
|
+
.map(({ envRef, keyName, relPath }) => ({ envRef, keyName, relPath }))
|
|
472
|
+
.sort((left, right) => `${left.relPath}\0${left.envRef}\0${left.keyName}`.localeCompare(`${right.relPath}\0${right.envRef}\0${right.keyName}`)));
|
|
473
|
+
}
|
|
474
|
+
async function contentGeneration(root) {
|
|
475
|
+
const hash = createHash("sha256");
|
|
476
|
+
const resolvedRoot = path.resolve(root);
|
|
477
|
+
const walk = async (directory) => {
|
|
478
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
if (entry.name === ".git")
|
|
481
|
+
continue;
|
|
482
|
+
const absolute = path.join(directory, entry.name);
|
|
483
|
+
const relative = path.relative(resolvedRoot, absolute).replaceAll(path.sep, "/");
|
|
484
|
+
const before = fs.lstatSync(absolute);
|
|
485
|
+
if (before.isDirectory()) {
|
|
486
|
+
hash.update(`D\0${relative}\0${before.mode & 0o777}\0`);
|
|
487
|
+
await walk(absolute);
|
|
488
|
+
}
|
|
489
|
+
else if (before.isSymbolicLink()) {
|
|
490
|
+
hash.update(`L\0${relative}\0${fs.readlinkSync(absolute)}\0`);
|
|
491
|
+
}
|
|
492
|
+
else if (before.isFile()) {
|
|
493
|
+
hash.update(`F\0${relative}\0${before.mode & 0o777}\0${before.size}\0`);
|
|
494
|
+
for await (const chunk of fs.createReadStream(absolute))
|
|
495
|
+
hash.update(chunk);
|
|
496
|
+
const after = fs.lstatSync(absolute);
|
|
497
|
+
if (after.ino !== before.ino || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
|
|
498
|
+
throw new ConfigError(`Source content changed while ${relative} was being audited; retry the update.`);
|
|
499
|
+
}
|
|
500
|
+
hash.update("\0");
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
throw new ConfigError(`Unsupported filesystem entry ${absolute}; refusing to audit an unstable source tree.`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
if (!fs.statSync(resolvedRoot).isDirectory())
|
|
508
|
+
throw new ConfigError(`Expected ${resolvedRoot} to be a directory.`);
|
|
509
|
+
await walk(resolvedRoot);
|
|
510
|
+
return hash.digest("hex");
|
|
511
|
+
}
|
|
512
|
+
async function verifyPublishedAudit(prepared, id) {
|
|
513
|
+
const liveRoot = prepared.publishedAuditRoot;
|
|
514
|
+
if (!liveRoot)
|
|
515
|
+
return;
|
|
516
|
+
const resolveLiveAuditRoot = () => {
|
|
517
|
+
const physicalRoot = canonicalWritablePath(liveRoot, `published component root for "${id}"`);
|
|
518
|
+
const containmentRoot = prepared.publishedAuditContainmentRoot;
|
|
519
|
+
if (containmentRoot && !pathAtOrBelow(physicalRoot, containmentRoot)) {
|
|
520
|
+
throw new ConfigError(`Published component root ${liveRoot} resolves outside its physical checkout at ${containmentRoot}.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
521
|
+
}
|
|
522
|
+
const expected = prepared.publishedAuditExpectedPhysicalRoot;
|
|
523
|
+
if (expected && path.resolve(physicalRoot) !== path.resolve(expected)) {
|
|
524
|
+
throw new ConfigError(`Published component root ${liveRoot} resolves to ${physicalRoot}, not its audited staged target ${expected}.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
525
|
+
}
|
|
526
|
+
return physicalRoot;
|
|
527
|
+
};
|
|
528
|
+
const beforeRoot = resolveLiveAuditRoot();
|
|
529
|
+
const beforeAudit = await contentGeneration(beforeRoot);
|
|
530
|
+
let findings;
|
|
531
|
+
try {
|
|
532
|
+
const scanRoot = resolveLiveAuditRoot();
|
|
533
|
+
if (scanRoot !== beforeRoot)
|
|
534
|
+
throw new Error("configured component symlink changed during re-audit");
|
|
535
|
+
findings = scanStashForDangerousKeys(scanRoot);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
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");
|
|
539
|
+
}
|
|
540
|
+
const afterRoot = resolveLiveAuditRoot();
|
|
541
|
+
const afterAudit = await contentGeneration(afterRoot);
|
|
542
|
+
if (afterRoot !== beforeRoot ||
|
|
543
|
+
beforeAudit !== prepared.auditedContentGeneration ||
|
|
544
|
+
afterAudit !== prepared.auditedContentGeneration ||
|
|
545
|
+
dangerousFindingGeneration(findings) !== prepared.approvedDangerousFindings) {
|
|
546
|
+
throw new ConfigError(`Writable source "${id}" changed while its audited Git target was materialized; refusing to activate unaudited bytes.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function auditPreparedUpdate(prepared, id, ref, allowInsecure) {
|
|
550
|
+
updateTransactionHook("staged", id);
|
|
551
|
+
const decision = await auditStashForDangerousKeys({
|
|
552
|
+
stashRoot: prepared.auditRoot,
|
|
553
|
+
ref,
|
|
554
|
+
allowDangerousKeys: allowInsecure,
|
|
555
|
+
isTTY: process.stdin.isTTY === true,
|
|
556
|
+
operation: "update",
|
|
557
|
+
renderBlockedError: false,
|
|
315
558
|
});
|
|
316
|
-
|
|
559
|
+
if (decision.blocked)
|
|
560
|
+
throw new NotFoundError(decision.error, decision.code);
|
|
561
|
+
prepared.approvedDangerousFindings = dangerousFindingGeneration(decision.findings);
|
|
562
|
+
prepared.auditedContentGeneration = await contentGeneration(prepared.auditRoot);
|
|
563
|
+
updateTransactionHook("audited", id);
|
|
564
|
+
}
|
|
565
|
+
function canonicalWritablePath(candidate, description) {
|
|
566
|
+
try {
|
|
567
|
+
return fs.realpathSync(candidate);
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
throw new ConfigError(`Cannot resolve ${description} at ${candidate}; refusing writable Git update. ${error instanceof Error ? error.message : String(error)}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function containedWritablePath(candidate, physicalRepo, description) {
|
|
574
|
+
const physicalCandidate = canonicalWritablePath(candidate, description);
|
|
575
|
+
if (!pathAtOrBelow(physicalCandidate, physicalRepo)) {
|
|
576
|
+
throw new ConfigError(`${description} ${candidate} resolves outside its physical checkout at ${physicalRepo}.`);
|
|
577
|
+
}
|
|
578
|
+
return physicalCandidate;
|
|
579
|
+
}
|
|
580
|
+
function prepareWritableManagedUpdate(managed, force) {
|
|
581
|
+
const physicalLocalRoot = canonicalWritablePath(managed.localRoot, "managed content root");
|
|
582
|
+
const topLevel = runGit(["-C", physicalLocalRoot, "rev-parse", "--show-toplevel"]);
|
|
583
|
+
// Production writable installs are Git checkouts. Falling back to the
|
|
584
|
+
// locked root keeps the provider seam testable; the real Git provider still
|
|
585
|
+
// refuses a copied non-checkout before publication.
|
|
586
|
+
const liveRepo = topLevel.status === 0 && topLevel.stdout.trim()
|
|
587
|
+
? canonicalWritablePath(topLevel.stdout.trim(), "managed Git top-level")
|
|
588
|
+
: physicalLocalRoot;
|
|
589
|
+
if (!pathAtOrBelow(physicalLocalRoot, liveRepo)) {
|
|
590
|
+
throw new ConfigError(`Writable Git content root ${managed.localRoot} resolves outside its physical checkout at ${liveRepo}.`);
|
|
591
|
+
}
|
|
592
|
+
// Keep the configured component path as a lexical identity relative to the
|
|
593
|
+
// content root. Resolving it to today's symlink target here would pin the old
|
|
594
|
+
// target and miss an upstream commit that retargets the symlink.
|
|
595
|
+
const lexicalLocalRoot = path.resolve(managed.localRoot);
|
|
596
|
+
const configuredLiveRoots = managed.requiredRoots.map((root) => {
|
|
597
|
+
const lexicalRoot = path.resolve(root);
|
|
598
|
+
if (!pathAtOrBelow(lexicalRoot, lexicalLocalRoot)) {
|
|
599
|
+
throw new ConfigError(`Configured writable Git component root ${lexicalRoot} escapes content root ${lexicalLocalRoot}.`);
|
|
600
|
+
}
|
|
601
|
+
return path.resolve(physicalLocalRoot, path.relative(lexicalLocalRoot, lexicalRoot));
|
|
602
|
+
});
|
|
603
|
+
const physicalRequiredRoots = configuredLiveRoots.map((root) => containedWritablePath(root, liveRepo, `Configured component root for "${managed.installId}"`));
|
|
604
|
+
// The Git provider must preserve ordinary component directories, but not a
|
|
605
|
+
// symlink's current physical target. The configured lexical path is audited
|
|
606
|
+
// again after sync, so retaining the old target here would reject a valid
|
|
607
|
+
// upstream retarget before the new target can reach that audit.
|
|
608
|
+
const preservedPhysicalRequiredRoots = physicalRequiredRoots.filter((root, index) => configuredLiveRoots[index] === root);
|
|
609
|
+
const expectedOldHead = gitHead(liveRepo);
|
|
610
|
+
const stagingParent = createStagingParent(path.dirname(liveRepo));
|
|
611
|
+
const stagedRepo = path.join(stagingParent, path.basename(liveRepo));
|
|
612
|
+
fs.cpSync(liveRepo, stagedRepo, { recursive: true, preserveTimestamps: true, verbatimSymlinks: true });
|
|
613
|
+
const stagedContentRoot = remapStagedPath(physicalLocalRoot, liveRepo, stagedRepo);
|
|
614
|
+
const stagedPhysicalRequiredRoots = preservedPhysicalRequiredRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
|
|
615
|
+
const stagedConfiguredRoots = configuredLiveRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
|
|
616
|
+
return syncFromRef(managed.ref, {
|
|
617
|
+
force,
|
|
618
|
+
writable: true,
|
|
619
|
+
writableRoot: stagedContentRoot,
|
|
620
|
+
...(stagedPhysicalRequiredRoots.length > 0 ? { writableRequiredRoots: stagedPhysicalRequiredRoots } : {}),
|
|
621
|
+
})
|
|
622
|
+
.then((staged) => {
|
|
623
|
+
const auditedTargetHead = gitHead(stagedRepo);
|
|
624
|
+
const physicalStagedRepo = canonicalWritablePath(stagedRepo, "staged managed Git checkout");
|
|
625
|
+
const auditedConfiguredRoots = stagedConfiguredRoots.map((root) => containedWritablePath(root, physicalStagedRepo, `Post-sync component root for "${managed.installId}"`));
|
|
626
|
+
const expectedPublishedRoots = auditedConfiguredRoots.map((root) => path.join(liveRepo, path.relative(physicalStagedRepo, root)));
|
|
627
|
+
const synced = {
|
|
628
|
+
...staged,
|
|
629
|
+
resolvedRevision: auditedTargetHead ?? staged.resolvedRevision,
|
|
630
|
+
contentDir: remapStagedPath(staged.contentDir, stagedRepo, liveRepo),
|
|
631
|
+
cacheDir: remapStagedPath(staged.cacheDir, stagedRepo, liveRepo),
|
|
632
|
+
extractedDir: remapStagedPath(staged.extractedDir, stagedRepo, liveRepo),
|
|
633
|
+
};
|
|
634
|
+
return {
|
|
635
|
+
synced,
|
|
636
|
+
auditRoot: auditedConfiguredRoots[0] ?? staged.contentDir,
|
|
637
|
+
publishedAuditRoot: configuredLiveRoots[0] ?? synced.contentDir,
|
|
638
|
+
publishedAuditContainmentRoot: liveRepo,
|
|
639
|
+
publishedAuditExpectedPhysicalRoot: expectedPublishedRoots[0] ?? synced.contentDir,
|
|
640
|
+
publication: writableGitPublication({ stagedRepo, liveRepo, expectedOldHead, auditedTargetHead }),
|
|
641
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
642
|
+
};
|
|
643
|
+
})
|
|
644
|
+
.catch((error) => {
|
|
645
|
+
cleanupStagingParent(stagingParent);
|
|
646
|
+
throw error;
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
async function prepareManagedUpdate(managed, force) {
|
|
650
|
+
if (managed.writable && managed.localRoot)
|
|
651
|
+
return prepareWritableManagedUpdate(managed, force);
|
|
652
|
+
if (managed.source === "local") {
|
|
653
|
+
const synced = await syncFromRef(managed.ref, { force, writable: managed.writable });
|
|
654
|
+
return {
|
|
655
|
+
synced,
|
|
656
|
+
auditRoot: resolveComponentAuditRoot(synced.contentDir, managed.componentRoot, managed.installId),
|
|
657
|
+
cleanup: () => { },
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
const canonicalCacheRoot = getRegistryCacheDir();
|
|
661
|
+
const stagingParent = createStagingParent(canonicalCacheRoot);
|
|
662
|
+
try {
|
|
663
|
+
const staged = await syncFromRef(managed.ref, {
|
|
664
|
+
force,
|
|
665
|
+
writable: false,
|
|
666
|
+
cacheRootDir: stagingParent,
|
|
667
|
+
});
|
|
668
|
+
if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
|
|
669
|
+
// Test/provider seams may return an already-isolated candidate. It remains
|
|
670
|
+
// invisible until the lock points at it, so no directory promotion is needed.
|
|
671
|
+
return {
|
|
672
|
+
synced: staged,
|
|
673
|
+
auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
|
|
674
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const liveCacheDir = path.join(canonicalCacheRoot, path.relative(stagingParent, staged.cacheDir));
|
|
678
|
+
const synced = {
|
|
679
|
+
...staged,
|
|
680
|
+
contentDir: remapStagedPath(staged.contentDir, staged.cacheDir, liveCacheDir),
|
|
681
|
+
cacheDir: liveCacheDir,
|
|
682
|
+
extractedDir: remapStagedPath(staged.extractedDir, staged.cacheDir, liveCacheDir),
|
|
683
|
+
};
|
|
684
|
+
return {
|
|
685
|
+
synced,
|
|
686
|
+
auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
|
|
687
|
+
publication: prepareDirectoryPublication(staged.cacheDir, liveCacheDir),
|
|
688
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
cleanupStagingParent(stagingParent);
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function resolveComponentAuditRoot(contentRoot, componentRoot, id) {
|
|
697
|
+
const resolved = path.resolve(contentRoot, componentRoot);
|
|
698
|
+
if (!pathAtOrBelow(resolved, contentRoot)) {
|
|
699
|
+
throw new ConfigError(`Component root "${componentRoot}" escapes staged bundle "${id}".`);
|
|
700
|
+
}
|
|
701
|
+
return resolved;
|
|
702
|
+
}
|
|
703
|
+
async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, full, expectedConfigGeneration) {
|
|
704
|
+
try {
|
|
705
|
+
await auditPreparedUpdate(prepared, id, ref, allowInsecure);
|
|
706
|
+
return await withAssetMutationLease("source-update", async () => {
|
|
707
|
+
let transaction;
|
|
708
|
+
let releaseConfigFence;
|
|
709
|
+
let contentPublished = false;
|
|
710
|
+
let committed = false;
|
|
711
|
+
try {
|
|
712
|
+
const configFence = acquireConfigReadFence();
|
|
713
|
+
releaseConfigFence = configFence.release;
|
|
714
|
+
assertBundleAuditFence(configFence.config, id, expectedConfigGeneration);
|
|
715
|
+
updateTransactionHook("fenced", id);
|
|
716
|
+
if (prepared.publication) {
|
|
717
|
+
prepared.publication.publish();
|
|
718
|
+
contentPublished = true;
|
|
719
|
+
}
|
|
720
|
+
await verifyPublishedAudit(prepared, id);
|
|
721
|
+
updateTransactionHook("published", id);
|
|
722
|
+
updateTransactionHook("before-index", id);
|
|
723
|
+
transaction = openUnifiedUpdateTransaction();
|
|
724
|
+
const index = await akmIndex({
|
|
725
|
+
stashDir,
|
|
726
|
+
...(full ? { full: true } : {}),
|
|
727
|
+
hydrateSources: false,
|
|
728
|
+
persistDetectedAdapters: false,
|
|
729
|
+
deferredUpdateTransaction: transaction.deferred,
|
|
730
|
+
});
|
|
731
|
+
updateTransactionHook("indexed", id, { db: transaction.db });
|
|
732
|
+
// Re-fence the exact published worktree after the potentially long
|
|
733
|
+
// index pass and immediately before the coordinator commit. This
|
|
734
|
+
// catches cooperating or observable external writes that land after
|
|
735
|
+
// the first post-materialization audit.
|
|
736
|
+
await verifyPublishedAudit(prepared, id);
|
|
737
|
+
updateTransactionHook("before-commit", id, { db: transaction.db });
|
|
738
|
+
commitUnifiedUpdateTransaction(transaction);
|
|
739
|
+
committed = true;
|
|
740
|
+
try {
|
|
741
|
+
transaction.deferred.afterCommit?.();
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
|
|
745
|
+
}
|
|
746
|
+
prepared.publication?.commit();
|
|
747
|
+
return index;
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
|
|
751
|
+
try {
|
|
752
|
+
if (contentPublished)
|
|
753
|
+
prepared.publication?.rollback();
|
|
754
|
+
}
|
|
755
|
+
catch (rollbackError) {
|
|
756
|
+
recoveryError ??= rollbackError;
|
|
757
|
+
}
|
|
758
|
+
if (recoveryError)
|
|
759
|
+
throw recoveryError;
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
finally {
|
|
763
|
+
try {
|
|
764
|
+
if (transaction)
|
|
765
|
+
closeUnifiedUpdateTransaction(transaction, committed);
|
|
766
|
+
}
|
|
767
|
+
finally {
|
|
768
|
+
releaseConfigFence?.();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
finally {
|
|
774
|
+
prepared.cleanup();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
|
|
778
|
+
if (!gitSource.url)
|
|
779
|
+
throw new ConfigError(`Git source "${gitSource.name ?? "git"}" has no URL.`);
|
|
780
|
+
const repo = parseGitRepoUrl(gitSource.url);
|
|
781
|
+
const livePaths = getCachePaths(repo.canonicalUrl);
|
|
782
|
+
const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
|
|
783
|
+
const stagedPaths = getCachePaths(repo.canonicalUrl, stagingParent);
|
|
784
|
+
try {
|
|
785
|
+
const expectedOldHead = writable ? gitHead(livePaths.repoDir) : undefined;
|
|
786
|
+
if (writable && fs.existsSync(livePaths.rootDir)) {
|
|
787
|
+
fs.cpSync(livePaths.rootDir, stagedPaths.rootDir, { recursive: true, preserveTimestamps: true });
|
|
788
|
+
}
|
|
789
|
+
const staged = await syncMirroredRepo(gitSource, {
|
|
790
|
+
force: true,
|
|
791
|
+
writable,
|
|
792
|
+
cacheRootDir: stagingParent,
|
|
793
|
+
});
|
|
794
|
+
const configuredStagedAuditRoot = resolveComponentAuditRoot(resolveGitContentRoot(staged.contentDir), componentRoot, gitSource.name ?? "git");
|
|
795
|
+
if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
|
|
796
|
+
if (pathAtOrBelow(configuredStagedAuditRoot, livePaths.rootDir)) {
|
|
797
|
+
throw new ConfigError(`Git provider for "${gitSource.name ?? gitSource.url}" wrote into the active cache instead of the update staging root; refusing to index it.`);
|
|
798
|
+
}
|
|
799
|
+
// A provider test seam may hand back an already-isolated candidate.
|
|
800
|
+
// Audit it, but do not move an unowned directory into the canonical
|
|
801
|
+
// cache. Real providers honor cacheRootDir and use the promotion below.
|
|
802
|
+
return {
|
|
803
|
+
auditRoot: configuredStagedAuditRoot,
|
|
804
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
let auditRoot = configuredStagedAuditRoot;
|
|
808
|
+
let publishedAuditRoot;
|
|
809
|
+
let publishedAuditContainmentRoot;
|
|
810
|
+
let publishedAuditExpectedPhysicalRoot;
|
|
811
|
+
if (writable) {
|
|
812
|
+
const physicalStagedRepo = canonicalWritablePath(stagedPaths.repoDir, "staged plain Git checkout");
|
|
813
|
+
auditRoot = containedWritablePath(configuredStagedAuditRoot, physicalStagedRepo, `Post-sync component root for "${gitSource.name ?? "git"}"`);
|
|
814
|
+
publishedAuditRoot = remapStagedPath(configuredStagedAuditRoot, staged.cacheDir, livePaths.rootDir);
|
|
815
|
+
publishedAuditContainmentRoot = canonicalWritablePath(livePaths.repoDir, "plain Git checkout");
|
|
816
|
+
publishedAuditExpectedPhysicalRoot = path.join(publishedAuditContainmentRoot, path.relative(physicalStagedRepo, auditRoot));
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
auditRoot,
|
|
820
|
+
...(publishedAuditRoot ? { publishedAuditRoot } : {}),
|
|
821
|
+
...(publishedAuditContainmentRoot ? { publishedAuditContainmentRoot } : {}),
|
|
822
|
+
...(publishedAuditExpectedPhysicalRoot ? { publishedAuditExpectedPhysicalRoot } : {}),
|
|
823
|
+
publication: writable
|
|
824
|
+
? writableGitPublication({
|
|
825
|
+
stagedRepo: stagedPaths.repoDir,
|
|
826
|
+
liveRepo: livePaths.repoDir,
|
|
827
|
+
expectedOldHead,
|
|
828
|
+
})
|
|
829
|
+
: prepareDirectoryPublication(staged.cacheDir, livePaths.rootDir),
|
|
830
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
cleanupStagingParent(stagingParent);
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function syncGitPlainSource(gitSource, stashDir, allowInsecure) {
|
|
839
|
+
const id = gitSource.name ?? gitSource.url ?? "";
|
|
840
|
+
const currentConfig = loadConfig();
|
|
841
|
+
const currentSource = currentPlainSource(currentConfig, id, "git");
|
|
842
|
+
const ref = currentSource.url ?? "";
|
|
843
|
+
const fence = bundleAuditFence(currentConfig, id);
|
|
844
|
+
const prepared = await prepareGitPlainUpdate(currentSource, fence.componentRoot, fence.writable);
|
|
845
|
+
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, true, fence.generation);
|
|
846
|
+
return { item: { id, kind: "git", ref }, index };
|
|
847
|
+
}
|
|
848
|
+
async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
|
|
849
|
+
if (!websiteSource.url)
|
|
850
|
+
throw new ConfigError(`Website source "${websiteSource.name ?? "website"}" has no URL.`);
|
|
851
|
+
const livePaths = getWebsiteCachePaths(websiteSource.url);
|
|
852
|
+
const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
|
|
853
|
+
const stagedPaths = getWebsiteCachePaths(websiteSource.url, stagingParent);
|
|
854
|
+
try {
|
|
855
|
+
const provider = createWebsiteProvider(websiteSource);
|
|
856
|
+
await provider.sync({
|
|
857
|
+
force: true,
|
|
858
|
+
secrets: storeSecretResolver,
|
|
859
|
+
ensureWebsiteMirror: (config, options) => ensureWebsiteMirror(config, {
|
|
860
|
+
...options,
|
|
861
|
+
resolveSecret: options?.resolveSecret ?? storeSecretResolver.resolveSecret,
|
|
862
|
+
cacheRootDir: stagingParent,
|
|
863
|
+
}),
|
|
864
|
+
});
|
|
865
|
+
return {
|
|
866
|
+
auditRoot: resolveComponentAuditRoot(stagedPaths.stashDir, componentRoot, websiteSource.name ?? "website"),
|
|
867
|
+
publication: prepareDirectoryPublication(stagedPaths.rootDir, livePaths.rootDir),
|
|
868
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
cleanupStagingParent(stagingParent);
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function syncWebsitePlainSource(websiteSource, stashDir, allowInsecure) {
|
|
877
|
+
const id = websiteSource.name ?? websiteSource.url ?? "";
|
|
878
|
+
const currentConfig = loadConfig();
|
|
879
|
+
const currentSource = currentPlainSource(currentConfig, id, "website");
|
|
880
|
+
const ref = currentSource.url ?? "";
|
|
881
|
+
const fence = bundleAuditFence(currentConfig, id);
|
|
882
|
+
const prepared = await prepareWebsitePlainUpdate(currentSource, fence.componentRoot);
|
|
883
|
+
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, true, fence.generation);
|
|
884
|
+
return { item: { id, kind: "website", ref }, index };
|
|
317
885
|
}
|
|
318
886
|
/** Sync a git-mirrored (plain) source and return an UpdateResponse (single-target path). */
|
|
319
|
-
async function updateGitSource(stashDir, target, all, gitSource) {
|
|
320
|
-
const synced = await syncGitPlainSource(gitSource);
|
|
321
|
-
return buildUpdateResponse(stashDir, target, all, [], {
|
|
887
|
+
async function updateGitSource(stashDir, target, all, gitSource, allowInsecure) {
|
|
888
|
+
const synced = await syncGitPlainSource(gitSource, stashDir, allowInsecure);
|
|
889
|
+
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
322
890
|
}
|
|
323
891
|
/** Re-crawl a website (plain) source and return an UpdateResponse (single-target path). */
|
|
324
|
-
async function updateWebsiteSource(stashDir, target, all, websiteSource) {
|
|
325
|
-
const synced = await syncWebsitePlainSource(websiteSource);
|
|
326
|
-
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced] });
|
|
892
|
+
async function updateWebsiteSource(stashDir, target, all, websiteSource, allowInsecure) {
|
|
893
|
+
const synced = await syncWebsitePlainSource(websiteSource, stashDir, allowInsecure);
|
|
894
|
+
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
327
895
|
}
|
|
328
896
|
/**
|
|
329
897
|
* A plain (lockless) npm bundle has no deterministic content path — unlike
|
|
@@ -338,9 +906,12 @@ async function updateWebsiteSource(stashDir, target, all, websiteSource) {
|
|
|
338
906
|
* bookkeeping.
|
|
339
907
|
*/
|
|
340
908
|
function managedInstallViewOfPlainNpm(npmSource) {
|
|
341
|
-
const
|
|
909
|
+
const id = npmSource.name ?? npmSource.path ?? "";
|
|
910
|
+
const config = loadConfig();
|
|
911
|
+
const currentSource = currentPlainSource(config, id, "npm");
|
|
912
|
+
const spec = currentSource.path ?? "";
|
|
342
913
|
const ref = spec.startsWith("npm:") ? spec : `npm:${spec}`;
|
|
343
|
-
const
|
|
914
|
+
const fence = bundleAuditFence(config, id);
|
|
344
915
|
return {
|
|
345
916
|
bundleKey: id,
|
|
346
917
|
installId: id,
|
|
@@ -350,7 +921,9 @@ function managedInstallViewOfPlainNpm(npmSource) {
|
|
|
350
921
|
resolvedVersion: undefined,
|
|
351
922
|
resolvedRevision: undefined,
|
|
352
923
|
writable: false,
|
|
924
|
+
componentRoot: fence.componentRoot,
|
|
353
925
|
requiredRoots: [],
|
|
926
|
+
auditConfigGeneration: fence.generation,
|
|
354
927
|
};
|
|
355
928
|
}
|
|
356
929
|
/**
|
|
@@ -364,101 +937,193 @@ function managedInstallViewOfPlainNpm(npmSource) {
|
|
|
364
937
|
* scripts; only the branch that can `rm -rf` needs a gate, not the whole
|
|
365
938
|
* command).
|
|
366
939
|
*/
|
|
367
|
-
async function updateManagedInstall(managed, force, yes, stashDir) {
|
|
940
|
+
async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure) {
|
|
368
941
|
// No pre-cleanup of the old root, even under --force: the providers already
|
|
369
942
|
// re-materialize staging-first (git clones into a `.tmp-*` sibling and swaps
|
|
370
943
|
// only on success), so destroying `managed.localRoot` BEFORE `syncFromRef`
|
|
371
944
|
// succeeds would turn any sync failure (network down, bad ref) into losing a
|
|
372
945
|
// previously-working install. The old root is cleaned up below only after
|
|
373
946
|
// config/lock publication and reindexing both succeed.
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// source type from the ref scheme (e.g. "github:" → source: "github"), but
|
|
384
|
-
// an update should not reclassify an existing entry.
|
|
385
|
-
source: managed.source,
|
|
386
|
-
ref: synced.ref,
|
|
387
|
-
artifactUrl: synced.artifactUrl,
|
|
388
|
-
resolvedVersion: synced.resolvedVersion,
|
|
389
|
-
resolvedRevision: synced.resolvedRevision,
|
|
390
|
-
stashRoot: synced.contentDir,
|
|
391
|
-
cacheDir: synced.cacheDir,
|
|
392
|
-
installedAt: synced.syncedAt,
|
|
393
|
-
writable: synced.writable ?? managed.writable,
|
|
394
|
-
};
|
|
395
|
-
const movedRoot = managed.localRoot !== "" &&
|
|
396
|
-
path.resolve(managed.localRoot) !== path.resolve(synced.contentDir) &&
|
|
397
|
-
managed.source !== "local" &&
|
|
398
|
-
!managed.writable;
|
|
399
|
-
if (movedRoot) {
|
|
400
|
-
const { confirmDestructive } = await import("../../cli/confirm.js");
|
|
401
|
-
const confirmed = await confirmDestructive(`Update resolved a new content directory for "${managed.installId}" (${synced.contentDir}) and would delete the previous install directory at ${managed.localRoot}. This cannot be undone.`, { yes });
|
|
402
|
-
if (!confirmed)
|
|
403
|
-
throw new UsageError(`Update cancelled for "${managed.installId}"; no configuration was changed.`);
|
|
404
|
-
}
|
|
405
|
-
return withAssetMutationLease("source-update", async () => {
|
|
406
|
-
const config = loadConfig();
|
|
407
|
-
const oldLocks = readLockfile();
|
|
408
|
-
if (!config.bundles?.[managed.bundleKey]) {
|
|
409
|
-
throw new ConfigError(`Managed source "${managed.installId}" disappeared before update publication.`);
|
|
410
|
-
}
|
|
411
|
-
const desiredLock = {
|
|
412
|
-
id: managed.bundleKey,
|
|
413
|
-
// Preserve the STORED install kind: a `github:`-ref entry recorded as
|
|
414
|
-
// source "git" must not be reclassified by the sync flow's re-derivation.
|
|
947
|
+
const prepared = await prepareManagedUpdate(managed, force);
|
|
948
|
+
try {
|
|
949
|
+
await auditPreparedUpdate(prepared, managed.installId, managed.ref, allowInsecure);
|
|
950
|
+
const synced = prepared.synced;
|
|
951
|
+
const installedEntry = {
|
|
952
|
+
id: managed.installId,
|
|
953
|
+
// Preserve the original source classification. syncFromRef() re-derives the
|
|
954
|
+
// source type from the ref scheme (e.g. "github:" → source: "github"), but
|
|
955
|
+
// an update should not reclassify an existing entry.
|
|
415
956
|
source: managed.source,
|
|
416
957
|
ref: synced.ref,
|
|
958
|
+
artifactUrl: synced.artifactUrl,
|
|
417
959
|
resolvedVersion: synced.resolvedVersion,
|
|
418
960
|
resolvedRevision: synced.resolvedRevision,
|
|
419
|
-
|
|
420
|
-
|
|
961
|
+
stashRoot: synced.contentDir,
|
|
962
|
+
cacheDir: synced.cacheDir,
|
|
421
963
|
installedAt: synced.syncedAt,
|
|
964
|
+
writable: synced.writable ?? managed.writable,
|
|
422
965
|
};
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const index = await akmIndex({ stashDir });
|
|
428
|
-
if (generationHash(readLockfile()) !== desiredLockGeneration)
|
|
429
|
-
throw concurrentGenerationError(managed);
|
|
966
|
+
const movedRoot = managed.localRoot !== "" &&
|
|
967
|
+
path.resolve(managed.localRoot) !== path.resolve(synced.contentDir) &&
|
|
968
|
+
managed.source !== "local" &&
|
|
969
|
+
!managed.writable;
|
|
430
970
|
if (movedRoot) {
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
cleanupDirectoryBestEffort(managed.localRoot, "update");
|
|
438
|
-
}
|
|
971
|
+
const { confirmDestructive } = await import("../../cli/confirm.js");
|
|
972
|
+
const confirmed = await confirmDestructive(`Update resolved a new content directory for "${managed.installId}" (${synced.contentDir}) and would delete the previous install directory at ${managed.localRoot}. This cannot be undone.`, { yes });
|
|
973
|
+
if (!confirmed)
|
|
974
|
+
throw new UsageError(`Update cancelled for "${managed.installId}"; no configuration was changed.`);
|
|
439
975
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
976
|
+
return await withAssetMutationLease("source-update", async () => {
|
|
977
|
+
const oldLockSnapshot = readLockfileForUpdate();
|
|
978
|
+
const oldLocks = oldLockSnapshot.entries;
|
|
979
|
+
const desiredLock = {
|
|
980
|
+
id: managed.bundleKey,
|
|
981
|
+
// Preserve the STORED install kind: a `github:`-ref entry recorded as
|
|
982
|
+
// source "git" must not be reclassified by the sync flow's re-derivation.
|
|
446
983
|
source: managed.source,
|
|
447
|
-
ref:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
984
|
+
ref: synced.ref,
|
|
985
|
+
resolvedVersion: synced.resolvedVersion,
|
|
986
|
+
resolvedRevision: synced.resolvedRevision,
|
|
987
|
+
integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
|
|
988
|
+
localRoot: synced.contentDir,
|
|
989
|
+
installedAt: synced.syncedAt,
|
|
990
|
+
};
|
|
991
|
+
const desiredLocks = [...oldLocks.filter((entry) => entry.id !== desiredLock.id), desiredLock];
|
|
992
|
+
const desiredLockGeneration = generationHash(desiredLocks);
|
|
993
|
+
let transaction;
|
|
994
|
+
let releaseConfigFence;
|
|
995
|
+
let contentPublished = false;
|
|
996
|
+
let lockPublished = false;
|
|
997
|
+
let publishedLockSnapshot;
|
|
998
|
+
let committed = false;
|
|
999
|
+
let index;
|
|
1000
|
+
try {
|
|
1001
|
+
const configFence = acquireConfigReadFence();
|
|
1002
|
+
releaseConfigFence = configFence.release;
|
|
1003
|
+
assertBundleAuditFence(configFence.config, managed.bundleKey, managed.auditConfigGeneration);
|
|
1004
|
+
updateTransactionHook("fenced", managed.installId);
|
|
1005
|
+
if (prepared.publication) {
|
|
1006
|
+
prepared.publication.publish();
|
|
1007
|
+
contentPublished = true;
|
|
1008
|
+
}
|
|
1009
|
+
await verifyPublishedAudit(prepared, managed.installId);
|
|
1010
|
+
publishedLockSnapshot = await publishLockfileUpdate(oldLockSnapshot, desiredLocks);
|
|
1011
|
+
if (!publishedLockSnapshot)
|
|
1012
|
+
throw concurrentGenerationError(managed);
|
|
1013
|
+
lockPublished = true;
|
|
1014
|
+
updateTransactionHook("published", managed.installId);
|
|
1015
|
+
updateTransactionHook("before-index", managed.installId);
|
|
1016
|
+
transaction = openUnifiedUpdateTransaction();
|
|
1017
|
+
index = await akmIndex({
|
|
1018
|
+
stashDir,
|
|
1019
|
+
full: true,
|
|
1020
|
+
hydrateSources: false,
|
|
1021
|
+
persistDetectedAdapters: false,
|
|
1022
|
+
deferredUpdateTransaction: transaction.deferred,
|
|
1023
|
+
});
|
|
1024
|
+
const currentLockSnapshot = readLockfileForUpdate();
|
|
1025
|
+
if (generationHash(currentLockSnapshot.entries) !== desiredLockGeneration ||
|
|
1026
|
+
currentLockSnapshot.raw !== publishedLockSnapshot.raw ||
|
|
1027
|
+
currentLockSnapshot.mode !== publishedLockSnapshot.mode) {
|
|
1028
|
+
throw concurrentGenerationError(managed);
|
|
1029
|
+
}
|
|
1030
|
+
updateTransactionHook("indexed", managed.installId, { db: transaction.db });
|
|
1031
|
+
// Indexing can be long enough for a local writer to race the first
|
|
1032
|
+
// post-materialization audit. Re-verify the exact bytes/findings at the
|
|
1033
|
+
// final cross-store commit boundary while every AKM fence is held.
|
|
1034
|
+
await verifyPublishedAudit(prepared, managed.installId);
|
|
1035
|
+
updateTransactionHook("before-commit", managed.installId, { db: transaction.db });
|
|
1036
|
+
commitUnifiedUpdateTransaction(transaction);
|
|
1037
|
+
committed = true;
|
|
1038
|
+
try {
|
|
1039
|
+
transaction.deferred.afterCommit?.();
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
catch (error) {
|
|
1046
|
+
let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
|
|
1047
|
+
let concurrentGeneration = false;
|
|
1048
|
+
let concurrentGenerationOwnsPublishedRoot = false;
|
|
1049
|
+
if (lockPublished) {
|
|
1050
|
+
try {
|
|
1051
|
+
if (!publishedLockSnapshot ||
|
|
1052
|
+
!(await compareAndSwapLockfileSnapshot(publishedLockSnapshot, oldLockSnapshot))) {
|
|
1053
|
+
concurrentGeneration = true;
|
|
1054
|
+
const concurrentLocks = readLockfileForUpdate().entries;
|
|
1055
|
+
const concurrentEntry = concurrentLocks.find((entry) => entry.id === desiredLock.id);
|
|
1056
|
+
concurrentGenerationOwnsPublishedRoot =
|
|
1057
|
+
prepared.publication !== undefined &&
|
|
1058
|
+
concurrentEntry?.localRoot !== undefined &&
|
|
1059
|
+
path.resolve(concurrentEntry.localRoot) === path.resolve(synced.contentDir);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
catch (compensationError) {
|
|
1063
|
+
recoveryError ??= compensationError;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (!concurrentGenerationOwnsPublishedRoot) {
|
|
1067
|
+
try {
|
|
1068
|
+
if (contentPublished)
|
|
1069
|
+
prepared.publication?.rollback();
|
|
1070
|
+
}
|
|
1071
|
+
catch (rollbackError) {
|
|
1072
|
+
recoveryError ??= rollbackError;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (recoveryError)
|
|
1076
|
+
throw recoveryError;
|
|
1077
|
+
if (concurrentGeneration)
|
|
1078
|
+
throw concurrentGenerationError(managed);
|
|
1079
|
+
throw error;
|
|
1080
|
+
}
|
|
1081
|
+
finally {
|
|
1082
|
+
try {
|
|
1083
|
+
if (transaction)
|
|
1084
|
+
closeUnifiedUpdateTransaction(transaction, committed);
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
releaseConfigFence?.();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
prepared.publication?.commit();
|
|
1091
|
+
if (movedRoot) {
|
|
1092
|
+
const currentConfig = loadConfig();
|
|
1093
|
+
const currentLocks = readLockfile();
|
|
1094
|
+
if (referencesRoot(managed.localRoot, currentConfig, currentLocks)) {
|
|
1095
|
+
warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
cleanupDirectoryBestEffort(managed.localRoot, "update");
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const versionChanged = (managed.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
|
|
1102
|
+
const revisionChanged = (managed.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
|
|
1103
|
+
return {
|
|
1104
|
+
index,
|
|
1105
|
+
item: {
|
|
1106
|
+
id: managed.installId,
|
|
1107
|
+
source: managed.source,
|
|
1108
|
+
ref: managed.ref,
|
|
1109
|
+
previous: {
|
|
1110
|
+
resolvedVersion: managed.resolvedVersion,
|
|
1111
|
+
resolvedRevision: managed.resolvedRevision,
|
|
1112
|
+
cacheDir: managed.localRoot,
|
|
1113
|
+
},
|
|
1114
|
+
installed: { ...installedEntry, extractedDir: synced.extractedDir },
|
|
1115
|
+
changed: {
|
|
1116
|
+
version: versionChanged,
|
|
1117
|
+
revision: revisionChanged,
|
|
1118
|
+
any: versionChanged || revisionChanged,
|
|
1119
|
+
},
|
|
458
1120
|
},
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
}
|
|
1121
|
+
};
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
finally {
|
|
1125
|
+
prepared.cleanup();
|
|
1126
|
+
}
|
|
462
1127
|
}
|
|
463
1128
|
function generationHash(value) {
|
|
464
1129
|
return sha256Hex(JSON.stringify(value));
|
|
@@ -494,13 +1159,14 @@ export async function akmUpdate(input) {
|
|
|
494
1159
|
const all = input?.all === true;
|
|
495
1160
|
const force = input?.force === true;
|
|
496
1161
|
const yes = input?.yes === true;
|
|
1162
|
+
const allowInsecure = input?.allowInsecure === true;
|
|
497
1163
|
const config = loadConfig();
|
|
498
1164
|
const managedInstalls = listManagedInstalls(config);
|
|
499
1165
|
if (target && !all) {
|
|
500
1166
|
// Registry-managed install (lock-backed) — re-download from its locator.
|
|
501
1167
|
const managed = resolveManagedTarget(config, target);
|
|
502
1168
|
if (managed) {
|
|
503
|
-
const updated = await updateManagedInstall(managed, force, yes, stashDir);
|
|
1169
|
+
const updated = await updateManagedInstall(managed, force, yes, stashDir, allowInsecure);
|
|
504
1170
|
return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
|
|
505
1171
|
}
|
|
506
1172
|
// Plain git / website source (bundles without a lock) — provider re-sync.
|
|
@@ -529,7 +1195,7 @@ export async function akmUpdate(input) {
|
|
|
529
1195
|
return false;
|
|
530
1196
|
});
|
|
531
1197
|
if (gitMatch)
|
|
532
|
-
return updateGitSource(stashDir, target, all, gitMatch);
|
|
1198
|
+
return updateGitSource(stashDir, target, all, gitMatch, allowInsecure);
|
|
533
1199
|
const websiteMatch = stashes.find((s) => {
|
|
534
1200
|
if (s.type !== "website")
|
|
535
1201
|
return false;
|
|
@@ -542,7 +1208,7 @@ export async function akmUpdate(input) {
|
|
|
542
1208
|
return false;
|
|
543
1209
|
});
|
|
544
1210
|
if (websiteMatch)
|
|
545
|
-
return updateWebsiteSource(stashDir, target, all, websiteMatch);
|
|
1211
|
+
return updateWebsiteSource(stashDir, target, all, websiteMatch, allowInsecure);
|
|
546
1212
|
// Plain npm source (bundle without a lock) — sync via the registry
|
|
547
1213
|
// pipeline and promote to a managed install (see
|
|
548
1214
|
// managedInstallViewOfPlainNpm's doc comment for why npm can't stay
|
|
@@ -557,7 +1223,7 @@ export async function akmUpdate(input) {
|
|
|
557
1223
|
return false;
|
|
558
1224
|
});
|
|
559
1225
|
if (npmMatch) {
|
|
560
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir);
|
|
1226
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir, allowInsecure);
|
|
561
1227
|
return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
|
|
562
1228
|
}
|
|
563
1229
|
}
|
|
@@ -566,71 +1232,88 @@ export async function akmUpdate(input) {
|
|
|
566
1232
|
: managedInstalls;
|
|
567
1233
|
const selected = selectManagedTargets(config, enabledManagedInstalls, target, all);
|
|
568
1234
|
const processed = [];
|
|
569
|
-
|
|
1235
|
+
const plainSynced = [];
|
|
1236
|
+
const skipped = [];
|
|
1237
|
+
let latestIndex;
|
|
570
1238
|
for (const managed of selected) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1239
|
+
try {
|
|
1240
|
+
const updated = await updateManagedInstall(managed, force, yes, stashDir, allowInsecure);
|
|
1241
|
+
processed.push(updated.item);
|
|
1242
|
+
latestIndex = updated.index;
|
|
1243
|
+
}
|
|
1244
|
+
catch (error) {
|
|
1245
|
+
if (!all)
|
|
1246
|
+
throw error;
|
|
1247
|
+
skipped.push(updateFailureOutcome(managed.installId, sourceKindForInstall(managed.source), error));
|
|
1248
|
+
}
|
|
574
1249
|
}
|
|
575
1250
|
// `--all` must account for EVERY configured source, not only the
|
|
576
1251
|
// registry-managed (lock-backed) ones (R-015) — `selectManagedTargets`
|
|
577
1252
|
// above returns only `installs` for `all`, so plain sources were
|
|
578
1253
|
// previously never even looked at. Git/npm plain sources are synced here
|
|
579
|
-
// too (git
|
|
580
|
-
// path above)
|
|
581
|
-
//
|
|
582
|
-
// `selectManagedTargets` already uses for a single unmatched target.
|
|
583
|
-
let plainSynced;
|
|
584
|
-
let skipped;
|
|
585
|
-
let sawGitSync = false;
|
|
1254
|
+
// too (git and website through staged promotion; npm promoted to managed,
|
|
1255
|
+
// same as the single-target path above). Filesystem sources reflect their
|
|
1256
|
+
// files in place and are reported as intentionally skipped.
|
|
586
1257
|
if (all) {
|
|
587
1258
|
const managedKeys = new Set(managedInstalls.map((m) => m.bundleKey));
|
|
588
1259
|
const plainSources = getSources(config).filter((source) => source.enabled !== false && !managedKeys.has(source.name ?? ""));
|
|
589
|
-
plainSynced = [];
|
|
590
|
-
skipped = [];
|
|
591
1260
|
for (const plain of plainSources) {
|
|
592
1261
|
const id = plain.name ?? plain.path ?? plain.url ?? "";
|
|
593
1262
|
try {
|
|
594
1263
|
if (plain.type === "git") {
|
|
595
|
-
|
|
596
|
-
|
|
1264
|
+
const updated = await syncGitPlainSource(plain, stashDir, allowInsecure);
|
|
1265
|
+
plainSynced.push(updated.item);
|
|
1266
|
+
latestIndex = updated.index;
|
|
597
1267
|
}
|
|
598
1268
|
else if (plain.type === "npm") {
|
|
599
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir);
|
|
1269
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir, allowInsecure);
|
|
600
1270
|
processed.push(updated.item);
|
|
601
|
-
|
|
1271
|
+
latestIndex = updated.index;
|
|
602
1272
|
}
|
|
603
1273
|
else if (plain.type === "website") {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
reason: `website caching not yet implemented for --all; run \`akm bundle update ${id}\` to re-mirror this source individually.`,
|
|
608
|
-
});
|
|
1274
|
+
const updated = await syncWebsitePlainSource(plain, stashDir, allowInsecure);
|
|
1275
|
+
plainSynced.push(updated.item);
|
|
1276
|
+
latestIndex = updated.index;
|
|
609
1277
|
}
|
|
610
1278
|
else {
|
|
611
1279
|
skipped.push({
|
|
612
1280
|
id,
|
|
613
1281
|
kind: plain.type,
|
|
1282
|
+
status: "skipped",
|
|
614
1283
|
reason: "reflects your files in place and has no remote to sync; run `akm index` to refresh the search index.",
|
|
615
1284
|
});
|
|
616
1285
|
}
|
|
617
1286
|
}
|
|
618
|
-
catch (
|
|
619
|
-
skipped.push(
|
|
620
|
-
id,
|
|
621
|
-
kind: plain.type,
|
|
622
|
-
reason: `sync failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
623
|
-
});
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
skipped.push(updateFailureOutcome(id, plain.type, error));
|
|
624
1289
|
}
|
|
625
1290
|
}
|
|
626
1291
|
}
|
|
627
1292
|
return buildUpdateResponse(stashDir, target, all, processed, {
|
|
628
|
-
full: sawGitSync,
|
|
629
1293
|
plainSynced,
|
|
630
1294
|
skipped,
|
|
631
|
-
|
|
1295
|
+
index: latestIndex ?? readCurrentIndexSummary(),
|
|
632
1296
|
});
|
|
633
1297
|
}
|
|
1298
|
+
function sourceKindForInstall(source) {
|
|
1299
|
+
if (source === "local")
|
|
1300
|
+
return "filesystem";
|
|
1301
|
+
if (source === "github")
|
|
1302
|
+
return "git";
|
|
1303
|
+
return source;
|
|
1304
|
+
}
|
|
1305
|
+
function updateFailureOutcome(id, kind, error) {
|
|
1306
|
+
const code = error instanceof AkmError ? error.code : undefined;
|
|
1307
|
+
const status = code === "DANGEROUS_ENV_KEY" || code === "DANGEROUS_ENV_AUDIT_FAILED" ? "blocked" : "failed";
|
|
1308
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1309
|
+
return {
|
|
1310
|
+
id,
|
|
1311
|
+
kind,
|
|
1312
|
+
status,
|
|
1313
|
+
...(code ? { code } : {}),
|
|
1314
|
+
reason: `${status}: ${message}`,
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
634
1317
|
function selectManagedTargets(config, installs, target, all) {
|
|
635
1318
|
if (all && target) {
|
|
636
1319
|
throw new UsageError("Specify either <target> or --all, not both.", "MISSING_OR_AMBIGUOUS_TARGET");
|