@the-open-engine/zeroshot 6.38.0 → 6.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/event-copy.js +8 -7
- package/cli/event-copy.ts +12 -0
- package/cli/index.js +7 -31
- package/cli/json-export.js +61 -64
- package/cli/json-export.ts +112 -0
- package/cli/message-formatter-utils.js +68 -56
- package/cli/message-formatter-utils.ts +121 -0
- package/cli/message-formatters-normal.js +124 -259
- package/cli/message-formatters-normal.ts +250 -0
- package/cli/message-formatters-watch.js +81 -84
- package/cli/message-formatters-watch.ts +189 -0
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +3 -1
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/cluster/generated/protocol-schema.cjs +1 -1
- package/lib/cluster/generated/protocol-schema.mjs +1 -1
- package/lib/cluster/generated/protocol.d.cts +8 -0
- package/lib/cluster/generated/protocol.d.mts +8 -0
- package/lib/cluster/generated/protocol.d.ts +8 -0
- package/lib/clusters-registry.d.ts +23 -0
- package/lib/clusters-registry.js +17 -29
- package/lib/completion.d.ts +12 -0
- package/lib/completion.js +84 -94
- package/lib/compose-utils.d.ts +32 -0
- package/lib/compose-utils.js +57 -72
- package/lib/detached-startup.d.ts +83 -0
- package/lib/detached-startup.js +231 -302
- package/lib/docker-config.d.ts +43 -0
- package/lib/docker-config.js +150 -325
- package/lib/docker-env-auth.d.ts +17 -0
- package/lib/docker-env-auth.js +75 -0
- package/lib/git-remote-utils.d.ts +48 -0
- package/lib/git-remote-utils.js +147 -213
- package/lib/id-detector.d.ts +15 -0
- package/lib/id-detector.js +37 -41
- package/lib/path-check.d.ts +30 -0
- package/lib/path-check.js +42 -45
- package/lib/process-liveness.d.ts +16 -0
- package/lib/process-liveness.js +19 -12
- package/lib/provider-credential-path.d.ts +16 -0
- package/lib/provider-credential-path.js +31 -29
- package/lib/provider-defaults.d.ts +30 -0
- package/lib/provider-defaults.js +47 -42
- package/lib/provider-detection.d.ts +11 -0
- package/lib/provider-detection.js +91 -54
- package/lib/provider-names.d.ts +38 -0
- package/lib/provider-names.js +56 -64
- package/lib/repo-settings.d.ts +28 -0
- package/lib/repo-settings.js +83 -59
- package/lib/run-mode.d.ts +21 -0
- package/lib/run-mode.js +21 -20
- package/lib/run-plan.d.ts +28 -0
- package/lib/run-plan.js +17 -16
- package/lib/settings/claude-auth.d.ts +35 -0
- package/lib/settings/claude-auth.js +30 -47
- package/lib/settings-defaults.d.ts +20 -0
- package/lib/settings-defaults.js +149 -0
- package/lib/settings-error.d.ts +7 -0
- package/lib/settings-error.js +8 -0
- package/lib/settings-issue-providers.d.ts +10 -0
- package/lib/settings-issue-providers.js +12 -0
- package/lib/settings-models.d.ts +22 -0
- package/lib/settings-models.js +93 -0
- package/lib/settings-storage.d.ts +23 -0
- package/lib/settings-storage.js +205 -0
- package/lib/settings-validation.d.ts +7 -0
- package/lib/settings-validation.js +182 -0
- package/lib/settings.d.ts +29 -0
- package/lib/settings.js +41 -736
- package/lib/setup-apply-types.d.ts +66 -0
- package/lib/setup-apply-types.js +2 -0
- package/lib/setup-apply.d.ts +39 -0
- package/lib/setup-apply.js +267 -264
- package/lib/setup-journal.d.ts +41 -0
- package/lib/setup-journal.js +119 -76
- package/lib/setup-plan-types.d.ts +102 -0
- package/lib/setup-plan-types.js +2 -0
- package/lib/setup-plan.d.ts +30 -0
- package/lib/setup-plan.js +300 -324
- package/lib/setup-undo.d.ts +44 -0
- package/lib/setup-undo.js +61 -74
- package/lib/start-cluster-config.d.ts +26 -0
- package/lib/start-cluster-config.js +56 -0
- package/lib/start-cluster-environment.d.ts +40 -0
- package/lib/start-cluster-environment.js +134 -0
- package/lib/start-cluster-run-options.d.ts +53 -0
- package/lib/start-cluster-run-options.js +147 -0
- package/lib/start-cluster.d.ts +64 -0
- package/lib/start-cluster.js +98 -454
- package/lib/stream-json-parser.d.ts +17 -0
- package/lib/stream-json-parser.js +51 -52
- package/npm-shrinkwrap.json +2 -2
- package/package.json +12 -6
- package/scripts/postinstall.js +27 -0
- package/scripts/rust-distribution.js +23 -14
- package/src/agent/agent-command-proofs-context.js +35 -45
- package/src/agent/agent-command-proofs-context.ts +87 -0
- package/src/agent/agent-config-settings.js +19 -0
- package/src/agent/agent-config-settings.ts +41 -0
- package/src/agent/agent-config.js +122 -231
- package/src/agent/agent-config.ts +285 -0
- package/src/agent/agent-context-builder.js +38 -273
- package/src/agent/agent-context-builder.ts +51 -0
- package/src/agent/agent-context-environment-sections.js +105 -0
- package/src/agent/agent-context-environment-sections.ts +134 -0
- package/src/agent/agent-context-packs.js +165 -0
- package/src/agent/agent-context-packs.ts +296 -0
- package/src/agent/agent-context-prompt-sections.js +100 -0
- package/src/agent/agent-context-prompt-sections.ts +148 -0
- package/src/agent/agent-context-sections.js +25 -334
- package/src/agent/agent-context-sections.ts +36 -0
- package/src/agent/agent-context-sources.js +96 -149
- package/src/agent/agent-context-sources.ts +253 -0
- package/src/agent/agent-context-types.js +2 -0
- package/src/agent/agent-context-types.ts +132 -0
- package/src/agent/agent-context-validation-sections.js +73 -0
- package/src/agent/agent-context-validation-sections.ts +129 -0
- package/src/agent/agent-input-injector.js +99 -118
- package/src/agent/agent-input-injector.ts +195 -0
- package/src/agent/agent-liveness-poll.js +74 -78
- package/src/agent/agent-liveness-poll.ts +163 -0
- package/src/agent/agent-quality-gate-schema.js +77 -86
- package/src/agent/agent-quality-gate-schema.ts +135 -0
- package/src/agent/agent-quality-gates-context.js +44 -50
- package/src/agent/agent-quality-gates-context.ts +91 -0
- package/src/agent/agent-stuck-detector.js +171 -239
- package/src/agent/agent-stuck-detector.ts +288 -0
- package/src/agent/agent-trigger-evaluator.js +29 -46
- package/src/agent/agent-trigger-evaluator.ts +98 -0
- package/src/agent/context-metrics.js +110 -141
- package/src/agent/context-metrics.ts +261 -0
- package/src/agent/context-pack-builder.js +72 -358
- package/src/agent/context-pack-builder.ts +105 -0
- package/src/agent/context-pack-core.js +70 -0
- package/src/agent/context-pack-core.ts +96 -0
- package/src/agent/context-pack-selection.js +119 -0
- package/src/agent/context-pack-selection.ts +187 -0
- package/src/agent/context-pack-truncation.js +139 -0
- package/src/agent/context-pack-truncation.ts +208 -0
- package/src/agent/context-pack-types.js +2 -0
- package/src/agent/context-pack-types.ts +98 -0
- package/src/agent/context-replay-policy.js +30 -39
- package/src/agent/context-replay-policy.ts +71 -0
- package/src/agent/critical-agent-policy.js +6 -10
- package/src/agent/critical-agent-policy.ts +20 -0
- package/src/agent/guidance-queue.js +74 -87
- package/src/agent/guidance-queue.ts +166 -0
- package/src/agent/output-extraction-error-detail.js +45 -0
- package/src/agent/output-extraction-error-detail.ts +49 -0
- package/src/agent/output-extraction-failures.js +125 -0
- package/src/agent/output-extraction-failures.ts +149 -0
- package/src/agent/output-extraction-json.js +234 -0
- package/src/agent/output-extraction-json.ts +235 -0
- package/src/agent/output-extraction-pi.js +124 -0
- package/src/agent/output-extraction-pi.ts +137 -0
- package/src/agent/output-extraction-types.js +2 -0
- package/src/agent/output-extraction-types.ts +49 -0
- package/src/agent/output-extraction.js +17 -617
- package/src/agent/output-extraction.ts +30 -0
- package/src/agent/output-reformatter-errors.js +83 -0
- package/src/agent/output-reformatter-errors.ts +106 -0
- package/src/agent/output-reformatter-types.js +2 -0
- package/src/agent/output-reformatter-types.ts +86 -0
- package/src/agent/output-reformatter.js +140 -185
- package/src/agent/output-reformatter.ts +238 -0
- package/src/agent/pi-terminal-lifecycle-types.js +2 -0
- package/src/agent/pi-terminal-lifecycle-types.ts +41 -0
- package/src/agent/pi-terminal-lifecycle.js +186 -206
- package/src/agent/pi-terminal-lifecycle.ts +297 -0
- package/src/agent/provider-control-plane.js +22 -21
- package/src/agent/provider-control-plane.ts +52 -0
- package/src/agent/provider-terminal-failure-types.js +2 -0
- package/src/agent/provider-terminal-failure-types.ts +60 -0
- package/src/agent/provider-terminal-failure.js +194 -191
- package/src/agent/provider-terminal-failure.ts +262 -0
- package/src/agent/rate-limit-backoff.js +31 -74
- package/src/agent/rate-limit-backoff.ts +71 -0
- package/src/agent/schema-utils.js +85 -125
- package/src/agent/schema-utils.ts +148 -0
- package/src/agent/structured-output-error.js +34 -33
- package/src/agent/structured-output-error.ts +105 -0
- package/src/agent/validation-platform.js +27 -27
- package/src/agent/validation-platform.ts +49 -0
- package/src/agent-cli-provider/adapters/codex.ts +3 -1
- package/src/attach/attach-client.js +232 -405
- package/src/attach/attach-client.ts +300 -0
- package/src/attach/attach-server-cleanup.js +43 -0
- package/src/attach/attach-server-cleanup.ts +39 -0
- package/src/attach/attach-server-clients.js +117 -0
- package/src/attach/attach-server-clients.ts +122 -0
- package/src/attach/attach-server-events.js +38 -0
- package/src/attach/attach-server-events.ts +36 -0
- package/src/attach/attach-server-pty.js +45 -0
- package/src/attach/attach-server-pty.ts +64 -0
- package/src/attach/attach-server-runtime.js +73 -0
- package/src/attach/attach-server-runtime.ts +62 -0
- package/src/attach/attach-server-socket.js +28 -0
- package/src/attach/attach-server-socket.ts +23 -0
- package/src/attach/attach-server-types.js +2 -0
- package/src/attach/attach-server-types.ts +82 -0
- package/src/attach/attach-server.js +106 -523
- package/src/attach/attach-server.ts +158 -0
- package/src/attach/index.js +18 -35
- package/src/attach/index.ts +18 -0
- package/src/attach/protocol.js +95 -184
- package/src/attach/protocol.ts +181 -0
- package/src/attach/ring-buffer.js +76 -102
- package/src/attach/ring-buffer.ts +109 -0
- package/src/attach/send-input.js +64 -75
- package/src/attach/send-input.ts +94 -0
- package/src/attach/socket-discovery.js +164 -218
- package/src/attach/socket-discovery.ts +230 -0
- package/src/attach/socket-paths.js +42 -61
- package/src/attach/socket-paths.ts +81 -0
- package/src/claude-credentials.js +51 -54
- package/src/claude-credentials.ts +89 -0
- package/src/cluster/generated/protocol-schema.ts +1 -1
- package/src/cluster/generated/protocol.ts +2 -1
- package/src/command-proofs.js +110 -141
- package/src/command-proofs.ts +216 -0
- package/src/config-router.js +81 -73
- package/src/config-router.ts +131 -0
- package/src/copy-worker.js +53 -29
- package/src/copy-worker.ts +75 -0
- package/src/darwin-keychain-boundary.js +80 -111
- package/src/darwin-keychain-boundary.ts +148 -0
- package/src/guidance-topics.js +4 -5
- package/src/guidance-topics.ts +10 -0
- package/src/input-helpers.js +33 -54
- package/src/input-helpers.ts +58 -0
- package/src/ledger-sequence.js +45 -48
- package/src/ledger-sequence.ts +63 -0
- package/src/legacy-lib/clusters-registry.ts +40 -0
- package/src/legacy-lib/completion.ts +148 -0
- package/src/legacy-lib/compose-utils.ts +102 -0
- package/src/legacy-lib/detached-startup.ts +449 -0
- package/src/legacy-lib/docker-config.ts +282 -0
- package/src/legacy-lib/docker-env-auth.ts +116 -0
- package/src/legacy-lib/git-remote-utils.ts +244 -0
- package/src/legacy-lib/id-detector.ts +57 -0
- package/src/legacy-lib/js-yaml.d.ts +3 -0
- package/src/legacy-lib/omelette.d.ts +15 -0
- package/src/legacy-lib/path-check.ts +89 -0
- package/src/legacy-lib/process-liveness.ts +36 -0
- package/src/legacy-lib/provider-credential-path.ts +57 -0
- package/src/legacy-lib/provider-defaults.ts +114 -0
- package/src/legacy-lib/provider-detection.ts +67 -0
- package/src/legacy-lib/provider-names.ts +126 -0
- package/src/legacy-lib/repo-settings.ts +88 -0
- package/src/legacy-lib/run-mode.ts +58 -0
- package/src/legacy-lib/run-plan.ts +49 -0
- package/src/legacy-lib/settings/claude-auth.ts +80 -0
- package/src/legacy-lib/settings-defaults.ts +212 -0
- package/src/legacy-lib/settings-error.ts +8 -0
- package/src/legacy-lib/settings-issue-providers.ts +18 -0
- package/src/legacy-lib/settings-models.ts +124 -0
- package/src/legacy-lib/settings-storage.ts +277 -0
- package/src/legacy-lib/settings-validation.ts +220 -0
- package/src/legacy-lib/settings.ts +87 -0
- package/src/legacy-lib/setup-apply-types.ts +70 -0
- package/src/legacy-lib/setup-apply.ts +456 -0
- package/src/legacy-lib/setup-journal.ts +185 -0
- package/src/legacy-lib/setup-plan-types.ts +96 -0
- package/src/legacy-lib/setup-plan.ts +500 -0
- package/src/legacy-lib/setup-undo.ts +142 -0
- package/src/legacy-lib/start-cluster-config.ts +124 -0
- package/src/legacy-lib/start-cluster-environment.ts +165 -0
- package/src/legacy-lib/start-cluster-run-options.ts +279 -0
- package/src/legacy-lib/start-cluster.ts +237 -0
- package/src/legacy-lib/stream-json-parser.ts +88 -0
- package/src/message-buffer.js +55 -71
- package/src/message-buffer.ts +107 -0
- package/src/message-bus-bridge.js +92 -133
- package/src/message-bus-bridge.ts +149 -0
- package/src/name-generator-shared.js +220 -0
- package/src/name-generator-shared.ts +231 -0
- package/src/name-generator.js +8 -217
- package/src/name-generator.ts +27 -0
- package/src/omp-blob-root.js +83 -64
- package/src/omp-blob-root.ts +116 -0
- package/src/omp-config-overlay.js +65 -38
- package/src/omp-config-overlay.ts +110 -0
- package/src/omp-execution-fingerprint.js +38 -43
- package/src/omp-execution-fingerprint.ts +92 -0
- package/src/omp-session-limits.js +10 -15
- package/src/omp-session-limits.ts +53 -0
- package/src/omp-session-partition.js +2 -2
- package/src/omp-session-verifier.js +1 -1
- package/src/orchestrator.js +19 -4
- package/src/providers/anthropic/index.js +3 -1
- package/src/providers/anthropic/index.ts +13 -0
- package/src/providers/capabilities.js +19 -27
- package/src/providers/capabilities.ts +44 -0
- package/src/providers/google/index.js +3 -1
- package/src/providers/google/index.ts +13 -0
- package/src/providers/openai/index.js +3 -1
- package/src/providers/openai/index.ts +13 -0
- package/src/providers/opencode/index.js +3 -1
- package/src/providers/opencode/index.ts +13 -0
- package/src/quality-gates.js +67 -127
- package/src/quality-gates.ts +149 -0
- package/src/repo-settings-access.js +40 -0
- package/src/repo-settings-access.ts +64 -0
- package/src/schemas/sub-cluster.js +127 -183
- package/src/schemas/sub-cluster.ts +238 -0
- package/src/state-snapshot-normalization.js +224 -0
- package/src/state-snapshot-normalization.ts +222 -0
- package/src/state-snapshot.js +192 -359
- package/src/state-snapshot.ts +291 -0
- package/src/state-snapshotter.js +118 -130
- package/src/state-snapshotter.ts +167 -0
- package/src/sub-cluster-wrapper.js +4 -13
- package/src/task-run-model-args.js +87 -124
- package/src/task-run-model-args.ts +204 -0
- package/src/task-runner.js +5 -25
- package/src/task-runner.ts +28 -0
- package/src/task-startup-error.js +73 -58
- package/src/task-startup-error.ts +126 -0
- package/src/template-validation/consensus-gate-contracts.js +2 -0
- package/src/template-validation/consensus-gate-contracts.ts +70 -0
- package/src/template-validation/consensus-gate-scenarios.js +76 -0
- package/src/template-validation/consensus-gate-scenarios.ts +130 -0
- package/src/template-validation/consensus-gate-stage.js +34 -0
- package/src/template-validation/consensus-gate-stage.ts +56 -0
- package/src/template-validation/random-topology-contracts.js +2 -0
- package/src/template-validation/random-topology-contracts.ts +54 -0
- package/src/template-validation/random-topology-dispatch.js +25 -0
- package/src/template-validation/random-topology-dispatch.ts +33 -0
- package/src/template-validation/random-topology-message-loop.js +53 -0
- package/src/template-validation/random-topology-message-loop.ts +73 -0
- package/src/template-validation/random-topology-operations.js +79 -0
- package/src/template-validation/random-topology-operations.ts +101 -0
- package/src/template-validation/random-topology-runtime.js +25 -0
- package/src/template-validation/random-topology-runtime.ts +43 -0
- package/src/template-validation/random-topology-scenario.js +52 -0
- package/src/template-validation/random-topology-scenario.ts +57 -0
- package/src/template-validation/random-topology-schema.js +159 -0
- package/src/template-validation/random-topology-schema.ts +190 -0
- package/src/template-validation/random-topology-state.js +28 -0
- package/src/template-validation/random-topology-state.ts +30 -0
- package/src/template-validation/random-topology-trigger-evaluation.js +45 -0
- package/src/template-validation/random-topology-trigger-evaluation.ts +65 -0
- package/src/template-validation/random-topology-trigger.js +75 -0
- package/src/template-validation/random-topology-trigger.ts +88 -0
- package/src/template-validation/report-formatter.js +40 -46
- package/src/template-validation/report-formatter.ts +101 -0
- package/src/template-validation/simulate-consensus-gates.js +111 -302
- package/src/template-validation/simulate-consensus-gates.ts +196 -0
- package/src/template-validation/simulate-random-topology.js +26 -519
- package/src/template-validation/simulate-random-topology.ts +57 -0
- package/src/template-validation/simulate-two-stage-validation.js +41 -265
- package/src/template-validation/simulate-two-stage-validation.ts +55 -0
- package/src/template-validation/simulation-agent-runtime.js +25 -0
- package/src/template-validation/simulation-agent-runtime.ts +40 -0
- package/src/template-validation/simulation-agent.js +29 -0
- package/src/template-validation/simulation-agent.ts +73 -0
- package/src/template-validation/simulation-runtime.js +34 -0
- package/src/template-validation/simulation-runtime.ts +72 -0
- package/src/template-validation/two-stage-contracts.js +2 -0
- package/src/template-validation/two-stage-contracts.ts +41 -0
- package/src/template-validation/two-stage-inputs.js +21 -0
- package/src/template-validation/two-stage-inputs.ts +29 -0
- package/src/template-validation/two-stage-results.js +30 -0
- package/src/template-validation/two-stage-results.ts +40 -0
- package/src/template-validation/two-stage-scenario.js +86 -0
- package/src/template-validation/two-stage-scenario.ts +120 -0
- package/src/worktree-claude-config.js +182 -220
- package/src/worktree-claude-config.ts +272 -0
- package/src/worktree-tooling-env.js +103 -125
- package/src/worktree-tooling-env.ts +142 -0
- package/task-lib/completion.js +2 -3
- package/task-lib/completion.ts +9 -0
- package/task-lib/config.js +1 -4
- package/task-lib/config.ts +11 -0
- package/task-lib/name-generator.js +4 -226
- package/task-lib/name-generator.ts +11 -0
- package/task-lib/omp-session-cleanup.js +1 -1
- package/task-lib/omp-storage-root.js +8 -12
- package/task-lib/omp-storage-root.ts +62 -0
- package/task-lib/process-termination.js +143 -166
- package/task-lib/process-termination.ts +268 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zeroshot setup apply` — writes decisions from #A's plan/decision contract
|
|
3
|
+
* (lib/setup-plan.js) to global settings, repo-local settings, and the undo
|
|
4
|
+
* journal (lib/setup-journal.js).
|
|
5
|
+
*
|
|
6
|
+
* Fail-closed: every input decisionId + value is resolved and validated
|
|
7
|
+
* against its domain before ANY write happens. Writes are then applied only
|
|
8
|
+
* to settings keys a run-mode resolver actually reads (setup-plan.js's
|
|
9
|
+
* CONSUMED_PATHS/isConsumedPath — the same set that filters proposedWrites)
|
|
10
|
+
* — a settings key nobody reads is dead config, the exact drift the
|
|
11
|
+
* canonical-path rule (issue #605) forbids.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ApplyDecisionValuesParams,
|
|
16
|
+
ApplyDecisionsParams,
|
|
17
|
+
ApplyDependencies,
|
|
18
|
+
DecisionResult,
|
|
19
|
+
PendingWrite,
|
|
20
|
+
ResolvedDecision,
|
|
21
|
+
SettingsMap,
|
|
22
|
+
SetupJournal,
|
|
23
|
+
} from './setup-apply-types';
|
|
24
|
+
|
|
25
|
+
interface SetupPlanFacade {
|
|
26
|
+
CONSUMED_PATHS: Set<string>;
|
|
27
|
+
domainFor(decisionId: string): string;
|
|
28
|
+
isConsumedPath(scope: string, targetPath: string): boolean;
|
|
29
|
+
resolveDecisionPath(decisionId: string): { path: string; scope: 'global' | 'repo' } | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DockerConfigFacade {
|
|
33
|
+
validateEnvPassthrough(value: unknown): string | null;
|
|
34
|
+
validateMountConfig(value: unknown): string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ProviderNamesFacade {
|
|
38
|
+
VALID_PROVIDERS: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FileSystemFacade {
|
|
42
|
+
readFileSync(filePath: string, encoding: 'utf8'): string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SetupJournalFacade {
|
|
46
|
+
deepEqual(left: unknown, right: unknown): boolean;
|
|
47
|
+
getNestedValue(source: unknown, pathStr: string): unknown;
|
|
48
|
+
loadJournal(): SetupJournal;
|
|
49
|
+
saveJournal(journal: SetupJournal): void;
|
|
50
|
+
setNestedValue(target: SettingsMap, pathStr: string, value: unknown): void;
|
|
51
|
+
upsertJournalEntry(journal: SetupJournal, entry: SetupJournal['entries'][number]): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ConvertDecisionParams {
|
|
55
|
+
decisionId: string;
|
|
56
|
+
deps: ApplyDependencies;
|
|
57
|
+
globalSettings: SettingsMap;
|
|
58
|
+
value: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ResolveDecisionContext {
|
|
62
|
+
allowRiskyDefaults: boolean;
|
|
63
|
+
globalSettings: SettingsMap;
|
|
64
|
+
repoSettings: SettingsMap;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ApplyWriteContext {
|
|
68
|
+
deps: ApplyDependencies;
|
|
69
|
+
journal: SetupJournal;
|
|
70
|
+
repoRoot: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface WriteResolvedContext extends ApplyWriteContext {
|
|
74
|
+
allowRiskyDefaults: boolean;
|
|
75
|
+
repoSettings: SettingsMap;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// These CommonJS paths intentionally resolve beside the emitted module in lib/.
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
80
|
+
const { resolveDecisionPath, domainFor, isConsumedPath, CONSUMED_PATHS }: SetupPlanFacade =
|
|
81
|
+
require('./setup-plan');
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
83
|
+
const { validateMountConfig, validateEnvPassthrough }: DockerConfigFacade =
|
|
84
|
+
require('./docker-config');
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
86
|
+
const { VALID_PROVIDERS }: ProviderNamesFacade = require('./provider-names');
|
|
87
|
+
const VALID_LEVELS = Object.freeze(['level1', 'level2', 'level3']);
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
89
|
+
const setupJournal: SetupJournalFacade = require('./setup-journal');
|
|
90
|
+
const { loadJournal, saveJournal, upsertJournalEntry } = setupJournal;
|
|
91
|
+
const { getNestedValue, setNestedValue, deepEqual } = setupJournal;
|
|
92
|
+
|
|
93
|
+
const SECRET_PATTERN = /token|secret|password|api[_-]?key|credential/i;
|
|
94
|
+
|
|
95
|
+
function includesValue(values: readonly unknown[], value: unknown): boolean {
|
|
96
|
+
return values.includes(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function assertSecretSafePath(targetPath: string): void {
|
|
100
|
+
if (SECRET_PATTERN.test(targetPath)) {
|
|
101
|
+
throw new Error(`Refusing to write secret-shaped settings path: ${targetPath}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function domainError(decisionId: string, value: unknown): Error {
|
|
106
|
+
const expected = domainFor(decisionId);
|
|
107
|
+
const received = JSON.stringify(value);
|
|
108
|
+
return new Error(
|
|
109
|
+
`Invalid value for decision "${decisionId}": expected ${expected}, got ${received}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function convertProviderLevelDecision({
|
|
114
|
+
decisionId,
|
|
115
|
+
value,
|
|
116
|
+
globalSettings,
|
|
117
|
+
deps,
|
|
118
|
+
}: ConvertDecisionParams): SettingsMap {
|
|
119
|
+
if (!decisionId.startsWith('providerLevel.')) {
|
|
120
|
+
throw new Error(`Unknown decision ID: ${decisionId}`);
|
|
121
|
+
}
|
|
122
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
123
|
+
throw domainError(decisionId, value);
|
|
124
|
+
}
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
126
|
+
const decisionValue = value as SettingsMap;
|
|
127
|
+
// Preserve Array.prototype.sort's default UTF-16 ordering.
|
|
128
|
+
// eslint-disable-next-line sonarjs/no-alphabetical-sort
|
|
129
|
+
const keys = Object.keys(decisionValue).sort();
|
|
130
|
+
const expectedKeys = ['defaultLevel', 'maxLevel', 'minLevel'];
|
|
131
|
+
if (
|
|
132
|
+
keys.length !== expectedKeys.length ||
|
|
133
|
+
keys.some((key, index) => key !== expectedKeys[index]) ||
|
|
134
|
+
expectedKeys.some((key) => !includesValue(VALID_LEVELS, decisionValue[key]))
|
|
135
|
+
) {
|
|
136
|
+
throw domainError(decisionId, value);
|
|
137
|
+
}
|
|
138
|
+
const providerName = decisionId.slice('providerLevel.'.length);
|
|
139
|
+
if (!deps.VALID_PROVIDERS.includes(providerName)) throw domainError(decisionId, value);
|
|
140
|
+
const rank = (level: unknown): number => VALID_LEVELS.findIndex((item) => item === level);
|
|
141
|
+
if (
|
|
142
|
+
rank(decisionValue.minLevel) > rank(decisionValue.defaultLevel) ||
|
|
143
|
+
rank(decisionValue.defaultLevel) > rank(decisionValue.maxLevel)
|
|
144
|
+
) {
|
|
145
|
+
throw domainError(decisionId, value);
|
|
146
|
+
}
|
|
147
|
+
const existingValue = getNestedValue(globalSettings, `providerSettings.${providerName}`) || {};
|
|
148
|
+
// Both operands have passed the same object-domain checks used by the legacy implementation.
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
150
|
+
const existing = existingValue as SettingsMap;
|
|
151
|
+
return { ...existing, ...decisionValue };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Resolve a submitted decision into the exact value stored at its canonical
|
|
155
|
+
// settings path, validating its decision domain before any write occurs.
|
|
156
|
+
function convertDecisionValue({
|
|
157
|
+
decisionId,
|
|
158
|
+
value,
|
|
159
|
+
globalSettings,
|
|
160
|
+
deps,
|
|
161
|
+
}: ConvertDecisionParams): unknown {
|
|
162
|
+
switch (decisionId) {
|
|
163
|
+
case 'defaultProvider':
|
|
164
|
+
if (typeof value !== 'string' || !deps.VALID_PROVIDERS.includes(value)) {
|
|
165
|
+
throw domainError(decisionId, value);
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
|
|
169
|
+
case 'defaultIsolation':
|
|
170
|
+
if (!includesValue(['worktree', 'docker', 'none'], value)) {
|
|
171
|
+
throw domainError(decisionId, value);
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
|
|
175
|
+
case 'allowLocalNoIsolation':
|
|
176
|
+
if (typeof value !== 'boolean') throw domainError(decisionId, value);
|
|
177
|
+
return value;
|
|
178
|
+
|
|
179
|
+
case 'defaultDelivery':
|
|
180
|
+
if (!includesValue(['none', 'pr', 'ship'], value)) throw domainError(decisionId, value);
|
|
181
|
+
return value;
|
|
182
|
+
|
|
183
|
+
case 'defaultIssueSource':
|
|
184
|
+
if (typeof value !== 'string' || !deps.listIssueProviders().includes(value)) {
|
|
185
|
+
throw domainError(decisionId, value);
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
|
|
189
|
+
case 'prBase':
|
|
190
|
+
if (typeof value !== 'string' || value.trim() === '') throw domainError(decisionId, value);
|
|
191
|
+
return value;
|
|
192
|
+
|
|
193
|
+
case 'dockerMounts': {
|
|
194
|
+
const err = validateMountConfig(value);
|
|
195
|
+
if (err) throw new Error(`Invalid value for decision "${decisionId}": ${err}`);
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'dockerEnvPassthrough': {
|
|
200
|
+
const err = validateEnvPassthrough(value);
|
|
201
|
+
if (err) throw new Error(`Invalid value for decision "${decisionId}": ${err}`);
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'updatePolicy':
|
|
206
|
+
if (!includesValue(['off', 'notify', 'auto'], value)) throw domainError(decisionId, value);
|
|
207
|
+
return value;
|
|
208
|
+
|
|
209
|
+
default:
|
|
210
|
+
return convertProviderLevelDecision({ decisionId, value, globalSettings, deps });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function defaultApplyDeps(): ApplyDependencies {
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
216
|
+
const fs: FileSystemFacade = require('fs');
|
|
217
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
218
|
+
const { loadSettings, mutateSettings }: Pick<
|
|
219
|
+
ApplyDependencies,
|
|
220
|
+
'loadSettings' | 'mutateSettings'
|
|
221
|
+
> = require('./settings');
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
223
|
+
const { readRepoSettings, writeRepoSettings }: Pick<
|
|
224
|
+
ApplyDependencies,
|
|
225
|
+
'readRepoSettings' | 'writeRepoSettings'
|
|
226
|
+
> = require('./repo-settings');
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
228
|
+
const { checkGhAuth }: Pick<ApplyDependencies, 'checkGhAuth'> = require('../src/preflight');
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
230
|
+
const { listProviders: listIssueProviders }: {
|
|
231
|
+
listProviders: ApplyDependencies['listIssueProviders'];
|
|
232
|
+
} = require('../src/issue-providers');
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
readFile: (filePath: string) => fs.readFileSync(filePath, 'utf8'),
|
|
236
|
+
loadSettings,
|
|
237
|
+
mutateSettings,
|
|
238
|
+
readRepoSettings,
|
|
239
|
+
writeRepoSettings,
|
|
240
|
+
checkGhAuth,
|
|
241
|
+
listIssueProviders,
|
|
242
|
+
VALID_PROVIDERS,
|
|
243
|
+
now: () => new Date().toISOString(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Phase 1: resolve + validate EVERY input decision before any write happens
|
|
248
|
+
// (fail-closed — a single bad decisionId or out-of-domain value rejects the
|
|
249
|
+
// whole request, never a partial apply).
|
|
250
|
+
function resolveAndValidateDecisions(
|
|
251
|
+
input: SettingsMap,
|
|
252
|
+
globalSettings: SettingsMap,
|
|
253
|
+
deps: ApplyDependencies
|
|
254
|
+
): ResolvedDecision[] {
|
|
255
|
+
const resolved: ResolvedDecision[] = [];
|
|
256
|
+
for (const [decisionId, value] of Object.entries(input)) {
|
|
257
|
+
const target = resolveDecisionPath(decisionId);
|
|
258
|
+
if (!target) {
|
|
259
|
+
throw new Error(`Unknown decision ID: ${decisionId}`);
|
|
260
|
+
}
|
|
261
|
+
assertSecretSafePath(target.path);
|
|
262
|
+
const writeValue = convertDecisionValue({ decisionId, value, globalSettings, deps });
|
|
263
|
+
resolved.push({ decisionId, target, inputValue: value, writeValue });
|
|
264
|
+
}
|
|
265
|
+
return resolved;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Decides the outcome for a single resolved decision: a skip (with reason)
|
|
269
|
+
// or the write to perform. Returns the write descriptor rather than mutating
|
|
270
|
+
// anything, so the caller controls when settings objects/journal are touched.
|
|
271
|
+
function resolveDecisionOutcome(
|
|
272
|
+
decision: ResolvedDecision,
|
|
273
|
+
context: ResolveDecisionContext
|
|
274
|
+
): { result: DecisionResult; write?: PendingWrite } {
|
|
275
|
+
const { decisionId, target, inputValue, writeValue } = decision;
|
|
276
|
+
const { globalSettings, repoSettings, allowRiskyDefaults } = context;
|
|
277
|
+
const settingsObj = target.scope === 'repo' ? repoSettings : globalSettings;
|
|
278
|
+
const currentValue = getNestedValue(settingsObj, target.path) ?? null;
|
|
279
|
+
const base = { decisionId, from: currentValue, to: writeValue };
|
|
280
|
+
|
|
281
|
+
if (decisionId === 'defaultDelivery' && inputValue === 'ship' && !allowRiskyDefaults) {
|
|
282
|
+
return { result: { ...base, applied: false, skippedReason: 'requires-explicit-opt-in' } };
|
|
283
|
+
}
|
|
284
|
+
if (!isConsumedPath(target.scope, target.path)) {
|
|
285
|
+
return { result: { ...base, applied: false, skippedReason: 'no-consumer' } };
|
|
286
|
+
}
|
|
287
|
+
if (deepEqual(currentValue, writeValue)) {
|
|
288
|
+
return { result: { ...base, applied: false, skippedReason: 'unchanged' } };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
result: { ...base, applied: true },
|
|
293
|
+
write: {
|
|
294
|
+
settingsObj,
|
|
295
|
+
scope: target.scope,
|
|
296
|
+
path: target.path,
|
|
297
|
+
writeValue,
|
|
298
|
+
priorValue: currentValue,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Applies one decision's write to its in-memory settings object and journals
|
|
304
|
+
// it (deferred persistence — the caller flushes to disk once at the end).
|
|
305
|
+
function applyWrite(
|
|
306
|
+
write: PendingWrite,
|
|
307
|
+
context: ApplyWriteContext
|
|
308
|
+
): 'global' | 'repo' {
|
|
309
|
+
const { repoRoot, journal, deps } = context;
|
|
310
|
+
setNestedValue(write.settingsObj, write.path, write.writeValue);
|
|
311
|
+
upsertJournalEntry(journal, {
|
|
312
|
+
scope: write.scope,
|
|
313
|
+
path: write.path,
|
|
314
|
+
repoRoot: write.scope === 'repo' ? repoRoot : null,
|
|
315
|
+
priorValue: write.priorValue,
|
|
316
|
+
appliedValue: write.writeValue,
|
|
317
|
+
appliedAt: deps.now(),
|
|
318
|
+
});
|
|
319
|
+
return write.scope;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Phase 2: resolve each global outcome against the locked, freshly read state.
|
|
323
|
+
// Repo-local settings remain intentionally outside the global settings lock.
|
|
324
|
+
function writeResolvedDecisions(
|
|
325
|
+
resolved: ResolvedDecision[],
|
|
326
|
+
context: WriteResolvedContext
|
|
327
|
+
): DecisionResult[] {
|
|
328
|
+
const { repoSettings, repoRoot, journal, allowRiskyDefaults, deps } = context;
|
|
329
|
+
const resultsById = new Map<string, DecisionResult>();
|
|
330
|
+
let repoDirty = false;
|
|
331
|
+
let globalDirty = false;
|
|
332
|
+
const globalDecisions = resolved.filter((decision) => decision.target.scope === 'global');
|
|
333
|
+
|
|
334
|
+
if (globalDecisions.length > 0) {
|
|
335
|
+
const globalResults = deps.mutateSettings((freshGlobalSettings) => {
|
|
336
|
+
const results: DecisionResult[] = [];
|
|
337
|
+
for (const decision of globalDecisions) {
|
|
338
|
+
const freshDecision = {
|
|
339
|
+
...decision,
|
|
340
|
+
writeValue: convertDecisionValue({
|
|
341
|
+
decisionId: decision.decisionId,
|
|
342
|
+
value: decision.inputValue,
|
|
343
|
+
globalSettings: freshGlobalSettings,
|
|
344
|
+
deps,
|
|
345
|
+
}),
|
|
346
|
+
};
|
|
347
|
+
const { result, write } = resolveDecisionOutcome(freshDecision, {
|
|
348
|
+
globalSettings: freshGlobalSettings,
|
|
349
|
+
repoSettings,
|
|
350
|
+
allowRiskyDefaults,
|
|
351
|
+
});
|
|
352
|
+
results.push(result);
|
|
353
|
+
if (write) {
|
|
354
|
+
applyWrite(write, { repoRoot, journal, deps });
|
|
355
|
+
globalDirty = true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return results;
|
|
359
|
+
});
|
|
360
|
+
for (const result of globalResults) resultsById.set(result.decisionId, result);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const decision of resolved.filter((item) => item.target.scope === 'repo')) {
|
|
364
|
+
const { result, write } = resolveDecisionOutcome(decision, {
|
|
365
|
+
globalSettings: {},
|
|
366
|
+
repoSettings,
|
|
367
|
+
allowRiskyDefaults,
|
|
368
|
+
});
|
|
369
|
+
resultsById.set(result.decisionId, result);
|
|
370
|
+
if (!write) continue;
|
|
371
|
+
applyWrite(write, { repoRoot, journal, deps });
|
|
372
|
+
repoDirty = true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (repoDirty) deps.writeRepoSettings(repoRoot, repoSettings);
|
|
376
|
+
if (globalDirty || repoDirty) saveJournal(journal);
|
|
377
|
+
return resolved.map((decision) => {
|
|
378
|
+
const result = resultsById.get(decision.decisionId);
|
|
379
|
+
if (!result) throw new Error(`Missing apply result for decision: ${decision.decisionId}`);
|
|
380
|
+
return result;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function assertDecisionObject(decisions: unknown, label: string): asserts decisions is SettingsMap {
|
|
385
|
+
if (!decisions || typeof decisions !== 'object' || Array.isArray(decisions)) {
|
|
386
|
+
throw new Error(`${label} must be a JSON object of { decisionId: value }`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Apply an in-memory decision object without temporary files. */
|
|
391
|
+
function applyDecisionValues({
|
|
392
|
+
decisions,
|
|
393
|
+
cwd,
|
|
394
|
+
allowRiskyDefaults = false,
|
|
395
|
+
deps = {},
|
|
396
|
+
}: ApplyDecisionValuesParams): DecisionResult[] {
|
|
397
|
+
assertDecisionObject(decisions, 'Decisions');
|
|
398
|
+
const resolvedDeps: ApplyDependencies = { ...defaultApplyDeps(), ...deps };
|
|
399
|
+
const globalSettings = resolvedDeps.loadSettings();
|
|
400
|
+
const { repoRoot, settings: repoSettingsRaw } = resolvedDeps.readRepoSettings(cwd);
|
|
401
|
+
const repoSettings = repoSettingsRaw || {};
|
|
402
|
+
const resolved = resolveAndValidateDecisions(decisions, globalSettings, resolvedDeps);
|
|
403
|
+
const results = writeResolvedDecisions(resolved, {
|
|
404
|
+
repoSettings,
|
|
405
|
+
repoRoot,
|
|
406
|
+
journal: loadJournal(),
|
|
407
|
+
allowRiskyDefaults,
|
|
408
|
+
deps: resolvedDeps,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const issueSourceApplied = results.find(
|
|
412
|
+
(result) => result.decisionId === 'defaultIssueSource' && result.applied
|
|
413
|
+
);
|
|
414
|
+
if (issueSourceApplied?.to === 'github') {
|
|
415
|
+
const auth = resolvedDeps.checkGhAuth();
|
|
416
|
+
if (!auth?.authenticated) console.log('Run: gh auth login');
|
|
417
|
+
}
|
|
418
|
+
return results;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Apply a decisions JSON file through the shared object API. */
|
|
422
|
+
function applyDecisions({
|
|
423
|
+
decisionsPath,
|
|
424
|
+
cwd,
|
|
425
|
+
allowRiskyDefaults = false,
|
|
426
|
+
deps = {},
|
|
427
|
+
}: ApplyDecisionsParams): DecisionResult[] {
|
|
428
|
+
const resolvedDeps: ApplyDependencies = { ...defaultApplyDeps(), ...deps };
|
|
429
|
+
let decisions: unknown;
|
|
430
|
+
try {
|
|
431
|
+
decisions = JSON.parse(resolvedDeps.readFile(decisionsPath));
|
|
432
|
+
} catch (error) {
|
|
433
|
+
const message =
|
|
434
|
+
typeof error === 'object' && error !== null && 'message' in error
|
|
435
|
+
? error.message
|
|
436
|
+
: undefined;
|
|
437
|
+
throw new Error(`Failed to read decisions file "${decisionsPath}": ${String(message)}`);
|
|
438
|
+
}
|
|
439
|
+
assertDecisionObject(decisions, 'Decisions file');
|
|
440
|
+
return applyDecisionValues({
|
|
441
|
+
decisions,
|
|
442
|
+
cwd,
|
|
443
|
+
allowRiskyDefaults,
|
|
444
|
+
deps: resolvedDeps,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export = {
|
|
449
|
+
applyDecisions,
|
|
450
|
+
applyDecisionValues,
|
|
451
|
+
resolveAndValidateDecisions,
|
|
452
|
+
writeResolvedDecisions,
|
|
453
|
+
assertSecretSafePath,
|
|
454
|
+
isConsumedPath,
|
|
455
|
+
CONSUMED_PATHS,
|
|
456
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo journal for `zeroshot setup apply` / `zeroshot setup undo`.
|
|
3
|
+
*
|
|
4
|
+
* One journal entry per setting this setup owns: { scope, path, repoRoot,
|
|
5
|
+
* priorValue, appliedValue, appliedAt }. `priorValue: null` means the key
|
|
6
|
+
* did not exist before apply, so undo deletes it rather than restoring it.
|
|
7
|
+
* Shared by lib/setup-apply.js (writer) and lib/setup-undo.js (reader) so the
|
|
8
|
+
* nested-path mutation and equality semantics can't drift between the two.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs = require('fs');
|
|
12
|
+
import path = require('path');
|
|
13
|
+
|
|
14
|
+
interface SettingsFacade {
|
|
15
|
+
getSettingsFile(): string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SetupPlanFacade {
|
|
19
|
+
getNestedValue(source: Record<string, unknown>, pathStr: string): unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface JournalEntry {
|
|
23
|
+
readonly appliedAt: string;
|
|
24
|
+
readonly appliedValue: unknown;
|
|
25
|
+
readonly path: string;
|
|
26
|
+
readonly priorValue: unknown;
|
|
27
|
+
readonly repoRoot: string | null;
|
|
28
|
+
readonly scope: string;
|
|
29
|
+
readonly [key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SetupJournal {
|
|
33
|
+
entries: JournalEntry[];
|
|
34
|
+
version: number;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// These CommonJS paths intentionally resolve beside the emitted module in lib/.
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
40
|
+
const { getSettingsFile }: SettingsFacade = require('./settings');
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
42
|
+
const { getNestedValue }: SetupPlanFacade = require('./setup-plan');
|
|
43
|
+
|
|
44
|
+
function getJournalPath(): string {
|
|
45
|
+
return path.join(path.dirname(getSettingsFile()), 'setup-undo-journal.json');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
49
|
+
return typeof value === 'object' && value !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isSetupJournal(value: unknown): value is SetupJournal {
|
|
53
|
+
return isRecord(value) && Array.isArray(value.entries);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function emptyJournal(): SetupJournal {
|
|
57
|
+
return { version: 1, entries: [] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadJournal(): SetupJournal {
|
|
61
|
+
const journalPath = getJournalPath();
|
|
62
|
+
if (!fs.existsSync(journalPath)) {
|
|
63
|
+
return emptyJournal();
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(journalPath, 'utf8'));
|
|
67
|
+
if (!isSetupJournal(parsed)) {
|
|
68
|
+
return emptyJournal();
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
} catch {
|
|
72
|
+
return emptyJournal();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function saveJournal(journal: SetupJournal): void {
|
|
77
|
+
const journalPath = getJournalPath();
|
|
78
|
+
fs.mkdirSync(path.dirname(journalPath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(journalPath, JSON.stringify(journal, null, 2), 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function entryKey(entry: JournalEntry): string {
|
|
83
|
+
return `${entry.scope}:${entry.repoRoot || ''}:${entry.path}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Re-applying an already-journaled write updates appliedValue/appliedAt but
|
|
87
|
+
// keeps the original priorValue — that's the true pre-apply state undo must
|
|
88
|
+
// restore to, and it must not drift across repeated `apply` runs.
|
|
89
|
+
function upsertJournalEntry(journal: SetupJournal, entry: JournalEntry): void {
|
|
90
|
+
const key = entryKey(entry);
|
|
91
|
+
const existingIndex = journal.entries.findIndex((candidate) => entryKey(candidate) === key);
|
|
92
|
+
if (existingIndex === -1) {
|
|
93
|
+
journal.entries.push(entry);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const existing = journal.entries[existingIndex];
|
|
97
|
+
if (!existing) {
|
|
98
|
+
throw new Error('Journal entry disappeared during synchronous update');
|
|
99
|
+
}
|
|
100
|
+
journal.entries[existingIndex] = {
|
|
101
|
+
...entry,
|
|
102
|
+
priorValue: existing.priorValue,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function safePathSegments(pathStr: string): string[] {
|
|
107
|
+
const keys = pathStr.split('.');
|
|
108
|
+
for (const key of keys) {
|
|
109
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
110
|
+
throw new Error(`Unsafe setup journal path: ${pathStr}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return keys;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function defineOwnValue(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
117
|
+
Object.defineProperty(target, key, {
|
|
118
|
+
configurable: true,
|
|
119
|
+
enumerable: true,
|
|
120
|
+
value,
|
|
121
|
+
writable: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function setNestedValue(target: Record<string, unknown>, pathStr: string, value: unknown): void {
|
|
126
|
+
const keys = safePathSegments(pathStr);
|
|
127
|
+
let node = target;
|
|
128
|
+
for (let index = 0; index < keys.length - 1; index++) {
|
|
129
|
+
const key = keys[index] || '';
|
|
130
|
+
const child = Object.hasOwn(node, key) ? node[key] : undefined;
|
|
131
|
+
if (isRecord(child)) {
|
|
132
|
+
node = child;
|
|
133
|
+
} else {
|
|
134
|
+
const replacement: Record<string, unknown> = {};
|
|
135
|
+
defineOwnValue(node, key, replacement);
|
|
136
|
+
node = replacement;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
defineOwnValue(node, keys[keys.length - 1] || '', value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function deleteNestedKey(target: Record<string, unknown>, pathStr: string): void {
|
|
143
|
+
const keys = safePathSegments(pathStr);
|
|
144
|
+
let node = target;
|
|
145
|
+
for (let index = 0; index < keys.length - 1; index++) {
|
|
146
|
+
const key = keys[index] || '';
|
|
147
|
+
if (!Object.hasOwn(node, key)) return;
|
|
148
|
+
const child = node[key];
|
|
149
|
+
if (!isRecord(child)) return;
|
|
150
|
+
node = child;
|
|
151
|
+
}
|
|
152
|
+
const finalKey = keys[keys.length - 1] || '';
|
|
153
|
+
if (Object.hasOwn(node, finalKey)) {
|
|
154
|
+
delete node[finalKey];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function deepEqual(left: unknown, right: unknown): boolean {
|
|
159
|
+
if (left === right) return true;
|
|
160
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
161
|
+
if (Array.isArray(left) !== Array.isArray(right)) return false;
|
|
162
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
163
|
+
if (left.length !== right.length) return false;
|
|
164
|
+
return left.every((value, index) => deepEqual(value, right[index]));
|
|
165
|
+
}
|
|
166
|
+
const leftKeys = Object.keys(left);
|
|
167
|
+
const rightKeys = Object.keys(right);
|
|
168
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
169
|
+
return leftKeys.every(
|
|
170
|
+
(key) =>
|
|
171
|
+
Object.prototype.hasOwnProperty.call(right, key) &&
|
|
172
|
+
deepEqual(Reflect.get(left, key), Reflect.get(right, key))
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export = {
|
|
177
|
+
getJournalPath,
|
|
178
|
+
loadJournal,
|
|
179
|
+
saveJournal,
|
|
180
|
+
upsertJournalEntry,
|
|
181
|
+
getNestedValue,
|
|
182
|
+
setNestedValue,
|
|
183
|
+
deleteNestedKey,
|
|
184
|
+
deepEqual,
|
|
185
|
+
};
|