akm-cli 0.9.1 → 0.9.2-alpha.2
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 +103 -28
- 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/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- 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 +5 -6
- 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-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- 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 +53 -22
- 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 +901 -177
- 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 +85 -35
- 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 +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- 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 +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- 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 +31 -33
- 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 +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- 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 +203 -229
- 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 +64 -71
- 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 +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- 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 +159 -36
- 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 -224
- 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,
|
|
@@ -293,37 +355,569 @@ async function buildUpdateResponse(stashDir, target, all, processed, opts) {
|
|
|
293
355
|
totalEntries: index.totalEntries,
|
|
294
356
|
directoriesScanned: index.directoriesScanned,
|
|
295
357
|
directoriesSkipped: index.directoriesSkipped,
|
|
358
|
+
...(index.scanComplete !== undefined ? { scanComplete: index.scanComplete } : {}),
|
|
296
359
|
},
|
|
297
360
|
};
|
|
298
361
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
362
|
+
function incompleteFilesystemOutcome(id) {
|
|
363
|
+
return {
|
|
364
|
+
id,
|
|
365
|
+
kind: "filesystem",
|
|
366
|
+
status: "skipped",
|
|
367
|
+
code: "SOURCE_SCAN_INCOMPLETE",
|
|
368
|
+
reason: `Filesystem source "${id}" was not scanned completely; preserving its last-known-good index rows.`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
let updateTransactionHookForTests;
|
|
372
|
+
/** TEST-ONLY. Inject faults at source-update transaction boundaries. */
|
|
373
|
+
export function _setUpdateTransactionHookForTests(hook) {
|
|
374
|
+
updateTransactionHookForTests = hook;
|
|
375
|
+
}
|
|
376
|
+
function updateTransactionHook(point, id, context) {
|
|
377
|
+
updateTransactionHookForTests?.(point, id, context);
|
|
378
|
+
}
|
|
379
|
+
function gitHead(repoDir) {
|
|
380
|
+
const result = runGit(["-C", repoDir, "rev-parse", "HEAD"]);
|
|
381
|
+
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : undefined;
|
|
382
|
+
}
|
|
383
|
+
function invalidWritableGitPublication(repoDir) {
|
|
384
|
+
return {
|
|
385
|
+
publish() {
|
|
386
|
+
throw new ConfigError(`Writable Git source at ${repoDir} is not a valid checkout; refusing to replace its live directory.`);
|
|
387
|
+
},
|
|
388
|
+
rollback() { },
|
|
389
|
+
commit() { },
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function writableGitPublication(opts) {
|
|
393
|
+
const auditedTargetHead = opts.auditedTargetHead ?? gitHead(opts.stagedRepo);
|
|
394
|
+
if (!opts.expectedOldHead || !auditedTargetHead)
|
|
395
|
+
return invalidWritableGitPublication(opts.liveRepo);
|
|
396
|
+
return prepareWritableGitPublication({
|
|
397
|
+
stagedRepo: opts.stagedRepo,
|
|
398
|
+
liveRepo: opts.liveRepo,
|
|
399
|
+
expectedOldHead: opts.expectedOldHead,
|
|
400
|
+
auditedTargetHead,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const UPDATE_STATE_SCHEMA = "akm_update_state";
|
|
404
|
+
function sqliteStringLiteral(value) {
|
|
405
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
406
|
+
}
|
|
407
|
+
function openUnifiedUpdateTransaction() {
|
|
408
|
+
// Keep canonical state activity registered for the entire attached
|
|
409
|
+
// transaction so maintenance/migration cannot replace state.db underneath
|
|
410
|
+
// the borrowed index connection.
|
|
411
|
+
const stateActivity = openStateDatabase();
|
|
412
|
+
try {
|
|
413
|
+
const statePath = getStateDbPath();
|
|
414
|
+
const db = openIndexDatabase(undefined, {
|
|
415
|
+
beforeSchema: (candidate) => {
|
|
416
|
+
candidate.exec(`ATTACH DATABASE ${sqliteStringLiteral(statePath)} AS "${UPDATE_STATE_SCHEMA}"`);
|
|
417
|
+
// openIndexDatabase invokes this before ensureSchema, so the outer
|
|
418
|
+
// transaction begins before the first update-owned index mutation.
|
|
419
|
+
beginImmediateTransaction(candidate);
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
db,
|
|
424
|
+
stateActivity,
|
|
425
|
+
deferred: { db, stateSchema: UPDATE_STATE_SCHEMA },
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
stateActivity.close();
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function rollbackUnifiedUpdateTransaction(transaction) {
|
|
434
|
+
if (!transaction.db.inTransaction)
|
|
435
|
+
return undefined;
|
|
436
|
+
try {
|
|
437
|
+
transaction.db.exec("ROLLBACK");
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
return error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function commitUnifiedUpdateTransaction(transaction) {
|
|
445
|
+
if (!transaction.db.inTransaction) {
|
|
446
|
+
throw new Error("Source update index returned without its active unified index/state transaction.");
|
|
447
|
+
}
|
|
448
|
+
transaction.db.exec("COMMIT");
|
|
449
|
+
}
|
|
450
|
+
function closeUnifiedUpdateTransaction(transaction, committed) {
|
|
451
|
+
let closeError = rollbackUnifiedUpdateTransaction(transaction);
|
|
452
|
+
try {
|
|
453
|
+
transaction.db.close();
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
closeError ??= error;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
transaction.stateActivity.close();
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
closeError ??= error;
|
|
463
|
+
}
|
|
464
|
+
if (closeError) {
|
|
465
|
+
warn(committed
|
|
466
|
+
? `[akm bundle update] committed, but closing its database handles failed: ${String(closeError)}`
|
|
467
|
+
: `[akm bundle update] rolled back, but closing its database handles failed: ${String(closeError)}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function pathAtOrBelow(candidate, root) {
|
|
471
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
472
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
473
|
+
}
|
|
474
|
+
function remapStagedPath(candidate, stagedRoot, liveRoot) {
|
|
475
|
+
if (!pathAtOrBelow(candidate, stagedRoot))
|
|
476
|
+
return candidate;
|
|
477
|
+
return path.join(liveRoot, path.relative(stagedRoot, candidate));
|
|
478
|
+
}
|
|
479
|
+
function dangerousFindingGeneration(findings) {
|
|
480
|
+
return JSON.stringify(findings
|
|
481
|
+
.map(({ envRef, keyName, relPath }) => ({ envRef, keyName, relPath }))
|
|
482
|
+
.sort((left, right) => `${left.relPath}\0${left.envRef}\0${left.keyName}`.localeCompare(`${right.relPath}\0${right.envRef}\0${right.keyName}`)));
|
|
483
|
+
}
|
|
484
|
+
async function contentGeneration(root) {
|
|
485
|
+
const hash = createHash("sha256");
|
|
486
|
+
const resolvedRoot = path.resolve(root);
|
|
487
|
+
const walk = async (directory) => {
|
|
488
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
489
|
+
for (const entry of entries) {
|
|
490
|
+
if (entry.name === ".git")
|
|
491
|
+
continue;
|
|
492
|
+
const absolute = path.join(directory, entry.name);
|
|
493
|
+
const relative = path.relative(resolvedRoot, absolute).replaceAll(path.sep, "/");
|
|
494
|
+
const before = fs.lstatSync(absolute);
|
|
495
|
+
if (before.isDirectory()) {
|
|
496
|
+
hash.update(`D\0${relative}\0${before.mode & 0o777}\0`);
|
|
497
|
+
await walk(absolute);
|
|
498
|
+
}
|
|
499
|
+
else if (before.isSymbolicLink()) {
|
|
500
|
+
hash.update(`L\0${relative}\0${fs.readlinkSync(absolute)}\0`);
|
|
501
|
+
}
|
|
502
|
+
else if (before.isFile()) {
|
|
503
|
+
hash.update(`F\0${relative}\0${before.mode & 0o777}\0${before.size}\0`);
|
|
504
|
+
for await (const chunk of fs.createReadStream(absolute))
|
|
505
|
+
hash.update(chunk);
|
|
506
|
+
const after = fs.lstatSync(absolute);
|
|
507
|
+
if (after.ino !== before.ino || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
|
|
508
|
+
throw new ConfigError(`Source content changed while ${relative} was being audited; retry the update.`);
|
|
509
|
+
}
|
|
510
|
+
hash.update("\0");
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
throw new ConfigError(`Unsupported filesystem entry ${absolute}; refusing to audit an unstable source tree.`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
if (!fs.statSync(resolvedRoot).isDirectory())
|
|
518
|
+
throw new ConfigError(`Expected ${resolvedRoot} to be a directory.`);
|
|
519
|
+
await walk(resolvedRoot);
|
|
520
|
+
return hash.digest("hex");
|
|
521
|
+
}
|
|
522
|
+
async function verifyPublishedAudit(prepared, id) {
|
|
523
|
+
const liveRoot = prepared.publishedAuditRoot;
|
|
524
|
+
if (!liveRoot)
|
|
525
|
+
return;
|
|
526
|
+
const resolveLiveAuditRoot = () => {
|
|
527
|
+
const physicalRoot = canonicalWritablePath(liveRoot, `published component root for "${id}"`);
|
|
528
|
+
const containmentRoot = prepared.publishedAuditContainmentRoot;
|
|
529
|
+
if (containmentRoot && !pathAtOrBelow(physicalRoot, containmentRoot)) {
|
|
530
|
+
throw new ConfigError(`Published component root ${liveRoot} resolves outside its physical checkout at ${containmentRoot}.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
531
|
+
}
|
|
532
|
+
const expected = prepared.publishedAuditExpectedPhysicalRoot;
|
|
533
|
+
if (expected && path.resolve(physicalRoot) !== path.resolve(expected)) {
|
|
534
|
+
throw new ConfigError(`Published component root ${liveRoot} resolves to ${physicalRoot}, not its audited staged target ${expected}.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
535
|
+
}
|
|
536
|
+
return physicalRoot;
|
|
537
|
+
};
|
|
538
|
+
const beforeRoot = resolveLiveAuditRoot();
|
|
539
|
+
const beforeAudit = await contentGeneration(beforeRoot);
|
|
540
|
+
let findings;
|
|
541
|
+
try {
|
|
542
|
+
const scanRoot = resolveLiveAuditRoot();
|
|
543
|
+
if (scanRoot !== beforeRoot)
|
|
544
|
+
throw new Error("configured component symlink changed during re-audit");
|
|
545
|
+
findings = scanStashForDangerousKeys(scanRoot);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
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");
|
|
549
|
+
}
|
|
550
|
+
const afterRoot = resolveLiveAuditRoot();
|
|
551
|
+
const afterAudit = await contentGeneration(afterRoot);
|
|
552
|
+
if (afterRoot !== beforeRoot ||
|
|
553
|
+
beforeAudit !== prepared.auditedContentGeneration ||
|
|
554
|
+
afterAudit !== prepared.auditedContentGeneration ||
|
|
555
|
+
dangerousFindingGeneration(findings) !== prepared.approvedDangerousFindings) {
|
|
556
|
+
throw new ConfigError(`Writable source "${id}" changed while its audited Git target was materialized; refusing to activate unaudited bytes.`, "DANGEROUS_ENV_AUDIT_FAILED");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function auditPreparedUpdate(prepared, id, ref, allowInsecure) {
|
|
560
|
+
updateTransactionHook("staged", id);
|
|
561
|
+
const decision = await auditStashForDangerousKeys({
|
|
562
|
+
stashRoot: prepared.auditRoot,
|
|
563
|
+
ref,
|
|
564
|
+
allowDangerousKeys: allowInsecure,
|
|
565
|
+
isTTY: process.stdin.isTTY === true,
|
|
566
|
+
operation: "update",
|
|
567
|
+
renderBlockedError: false,
|
|
315
568
|
});
|
|
316
|
-
|
|
569
|
+
if (decision.blocked)
|
|
570
|
+
throw new NotFoundError(decision.error, decision.code);
|
|
571
|
+
prepared.approvedDangerousFindings = dangerousFindingGeneration(decision.findings);
|
|
572
|
+
prepared.auditedContentGeneration = await contentGeneration(prepared.auditRoot);
|
|
573
|
+
updateTransactionHook("audited", id);
|
|
574
|
+
}
|
|
575
|
+
function canonicalWritablePath(candidate, description) {
|
|
576
|
+
try {
|
|
577
|
+
return fs.realpathSync(candidate);
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
throw new ConfigError(`Cannot resolve ${description} at ${candidate}; refusing writable Git update. ${error instanceof Error ? error.message : String(error)}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function containedWritablePath(candidate, physicalRepo, description) {
|
|
584
|
+
const physicalCandidate = canonicalWritablePath(candidate, description);
|
|
585
|
+
if (!pathAtOrBelow(physicalCandidate, physicalRepo)) {
|
|
586
|
+
throw new ConfigError(`${description} ${candidate} resolves outside its physical checkout at ${physicalRepo}.`);
|
|
587
|
+
}
|
|
588
|
+
return physicalCandidate;
|
|
589
|
+
}
|
|
590
|
+
function prepareWritableManagedUpdate(managed, force) {
|
|
591
|
+
const physicalLocalRoot = canonicalWritablePath(managed.localRoot, "managed content root");
|
|
592
|
+
const topLevel = runGit(["-C", physicalLocalRoot, "rev-parse", "--show-toplevel"]);
|
|
593
|
+
// Production writable installs are Git checkouts. Falling back to the
|
|
594
|
+
// locked root keeps the provider seam testable; the real Git provider still
|
|
595
|
+
// refuses a copied non-checkout before publication.
|
|
596
|
+
const liveRepo = topLevel.status === 0 && topLevel.stdout.trim()
|
|
597
|
+
? canonicalWritablePath(topLevel.stdout.trim(), "managed Git top-level")
|
|
598
|
+
: physicalLocalRoot;
|
|
599
|
+
if (!pathAtOrBelow(physicalLocalRoot, liveRepo)) {
|
|
600
|
+
throw new ConfigError(`Writable Git content root ${managed.localRoot} resolves outside its physical checkout at ${liveRepo}.`);
|
|
601
|
+
}
|
|
602
|
+
// Keep the configured component path as a lexical identity relative to the
|
|
603
|
+
// content root. Resolving it to today's symlink target here would pin the old
|
|
604
|
+
// target and miss an upstream commit that retargets the symlink.
|
|
605
|
+
const lexicalLocalRoot = path.resolve(managed.localRoot);
|
|
606
|
+
const configuredLiveRoots = managed.requiredRoots.map((root) => {
|
|
607
|
+
const lexicalRoot = path.resolve(root);
|
|
608
|
+
if (!pathAtOrBelow(lexicalRoot, lexicalLocalRoot)) {
|
|
609
|
+
throw new ConfigError(`Configured writable Git component root ${lexicalRoot} escapes content root ${lexicalLocalRoot}.`);
|
|
610
|
+
}
|
|
611
|
+
return path.resolve(physicalLocalRoot, path.relative(lexicalLocalRoot, lexicalRoot));
|
|
612
|
+
});
|
|
613
|
+
const physicalRequiredRoots = configuredLiveRoots.map((root) => containedWritablePath(root, liveRepo, `Configured component root for "${managed.installId}"`));
|
|
614
|
+
// The Git provider must preserve ordinary component directories, but not a
|
|
615
|
+
// symlink's current physical target. The configured lexical path is audited
|
|
616
|
+
// again after sync, so retaining the old target here would reject a valid
|
|
617
|
+
// upstream retarget before the new target can reach that audit.
|
|
618
|
+
const preservedPhysicalRequiredRoots = physicalRequiredRoots.filter((root, index) => configuredLiveRoots[index] === root);
|
|
619
|
+
const expectedOldHead = gitHead(liveRepo);
|
|
620
|
+
const stagingParent = createStagingParent(path.dirname(liveRepo));
|
|
621
|
+
const stagedRepo = path.join(stagingParent, path.basename(liveRepo));
|
|
622
|
+
fs.cpSync(liveRepo, stagedRepo, { recursive: true, preserveTimestamps: true, verbatimSymlinks: true });
|
|
623
|
+
const stagedContentRoot = remapStagedPath(physicalLocalRoot, liveRepo, stagedRepo);
|
|
624
|
+
const stagedPhysicalRequiredRoots = preservedPhysicalRequiredRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
|
|
625
|
+
const stagedConfiguredRoots = configuredLiveRoots.map((root) => remapStagedPath(root, liveRepo, stagedRepo));
|
|
626
|
+
return syncFromRef(managed.ref, {
|
|
627
|
+
force,
|
|
628
|
+
writable: true,
|
|
629
|
+
writableRoot: stagedContentRoot,
|
|
630
|
+
...(stagedPhysicalRequiredRoots.length > 0 ? { writableRequiredRoots: stagedPhysicalRequiredRoots } : {}),
|
|
631
|
+
})
|
|
632
|
+
.then((staged) => {
|
|
633
|
+
const auditedTargetHead = gitHead(stagedRepo);
|
|
634
|
+
const physicalStagedRepo = canonicalWritablePath(stagedRepo, "staged managed Git checkout");
|
|
635
|
+
const auditedConfiguredRoots = stagedConfiguredRoots.map((root) => containedWritablePath(root, physicalStagedRepo, `Post-sync component root for "${managed.installId}"`));
|
|
636
|
+
const expectedPublishedRoots = auditedConfiguredRoots.map((root) => path.join(liveRepo, path.relative(physicalStagedRepo, root)));
|
|
637
|
+
const synced = {
|
|
638
|
+
...staged,
|
|
639
|
+
resolvedRevision: auditedTargetHead ?? staged.resolvedRevision,
|
|
640
|
+
contentDir: remapStagedPath(staged.contentDir, stagedRepo, liveRepo),
|
|
641
|
+
cacheDir: remapStagedPath(staged.cacheDir, stagedRepo, liveRepo),
|
|
642
|
+
extractedDir: remapStagedPath(staged.extractedDir, stagedRepo, liveRepo),
|
|
643
|
+
};
|
|
644
|
+
return {
|
|
645
|
+
synced,
|
|
646
|
+
auditRoot: auditedConfiguredRoots[0] ?? staged.contentDir,
|
|
647
|
+
publishedAuditRoot: configuredLiveRoots[0] ?? synced.contentDir,
|
|
648
|
+
publishedAuditContainmentRoot: liveRepo,
|
|
649
|
+
publishedAuditExpectedPhysicalRoot: expectedPublishedRoots[0] ?? synced.contentDir,
|
|
650
|
+
publication: writableGitPublication({ stagedRepo, liveRepo, expectedOldHead, auditedTargetHead }),
|
|
651
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
652
|
+
};
|
|
653
|
+
})
|
|
654
|
+
.catch((error) => {
|
|
655
|
+
cleanupStagingParent(stagingParent);
|
|
656
|
+
throw error;
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
async function prepareManagedUpdate(managed, force) {
|
|
660
|
+
if (managed.writable && managed.localRoot)
|
|
661
|
+
return prepareWritableManagedUpdate(managed, force);
|
|
662
|
+
if (managed.source === "local") {
|
|
663
|
+
const synced = await syncFromRef(managed.ref, { force, writable: managed.writable });
|
|
664
|
+
return {
|
|
665
|
+
synced,
|
|
666
|
+
auditRoot: resolveComponentAuditRoot(synced.contentDir, managed.componentRoot, managed.installId),
|
|
667
|
+
cleanup: () => { },
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const canonicalCacheRoot = getRegistryCacheDir();
|
|
671
|
+
const stagingParent = createStagingParent(canonicalCacheRoot);
|
|
672
|
+
try {
|
|
673
|
+
const staged = await syncFromRef(managed.ref, {
|
|
674
|
+
force,
|
|
675
|
+
writable: false,
|
|
676
|
+
cacheRootDir: stagingParent,
|
|
677
|
+
});
|
|
678
|
+
if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
|
|
679
|
+
// Test/provider seams may return an already-isolated candidate. It remains
|
|
680
|
+
// invisible until the lock points at it, so no directory promotion is needed.
|
|
681
|
+
return {
|
|
682
|
+
synced: staged,
|
|
683
|
+
auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
|
|
684
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const liveCacheDir = path.join(canonicalCacheRoot, path.relative(stagingParent, staged.cacheDir));
|
|
688
|
+
const synced = {
|
|
689
|
+
...staged,
|
|
690
|
+
contentDir: remapStagedPath(staged.contentDir, staged.cacheDir, liveCacheDir),
|
|
691
|
+
cacheDir: liveCacheDir,
|
|
692
|
+
extractedDir: remapStagedPath(staged.extractedDir, staged.cacheDir, liveCacheDir),
|
|
693
|
+
};
|
|
694
|
+
return {
|
|
695
|
+
synced,
|
|
696
|
+
auditRoot: resolveComponentAuditRoot(staged.contentDir, managed.componentRoot, managed.installId),
|
|
697
|
+
publication: prepareDirectoryPublication(staged.cacheDir, liveCacheDir),
|
|
698
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
cleanupStagingParent(stagingParent);
|
|
703
|
+
throw error;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function resolveComponentAuditRoot(contentRoot, componentRoot, id) {
|
|
707
|
+
const resolved = path.resolve(contentRoot, componentRoot);
|
|
708
|
+
if (!pathAtOrBelow(resolved, contentRoot)) {
|
|
709
|
+
throw new ConfigError(`Component root "${componentRoot}" escapes staged bundle "${id}".`);
|
|
710
|
+
}
|
|
711
|
+
return resolved;
|
|
712
|
+
}
|
|
713
|
+
async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, full, expectedConfigGeneration) {
|
|
714
|
+
try {
|
|
715
|
+
await auditPreparedUpdate(prepared, id, ref, allowInsecure);
|
|
716
|
+
return await withAssetMutationLease("source-update", async () => {
|
|
717
|
+
let transaction;
|
|
718
|
+
let releaseConfigFence;
|
|
719
|
+
let contentPublished = false;
|
|
720
|
+
let committed = false;
|
|
721
|
+
try {
|
|
722
|
+
const configFence = acquireConfigReadFence();
|
|
723
|
+
releaseConfigFence = configFence.release;
|
|
724
|
+
assertBundleAuditFence(configFence.config, id, expectedConfigGeneration);
|
|
725
|
+
updateTransactionHook("fenced", id);
|
|
726
|
+
if (prepared.publication) {
|
|
727
|
+
prepared.publication.publish();
|
|
728
|
+
contentPublished = true;
|
|
729
|
+
}
|
|
730
|
+
await verifyPublishedAudit(prepared, id);
|
|
731
|
+
updateTransactionHook("published", id);
|
|
732
|
+
updateTransactionHook("before-index", id);
|
|
733
|
+
transaction = openUnifiedUpdateTransaction();
|
|
734
|
+
const index = await akmIndex({
|
|
735
|
+
stashDir,
|
|
736
|
+
...(full ? { full: true } : {}),
|
|
737
|
+
hydrateSources: false,
|
|
738
|
+
persistDetectedAdapters: false,
|
|
739
|
+
deferredUpdateTransaction: transaction.deferred,
|
|
740
|
+
});
|
|
741
|
+
updateTransactionHook("indexed", id, { db: transaction.db });
|
|
742
|
+
// Re-fence the exact published worktree after the potentially long
|
|
743
|
+
// index pass and immediately before the coordinator commit. This
|
|
744
|
+
// catches cooperating or observable external writes that land after
|
|
745
|
+
// the first post-materialization audit.
|
|
746
|
+
await verifyPublishedAudit(prepared, id);
|
|
747
|
+
updateTransactionHook("before-commit", id, { db: transaction.db });
|
|
748
|
+
commitUnifiedUpdateTransaction(transaction);
|
|
749
|
+
committed = true;
|
|
750
|
+
try {
|
|
751
|
+
transaction.deferred.afterCommit?.();
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
|
|
755
|
+
}
|
|
756
|
+
prepared.publication?.commit();
|
|
757
|
+
return index;
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
|
|
761
|
+
try {
|
|
762
|
+
if (contentPublished)
|
|
763
|
+
prepared.publication?.rollback();
|
|
764
|
+
}
|
|
765
|
+
catch (rollbackError) {
|
|
766
|
+
recoveryError ??= rollbackError;
|
|
767
|
+
}
|
|
768
|
+
if (recoveryError)
|
|
769
|
+
throw recoveryError;
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
finally {
|
|
773
|
+
try {
|
|
774
|
+
if (transaction)
|
|
775
|
+
closeUnifiedUpdateTransaction(transaction, committed);
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
releaseConfigFence?.();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
prepared.cleanup();
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async function prepareGitPlainUpdate(gitSource, componentRoot, writable) {
|
|
788
|
+
if (!gitSource.url)
|
|
789
|
+
throw new ConfigError(`Git source "${gitSource.name ?? "git"}" has no URL.`);
|
|
790
|
+
const repo = parseGitRepoUrl(gitSource.url);
|
|
791
|
+
const livePaths = getCachePaths(repo.canonicalUrl);
|
|
792
|
+
const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
|
|
793
|
+
const stagedPaths = getCachePaths(repo.canonicalUrl, stagingParent);
|
|
794
|
+
try {
|
|
795
|
+
const expectedOldHead = writable ? gitHead(livePaths.repoDir) : undefined;
|
|
796
|
+
if (writable && fs.existsSync(livePaths.rootDir)) {
|
|
797
|
+
fs.cpSync(livePaths.rootDir, stagedPaths.rootDir, { recursive: true, preserveTimestamps: true });
|
|
798
|
+
}
|
|
799
|
+
const staged = await syncMirroredRepo(gitSource, {
|
|
800
|
+
force: true,
|
|
801
|
+
writable,
|
|
802
|
+
cacheRootDir: stagingParent,
|
|
803
|
+
});
|
|
804
|
+
const configuredStagedAuditRoot = resolveComponentAuditRoot(resolveGitContentRoot(staged.contentDir), componentRoot, gitSource.name ?? "git");
|
|
805
|
+
if (!pathAtOrBelow(staged.cacheDir, stagingParent)) {
|
|
806
|
+
if (pathAtOrBelow(configuredStagedAuditRoot, livePaths.rootDir)) {
|
|
807
|
+
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.`);
|
|
808
|
+
}
|
|
809
|
+
// A provider test seam may hand back an already-isolated candidate.
|
|
810
|
+
// Audit it, but do not move an unowned directory into the canonical
|
|
811
|
+
// cache. Real providers honor cacheRootDir and use the promotion below.
|
|
812
|
+
return {
|
|
813
|
+
auditRoot: configuredStagedAuditRoot,
|
|
814
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
let auditRoot = configuredStagedAuditRoot;
|
|
818
|
+
let publishedAuditRoot;
|
|
819
|
+
let publishedAuditContainmentRoot;
|
|
820
|
+
let publishedAuditExpectedPhysicalRoot;
|
|
821
|
+
if (writable) {
|
|
822
|
+
const physicalStagedRepo = canonicalWritablePath(stagedPaths.repoDir, "staged plain Git checkout");
|
|
823
|
+
auditRoot = containedWritablePath(configuredStagedAuditRoot, physicalStagedRepo, `Post-sync component root for "${gitSource.name ?? "git"}"`);
|
|
824
|
+
publishedAuditRoot = remapStagedPath(configuredStagedAuditRoot, staged.cacheDir, livePaths.rootDir);
|
|
825
|
+
publishedAuditContainmentRoot = canonicalWritablePath(livePaths.repoDir, "plain Git checkout");
|
|
826
|
+
publishedAuditExpectedPhysicalRoot = path.join(publishedAuditContainmentRoot, path.relative(physicalStagedRepo, auditRoot));
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
auditRoot,
|
|
830
|
+
...(publishedAuditRoot ? { publishedAuditRoot } : {}),
|
|
831
|
+
...(publishedAuditContainmentRoot ? { publishedAuditContainmentRoot } : {}),
|
|
832
|
+
...(publishedAuditExpectedPhysicalRoot ? { publishedAuditExpectedPhysicalRoot } : {}),
|
|
833
|
+
publication: writable
|
|
834
|
+
? writableGitPublication({
|
|
835
|
+
stagedRepo: stagedPaths.repoDir,
|
|
836
|
+
liveRepo: livePaths.repoDir,
|
|
837
|
+
expectedOldHead,
|
|
838
|
+
})
|
|
839
|
+
: prepareDirectoryPublication(staged.cacheDir, livePaths.rootDir),
|
|
840
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
cleanupStagingParent(stagingParent);
|
|
845
|
+
throw error;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async function syncGitPlainSource(gitSource, stashDir, allowInsecure) {
|
|
849
|
+
const id = gitSource.name ?? gitSource.url ?? "";
|
|
850
|
+
const currentConfig = loadConfig();
|
|
851
|
+
const currentSource = currentPlainSource(currentConfig, id, "git");
|
|
852
|
+
const ref = currentSource.url ?? "";
|
|
853
|
+
const fence = bundleAuditFence(currentConfig, id);
|
|
854
|
+
const prepared = await prepareGitPlainUpdate(currentSource, fence.componentRoot, fence.writable);
|
|
855
|
+
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, true, fence.generation);
|
|
856
|
+
return { item: { id, kind: "git", ref }, index };
|
|
857
|
+
}
|
|
858
|
+
async function prepareWebsitePlainUpdate(websiteSource, componentRoot) {
|
|
859
|
+
if (!websiteSource.url)
|
|
860
|
+
throw new ConfigError(`Website source "${websiteSource.name ?? "website"}" has no URL.`);
|
|
861
|
+
const livePaths = getWebsiteCachePaths(websiteSource.url);
|
|
862
|
+
const stagingParent = createStagingParent(path.dirname(livePaths.rootDir));
|
|
863
|
+
const stagedPaths = getWebsiteCachePaths(websiteSource.url, stagingParent);
|
|
864
|
+
try {
|
|
865
|
+
const provider = createWebsiteProvider(websiteSource);
|
|
866
|
+
await provider.sync({
|
|
867
|
+
force: true,
|
|
868
|
+
secrets: storeSecretResolver,
|
|
869
|
+
ensureWebsiteMirror: (config, options) => ensureWebsiteMirror(config, {
|
|
870
|
+
...options,
|
|
871
|
+
resolveSecret: options?.resolveSecret ?? storeSecretResolver.resolveSecret,
|
|
872
|
+
cacheRootDir: stagingParent,
|
|
873
|
+
}),
|
|
874
|
+
});
|
|
875
|
+
return {
|
|
876
|
+
auditRoot: resolveComponentAuditRoot(stagedPaths.stashDir, componentRoot, websiteSource.name ?? "website"),
|
|
877
|
+
publication: prepareDirectoryPublication(stagedPaths.rootDir, livePaths.rootDir),
|
|
878
|
+
cleanup: () => cleanupStagingParent(stagingParent),
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
cleanupStagingParent(stagingParent);
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async function syncWebsitePlainSource(websiteSource, stashDir, allowInsecure) {
|
|
887
|
+
const id = websiteSource.name ?? websiteSource.url ?? "";
|
|
888
|
+
const currentConfig = loadConfig();
|
|
889
|
+
const currentSource = currentPlainSource(currentConfig, id, "website");
|
|
890
|
+
const ref = currentSource.url ?? "";
|
|
891
|
+
const fence = bundleAuditFence(currentConfig, id);
|
|
892
|
+
const prepared = await prepareWebsitePlainUpdate(currentSource, fence.componentRoot);
|
|
893
|
+
const index = await publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInsecure, true, fence.generation);
|
|
894
|
+
return { item: { id, kind: "website", ref }, index };
|
|
317
895
|
}
|
|
318
896
|
/** 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, [], {
|
|
897
|
+
async function updateGitSource(stashDir, target, all, gitSource, allowInsecure) {
|
|
898
|
+
const synced = await syncGitPlainSource(gitSource, stashDir, allowInsecure);
|
|
899
|
+
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
322
900
|
}
|
|
323
901
|
/** 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] });
|
|
902
|
+
async function updateWebsiteSource(stashDir, target, all, websiteSource, allowInsecure) {
|
|
903
|
+
const synced = await syncWebsitePlainSource(websiteSource, stashDir, allowInsecure);
|
|
904
|
+
return buildUpdateResponse(stashDir, target, all, [], { plainSynced: [synced.item], index: synced.index });
|
|
905
|
+
}
|
|
906
|
+
/** Reconcile a filesystem source's current bytes without provider hydration. */
|
|
907
|
+
async function updateFilesystemSource(stashDir, target, all, filesystemSource) {
|
|
908
|
+
const id = filesystemSource.name ?? filesystemSource.path ?? target;
|
|
909
|
+
const ref = filesystemSource.path ?? target;
|
|
910
|
+
const index = await akmIndex({ stashDir, hydrateSources: false });
|
|
911
|
+
if (!index.scanComplete) {
|
|
912
|
+
return buildUpdateResponse(stashDir, target, all, [], {
|
|
913
|
+
skipped: [incompleteFilesystemOutcome(id)],
|
|
914
|
+
index,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
return buildUpdateResponse(stashDir, target, all, [], {
|
|
918
|
+
plainSynced: [{ id, kind: "filesystem", ref }],
|
|
919
|
+
index,
|
|
920
|
+
});
|
|
327
921
|
}
|
|
328
922
|
/**
|
|
329
923
|
* A plain (lockless) npm bundle has no deterministic content path — unlike
|
|
@@ -338,9 +932,12 @@ async function updateWebsiteSource(stashDir, target, all, websiteSource) {
|
|
|
338
932
|
* bookkeeping.
|
|
339
933
|
*/
|
|
340
934
|
function managedInstallViewOfPlainNpm(npmSource) {
|
|
341
|
-
const
|
|
935
|
+
const id = npmSource.name ?? npmSource.path ?? "";
|
|
936
|
+
const config = loadConfig();
|
|
937
|
+
const currentSource = currentPlainSource(config, id, "npm");
|
|
938
|
+
const spec = currentSource.path ?? "";
|
|
342
939
|
const ref = spec.startsWith("npm:") ? spec : `npm:${spec}`;
|
|
343
|
-
const
|
|
940
|
+
const fence = bundleAuditFence(config, id);
|
|
344
941
|
return {
|
|
345
942
|
bundleKey: id,
|
|
346
943
|
installId: id,
|
|
@@ -350,7 +947,9 @@ function managedInstallViewOfPlainNpm(npmSource) {
|
|
|
350
947
|
resolvedVersion: undefined,
|
|
351
948
|
resolvedRevision: undefined,
|
|
352
949
|
writable: false,
|
|
950
|
+
componentRoot: fence.componentRoot,
|
|
353
951
|
requiredRoots: [],
|
|
952
|
+
auditConfigGeneration: fence.generation,
|
|
354
953
|
};
|
|
355
954
|
}
|
|
356
955
|
/**
|
|
@@ -364,101 +963,193 @@ function managedInstallViewOfPlainNpm(npmSource) {
|
|
|
364
963
|
* scripts; only the branch that can `rm -rf` needs a gate, not the whole
|
|
365
964
|
* command).
|
|
366
965
|
*/
|
|
367
|
-
async function updateManagedInstall(managed, force, yes, stashDir) {
|
|
966
|
+
async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure) {
|
|
368
967
|
// No pre-cleanup of the old root, even under --force: the providers already
|
|
369
968
|
// re-materialize staging-first (git clones into a `.tmp-*` sibling and swaps
|
|
370
969
|
// only on success), so destroying `managed.localRoot` BEFORE `syncFromRef`
|
|
371
970
|
// succeeds would turn any sync failure (network down, bad ref) into losing a
|
|
372
971
|
// previously-working install. The old root is cleaned up below only after
|
|
373
972
|
// 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.
|
|
973
|
+
const prepared = await prepareManagedUpdate(managed, force);
|
|
974
|
+
try {
|
|
975
|
+
await auditPreparedUpdate(prepared, managed.installId, managed.ref, allowInsecure);
|
|
976
|
+
const synced = prepared.synced;
|
|
977
|
+
const installedEntry = {
|
|
978
|
+
id: managed.installId,
|
|
979
|
+
// Preserve the original source classification. syncFromRef() re-derives the
|
|
980
|
+
// source type from the ref scheme (e.g. "github:" → source: "github"), but
|
|
981
|
+
// an update should not reclassify an existing entry.
|
|
415
982
|
source: managed.source,
|
|
416
983
|
ref: synced.ref,
|
|
984
|
+
artifactUrl: synced.artifactUrl,
|
|
417
985
|
resolvedVersion: synced.resolvedVersion,
|
|
418
986
|
resolvedRevision: synced.resolvedRevision,
|
|
419
|
-
|
|
420
|
-
|
|
987
|
+
stashRoot: synced.contentDir,
|
|
988
|
+
cacheDir: synced.cacheDir,
|
|
421
989
|
installedAt: synced.syncedAt,
|
|
990
|
+
writable: synced.writable ?? managed.writable,
|
|
422
991
|
};
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const index = await akmIndex({ stashDir });
|
|
428
|
-
if (generationHash(readLockfile()) !== desiredLockGeneration)
|
|
429
|
-
throw concurrentGenerationError(managed);
|
|
992
|
+
const movedRoot = managed.localRoot !== "" &&
|
|
993
|
+
path.resolve(managed.localRoot) !== path.resolve(synced.contentDir) &&
|
|
994
|
+
managed.source !== "local" &&
|
|
995
|
+
!managed.writable;
|
|
430
996
|
if (movedRoot) {
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
cleanupDirectoryBestEffort(managed.localRoot, "update");
|
|
438
|
-
}
|
|
997
|
+
const { confirmDestructive } = await import("../../cli/confirm.js");
|
|
998
|
+
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 });
|
|
999
|
+
if (!confirmed)
|
|
1000
|
+
throw new UsageError(`Update cancelled for "${managed.installId}"; no configuration was changed.`);
|
|
439
1001
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1002
|
+
return await withAssetMutationLease("source-update", async () => {
|
|
1003
|
+
const oldLockSnapshot = readLockfileForUpdate();
|
|
1004
|
+
const oldLocks = oldLockSnapshot.entries;
|
|
1005
|
+
const desiredLock = {
|
|
1006
|
+
id: managed.bundleKey,
|
|
1007
|
+
// Preserve the STORED install kind: a `github:`-ref entry recorded as
|
|
1008
|
+
// source "git" must not be reclassified by the sync flow's re-derivation.
|
|
446
1009
|
source: managed.source,
|
|
447
|
-
ref:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
1010
|
+
ref: synced.ref,
|
|
1011
|
+
resolvedVersion: synced.resolvedVersion,
|
|
1012
|
+
resolvedRevision: synced.resolvedRevision,
|
|
1013
|
+
integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
|
|
1014
|
+
localRoot: synced.contentDir,
|
|
1015
|
+
installedAt: synced.syncedAt,
|
|
1016
|
+
};
|
|
1017
|
+
const desiredLocks = [...oldLocks.filter((entry) => entry.id !== desiredLock.id), desiredLock];
|
|
1018
|
+
const desiredLockGeneration = generationHash(desiredLocks);
|
|
1019
|
+
let transaction;
|
|
1020
|
+
let releaseConfigFence;
|
|
1021
|
+
let contentPublished = false;
|
|
1022
|
+
let lockPublished = false;
|
|
1023
|
+
let publishedLockSnapshot;
|
|
1024
|
+
let committed = false;
|
|
1025
|
+
let index;
|
|
1026
|
+
try {
|
|
1027
|
+
const configFence = acquireConfigReadFence();
|
|
1028
|
+
releaseConfigFence = configFence.release;
|
|
1029
|
+
assertBundleAuditFence(configFence.config, managed.bundleKey, managed.auditConfigGeneration);
|
|
1030
|
+
updateTransactionHook("fenced", managed.installId);
|
|
1031
|
+
if (prepared.publication) {
|
|
1032
|
+
prepared.publication.publish();
|
|
1033
|
+
contentPublished = true;
|
|
1034
|
+
}
|
|
1035
|
+
await verifyPublishedAudit(prepared, managed.installId);
|
|
1036
|
+
publishedLockSnapshot = await publishLockfileUpdate(oldLockSnapshot, desiredLocks);
|
|
1037
|
+
if (!publishedLockSnapshot)
|
|
1038
|
+
throw concurrentGenerationError(managed);
|
|
1039
|
+
lockPublished = true;
|
|
1040
|
+
updateTransactionHook("published", managed.installId);
|
|
1041
|
+
updateTransactionHook("before-index", managed.installId);
|
|
1042
|
+
transaction = openUnifiedUpdateTransaction();
|
|
1043
|
+
index = await akmIndex({
|
|
1044
|
+
stashDir,
|
|
1045
|
+
full: true,
|
|
1046
|
+
hydrateSources: false,
|
|
1047
|
+
persistDetectedAdapters: false,
|
|
1048
|
+
deferredUpdateTransaction: transaction.deferred,
|
|
1049
|
+
});
|
|
1050
|
+
const currentLockSnapshot = readLockfileForUpdate();
|
|
1051
|
+
if (generationHash(currentLockSnapshot.entries) !== desiredLockGeneration ||
|
|
1052
|
+
currentLockSnapshot.raw !== publishedLockSnapshot.raw ||
|
|
1053
|
+
currentLockSnapshot.mode !== publishedLockSnapshot.mode) {
|
|
1054
|
+
throw concurrentGenerationError(managed);
|
|
1055
|
+
}
|
|
1056
|
+
updateTransactionHook("indexed", managed.installId, { db: transaction.db });
|
|
1057
|
+
// Indexing can be long enough for a local writer to race the first
|
|
1058
|
+
// post-materialization audit. Re-verify the exact bytes/findings at the
|
|
1059
|
+
// final cross-store commit boundary while every AKM fence is held.
|
|
1060
|
+
await verifyPublishedAudit(prepared, managed.installId);
|
|
1061
|
+
updateTransactionHook("before-commit", managed.installId, { db: transaction.db });
|
|
1062
|
+
commitUnifiedUpdateTransaction(transaction);
|
|
1063
|
+
committed = true;
|
|
1064
|
+
try {
|
|
1065
|
+
transaction.deferred.afterCommit?.();
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
|
|
1073
|
+
let concurrentGeneration = false;
|
|
1074
|
+
let concurrentGenerationOwnsPublishedRoot = false;
|
|
1075
|
+
if (lockPublished) {
|
|
1076
|
+
try {
|
|
1077
|
+
if (!publishedLockSnapshot ||
|
|
1078
|
+
!(await compareAndSwapLockfileSnapshot(publishedLockSnapshot, oldLockSnapshot))) {
|
|
1079
|
+
concurrentGeneration = true;
|
|
1080
|
+
const concurrentLocks = readLockfileForUpdate().entries;
|
|
1081
|
+
const concurrentEntry = concurrentLocks.find((entry) => entry.id === desiredLock.id);
|
|
1082
|
+
concurrentGenerationOwnsPublishedRoot =
|
|
1083
|
+
prepared.publication !== undefined &&
|
|
1084
|
+
concurrentEntry?.localRoot !== undefined &&
|
|
1085
|
+
path.resolve(concurrentEntry.localRoot) === path.resolve(synced.contentDir);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
catch (compensationError) {
|
|
1089
|
+
recoveryError ??= compensationError;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (!concurrentGenerationOwnsPublishedRoot) {
|
|
1093
|
+
try {
|
|
1094
|
+
if (contentPublished)
|
|
1095
|
+
prepared.publication?.rollback();
|
|
1096
|
+
}
|
|
1097
|
+
catch (rollbackError) {
|
|
1098
|
+
recoveryError ??= rollbackError;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (recoveryError)
|
|
1102
|
+
throw recoveryError;
|
|
1103
|
+
if (concurrentGeneration)
|
|
1104
|
+
throw concurrentGenerationError(managed);
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
finally {
|
|
1108
|
+
try {
|
|
1109
|
+
if (transaction)
|
|
1110
|
+
closeUnifiedUpdateTransaction(transaction, committed);
|
|
1111
|
+
}
|
|
1112
|
+
finally {
|
|
1113
|
+
releaseConfigFence?.();
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
prepared.publication?.commit();
|
|
1117
|
+
if (movedRoot) {
|
|
1118
|
+
const currentConfig = loadConfig();
|
|
1119
|
+
const currentLocks = readLockfile();
|
|
1120
|
+
if (referencesRoot(managed.localRoot, currentConfig, currentLocks)) {
|
|
1121
|
+
warn(`[akm bundle update] kept previous install directory at ${managed.localRoot} because another configured or locked bundle still references it.`);
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
cleanupDirectoryBestEffort(managed.localRoot, "update");
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
const versionChanged = (managed.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
|
|
1128
|
+
const revisionChanged = (managed.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");
|
|
1129
|
+
return {
|
|
1130
|
+
index,
|
|
1131
|
+
item: {
|
|
1132
|
+
id: managed.installId,
|
|
1133
|
+
source: managed.source,
|
|
1134
|
+
ref: managed.ref,
|
|
1135
|
+
previous: {
|
|
1136
|
+
resolvedVersion: managed.resolvedVersion,
|
|
1137
|
+
resolvedRevision: managed.resolvedRevision,
|
|
1138
|
+
cacheDir: managed.localRoot,
|
|
1139
|
+
},
|
|
1140
|
+
installed: { ...installedEntry, extractedDir: synced.extractedDir },
|
|
1141
|
+
changed: {
|
|
1142
|
+
version: versionChanged,
|
|
1143
|
+
revision: revisionChanged,
|
|
1144
|
+
any: versionChanged || revisionChanged,
|
|
1145
|
+
},
|
|
458
1146
|
},
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
}
|
|
1147
|
+
};
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
finally {
|
|
1151
|
+
prepared.cleanup();
|
|
1152
|
+
}
|
|
462
1153
|
}
|
|
463
1154
|
function generationHash(value) {
|
|
464
1155
|
return sha256Hex(JSON.stringify(value));
|
|
@@ -494,13 +1185,14 @@ export async function akmUpdate(input) {
|
|
|
494
1185
|
const all = input?.all === true;
|
|
495
1186
|
const force = input?.force === true;
|
|
496
1187
|
const yes = input?.yes === true;
|
|
1188
|
+
const allowInsecure = input?.allowInsecure === true;
|
|
497
1189
|
const config = loadConfig();
|
|
498
1190
|
const managedInstalls = listManagedInstalls(config);
|
|
499
1191
|
if (target && !all) {
|
|
500
1192
|
// Registry-managed install (lock-backed) — re-download from its locator.
|
|
501
1193
|
const managed = resolveManagedTarget(config, target);
|
|
502
1194
|
if (managed) {
|
|
503
|
-
const updated = await updateManagedInstall(managed, force, yes, stashDir);
|
|
1195
|
+
const updated = await updateManagedInstall(managed, force, yes, stashDir, allowInsecure);
|
|
504
1196
|
return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
|
|
505
1197
|
}
|
|
506
1198
|
// Plain git / website source (bundles without a lock) — provider re-sync.
|
|
@@ -529,7 +1221,7 @@ export async function akmUpdate(input) {
|
|
|
529
1221
|
return false;
|
|
530
1222
|
});
|
|
531
1223
|
if (gitMatch)
|
|
532
|
-
return updateGitSource(stashDir, target, all, gitMatch);
|
|
1224
|
+
return updateGitSource(stashDir, target, all, gitMatch, allowInsecure);
|
|
533
1225
|
const websiteMatch = stashes.find((s) => {
|
|
534
1226
|
if (s.type !== "website")
|
|
535
1227
|
return false;
|
|
@@ -542,7 +1234,7 @@ export async function akmUpdate(input) {
|
|
|
542
1234
|
return false;
|
|
543
1235
|
});
|
|
544
1236
|
if (websiteMatch)
|
|
545
|
-
return updateWebsiteSource(stashDir, target, all, websiteMatch);
|
|
1237
|
+
return updateWebsiteSource(stashDir, target, all, websiteMatch, allowInsecure);
|
|
546
1238
|
// Plain npm source (bundle without a lock) — sync via the registry
|
|
547
1239
|
// pipeline and promote to a managed install (see
|
|
548
1240
|
// managedInstallViewOfPlainNpm's doc comment for why npm can't stay
|
|
@@ -557,80 +1249,132 @@ export async function akmUpdate(input) {
|
|
|
557
1249
|
return false;
|
|
558
1250
|
});
|
|
559
1251
|
if (npmMatch) {
|
|
560
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir);
|
|
1252
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(npmMatch), force, yes, stashDir, allowInsecure);
|
|
561
1253
|
return buildUpdateResponse(stashDir, target, all, [updated.item], { index: updated.index });
|
|
562
1254
|
}
|
|
1255
|
+
const filesystemMatch = stashes.find((source) => {
|
|
1256
|
+
if (source.type !== "filesystem")
|
|
1257
|
+
return false;
|
|
1258
|
+
if (source.name === target)
|
|
1259
|
+
return true;
|
|
1260
|
+
return resolvedPath !== undefined && source.path !== undefined && path.resolve(source.path) === resolvedPath;
|
|
1261
|
+
});
|
|
1262
|
+
if (filesystemMatch)
|
|
1263
|
+
return updateFilesystemSource(stashDir, target, all, filesystemMatch);
|
|
563
1264
|
}
|
|
564
1265
|
const enabledManagedInstalls = all
|
|
565
1266
|
? managedInstalls.filter((managed) => config.bundles?.[managed.bundleKey]?.enabled !== false)
|
|
566
1267
|
: managedInstalls;
|
|
567
1268
|
const selected = selectManagedTargets(config, enabledManagedInstalls, target, all);
|
|
568
1269
|
const processed = [];
|
|
569
|
-
|
|
1270
|
+
const plainSynced = [];
|
|
1271
|
+
const skipped = [];
|
|
1272
|
+
let latestIndex;
|
|
570
1273
|
for (const managed of selected) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1274
|
+
try {
|
|
1275
|
+
const updated = await updateManagedInstall(managed, force, yes, stashDir, allowInsecure);
|
|
1276
|
+
processed.push(updated.item);
|
|
1277
|
+
latestIndex = updated.index;
|
|
1278
|
+
}
|
|
1279
|
+
catch (error) {
|
|
1280
|
+
if (!all)
|
|
1281
|
+
throw error;
|
|
1282
|
+
skipped.push(updateFailureOutcome(managed.installId, sourceKindForInstall(managed.source), error));
|
|
1283
|
+
}
|
|
574
1284
|
}
|
|
575
1285
|
// `--all` must account for EVERY configured source, not only the
|
|
576
1286
|
// registry-managed (lock-backed) ones (R-015) — `selectManagedTargets`
|
|
577
1287
|
// above returns only `installs` for `all`, so plain sources were
|
|
578
1288
|
// 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;
|
|
1289
|
+
// too (git and website through staged promotion; npm promoted to managed,
|
|
1290
|
+
// same as the single-target path above). Filesystem sources reflect their
|
|
1291
|
+
// files in place and are reported as intentionally skipped.
|
|
586
1292
|
if (all) {
|
|
587
1293
|
const managedKeys = new Set(managedInstalls.map((m) => m.bundleKey));
|
|
588
1294
|
const plainSources = getSources(config).filter((source) => source.enabled !== false && !managedKeys.has(source.name ?? ""));
|
|
589
|
-
|
|
590
|
-
skipped = [];
|
|
1295
|
+
const filesystemSources = [];
|
|
591
1296
|
for (const plain of plainSources) {
|
|
592
1297
|
const id = plain.name ?? plain.path ?? plain.url ?? "";
|
|
593
1298
|
try {
|
|
594
1299
|
if (plain.type === "git") {
|
|
595
|
-
|
|
596
|
-
|
|
1300
|
+
const updated = await syncGitPlainSource(plain, stashDir, allowInsecure);
|
|
1301
|
+
plainSynced.push(updated.item);
|
|
1302
|
+
latestIndex = updated.index;
|
|
597
1303
|
}
|
|
598
1304
|
else if (plain.type === "npm") {
|
|
599
|
-
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir);
|
|
1305
|
+
const updated = await updateManagedInstall(managedInstallViewOfPlainNpm(plain), force, yes, stashDir, allowInsecure);
|
|
600
1306
|
processed.push(updated.item);
|
|
601
|
-
|
|
1307
|
+
latestIndex = updated.index;
|
|
602
1308
|
}
|
|
603
1309
|
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
|
-
});
|
|
1310
|
+
const updated = await syncWebsitePlainSource(plain, stashDir, allowInsecure);
|
|
1311
|
+
plainSynced.push(updated.item);
|
|
1312
|
+
latestIndex = updated.index;
|
|
609
1313
|
}
|
|
610
1314
|
else {
|
|
611
|
-
|
|
612
|
-
id,
|
|
613
|
-
kind: plain.type,
|
|
614
|
-
reason: "reflects your files in place and has no remote to sync; run `akm index` to refresh the search index.",
|
|
615
|
-
});
|
|
1315
|
+
filesystemSources.push(plain);
|
|
616
1316
|
}
|
|
617
1317
|
}
|
|
618
|
-
catch (
|
|
619
|
-
skipped.push(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1318
|
+
catch (error) {
|
|
1319
|
+
skipped.push(updateFailureOutcome(id, plain.type, error));
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (filesystemSources.length > 0) {
|
|
1323
|
+
try {
|
|
1324
|
+
// A remote source may have reconciled before a later source finished
|
|
1325
|
+
// hydrating, so an incomplete intermediate result is not authoritative
|
|
1326
|
+
// for the final `--all` outcome. Retry once after every source update;
|
|
1327
|
+
// a genuinely missing filesystem root remains incomplete and is
|
|
1328
|
+
// reported below without claiming reconciliation.
|
|
1329
|
+
if (!latestIndex?.scanComplete)
|
|
1330
|
+
latestIndex = await akmIndex({ stashDir, hydrateSources: false });
|
|
1331
|
+
if (latestIndex.scanComplete) {
|
|
1332
|
+
for (const source of filesystemSources) {
|
|
1333
|
+
plainSynced.push({
|
|
1334
|
+
id: source.name ?? source.path ?? "",
|
|
1335
|
+
kind: "filesystem",
|
|
1336
|
+
ref: source.path ?? source.name ?? "",
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
for (const source of filesystemSources) {
|
|
1342
|
+
skipped.push(incompleteFilesystemOutcome(source.name ?? source.path ?? ""));
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
catch (error) {
|
|
1347
|
+
for (const source of filesystemSources) {
|
|
1348
|
+
skipped.push(updateFailureOutcome(source.name ?? source.path ?? "", "filesystem", error));
|
|
1349
|
+
}
|
|
624
1350
|
}
|
|
625
1351
|
}
|
|
626
1352
|
}
|
|
627
1353
|
return buildUpdateResponse(stashDir, target, all, processed, {
|
|
628
|
-
full: sawGitSync,
|
|
629
1354
|
plainSynced,
|
|
630
1355
|
skipped,
|
|
631
|
-
|
|
1356
|
+
index: latestIndex ?? readCurrentIndexSummary(),
|
|
632
1357
|
});
|
|
633
1358
|
}
|
|
1359
|
+
function sourceKindForInstall(source) {
|
|
1360
|
+
if (source === "local")
|
|
1361
|
+
return "filesystem";
|
|
1362
|
+
if (source === "github")
|
|
1363
|
+
return "git";
|
|
1364
|
+
return source;
|
|
1365
|
+
}
|
|
1366
|
+
function updateFailureOutcome(id, kind, error) {
|
|
1367
|
+
const code = error instanceof AkmError ? error.code : undefined;
|
|
1368
|
+
const status = code === "DANGEROUS_ENV_KEY" || code === "DANGEROUS_ENV_AUDIT_FAILED" ? "blocked" : "failed";
|
|
1369
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1370
|
+
return {
|
|
1371
|
+
id,
|
|
1372
|
+
kind,
|
|
1373
|
+
status,
|
|
1374
|
+
...(code ? { code } : {}),
|
|
1375
|
+
reason: `${status}: ${message}`,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
634
1378
|
function selectManagedTargets(config, installs, target, all) {
|
|
635
1379
|
if (all && target) {
|
|
636
1380
|
throw new UsageError("Specify either <target> or --all, not both.", "MISSING_OR_AMBIGUOUS_TARGET");
|
|
@@ -643,26 +1387,6 @@ function selectManagedTargets(config, installs, target, all) {
|
|
|
643
1387
|
const found = resolveManagedTarget(config, target);
|
|
644
1388
|
if (found)
|
|
645
1389
|
return [found];
|
|
646
|
-
// Give a helpful message when the target names a plain (non-managed) source.
|
|
647
|
-
const stashes = getSources(config);
|
|
648
|
-
const isUrl = target.startsWith("http://") || target.startsWith("https://");
|
|
649
|
-
const resolvedPath = !isUrl ? path.resolve(target) : undefined;
|
|
650
|
-
const stashMatch = stashes.find((s) => {
|
|
651
|
-
if (isUrl && s.url === target)
|
|
652
|
-
return true;
|
|
653
|
-
if (resolvedPath && s.path && path.resolve(s.path) === resolvedPath)
|
|
654
|
-
return true;
|
|
655
|
-
if (s.name === target)
|
|
656
|
-
return true;
|
|
657
|
-
return false;
|
|
658
|
-
});
|
|
659
|
-
if (stashMatch) {
|
|
660
|
-
if (stashMatch.type === "website") {
|
|
661
|
-
throw new UsageError(`"${target}" is a website source — website caching not yet implemented for --all. ` +
|
|
662
|
-
`Run \`akm bundle update ${target}\` to re-mirror this source individually.`, "TARGET_NOT_UPDATABLE");
|
|
663
|
-
}
|
|
664
|
-
throw new UsageError(`"${target}" is a local directory — it reflects your files in place. To refresh the search index, run: akm index`, "TARGET_NOT_UPDATABLE");
|
|
665
|
-
}
|
|
666
1390
|
throw new NotFoundError(`No matching source for target: ${target}`, "SOURCE_NOT_FOUND");
|
|
667
1391
|
}
|
|
668
1392
|
/**
|