humanish 0.107.0 → 0.109.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/ARCHITECTURE.md +73 -69
- package/CHANGELOG.md +27 -0
- package/CONTEXT.md +51 -44
- package/README.md +148 -175
- package/SECURITY.md +16 -26
- package/TELEMETRY.md +8 -6
- package/dist/actors/affordance.d.ts +4 -4
- package/dist/actors/affordance.js +12 -12
- package/dist/actors/agent-session.d.ts +2 -2
- package/dist/actors/agent-session.js +7 -7
- package/dist/actors/codex/app-server-actor-trace.d.ts +1 -1
- package/dist/actors/codex/app-server-actor-trace.js +1 -1
- package/dist/actors/codex/app-server-trace.js +2 -1
- package/dist/actors/codex/app-server-ui.js +1 -1
- package/dist/actors/codex/codex-admission.d.ts +91 -0
- package/dist/actors/codex/codex-admission.js +150 -0
- package/dist/actors/codex/restricted-executable.d.ts +4 -4
- package/dist/actors/codex/restricted-executable.js +8 -8
- package/dist/actors/codex/restricted-launch.d.ts +2 -1
- package/dist/actors/codex/restricted-launch.js +1 -1
- package/dist/actors/codex/restricted-participant.js +20 -18
- package/dist/actors/codex/restricted-policy.d.ts +2 -2
- package/dist/actors/codex/restricted-policy.js +2 -2
- package/dist/actors/codex/restricted-session.d.ts +3 -3
- package/dist/actors/codex/restricted-session.js +10 -5
- package/dist/actors/computer-use/actor.d.ts +18 -18
- package/dist/actors/computer-use/actor.js +4 -4
- package/dist/actors/computer-use/admission-limit.d.ts +3 -3
- package/dist/actors/computer-use/admission-limit.js +4 -4
- package/dist/actors/computer-use/executor-error.d.ts +2 -2
- package/dist/actors/computer-use/executor-error.js +3 -3
- package/dist/actors/computer-use/loop/actions.d.ts +3 -3
- package/dist/actors/computer-use/loop/actions.js +15 -15
- package/dist/actors/computer-use/loop/backstop.d.ts +3 -3
- package/dist/actors/computer-use/loop/backstop.js +14 -12
- package/dist/actors/computer-use/loop/debrief.js +2 -2
- package/dist/actors/computer-use/loop/ending.d.ts +6 -6
- package/dist/actors/computer-use/loop/ending.js +18 -17
- package/dist/actors/computer-use/loop/observation.d.ts +2 -2
- package/dist/actors/computer-use/loop/observation.js +15 -14
- package/dist/actors/computer-use/loop/provider-call.d.ts +1 -1
- package/dist/actors/computer-use/loop/provider-call.js +12 -12
- package/dist/actors/computer-use/loop/session.d.ts +4 -4
- package/dist/actors/computer-use/loop/session.js +4 -4
- package/dist/actors/computer-use/loop/spend.d.ts +1 -1
- package/dist/actors/computer-use/loop/spend.js +1 -1
- package/dist/actors/computer-use/loop/trace.js +11 -9
- package/dist/actors/computer-use/loop/types.d.ts +47 -45
- package/dist/actors/computer-use/loop/usage.d.ts +2 -2
- package/dist/actors/computer-use/loop/usage.js +2 -2
- package/dist/actors/computer-use/loop.d.ts +6 -1
- package/dist/actors/computer-use/loop.js +26 -4
- package/dist/actors/computer-use/openai-context.d.ts +37 -0
- package/dist/actors/computer-use/openai-context.js +214 -0
- package/dist/actors/computer-use/openai-provider.d.ts +13 -10
- package/dist/actors/computer-use/openai-provider.js +52 -41
- package/dist/actors/computer-use/openai-requests.d.ts +36 -10
- package/dist/actors/computer-use/openai-requests.js +82 -15
- package/dist/actors/computer-use/openai-wire.d.ts +19 -7
- package/dist/actors/computer-use/openai-wire.js +36 -34
- package/dist/actors/computer-use/provider-error.d.ts +4 -4
- package/dist/actors/computer-use/provider-error.js +6 -6
- package/dist/actors/contract.d.ts +82 -47
- package/dist/actors/contract.js +30 -24
- package/dist/actors/local-agent/claude-session.d.ts +1 -1
- package/dist/actors/local-agent/claude-session.js +13 -12
- package/dist/actors/local-agent/cli.d.ts +9 -7
- package/dist/actors/local-agent/cli.js +24 -28
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +4 -4
- package/dist/actors/reasoning-effort.js +8 -8
- package/dist/actors/registry.d.ts +12 -12
- package/dist/actors/registry.js +21 -20
- package/dist/actors/scripted-browser/actor.d.ts +10 -10
- package/dist/actors/scripted-browser/actor.js +17 -17
- package/dist/actors/scripted-browser/browser-command.js +1 -1
- package/dist/actors/scripted-browser/steps.js +1 -1
- package/dist/actors/scripted-browser/types.d.ts +1 -1
- package/dist/actors/stop-conditions.d.ts +1 -1
- package/dist/analysis/automatic-completion.d.ts +7 -7
- package/dist/analysis/automatic-completion.js +1 -1
- package/dist/analysis/automatic-config.d.ts +6 -6
- package/dist/analysis/automatic-config.js +1 -1
- package/dist/analysis/codex-config.d.ts +6 -2
- package/dist/analysis/codex-config.js +22 -14
- package/dist/analysis/execute.d.ts +1 -1
- package/dist/analysis/execute.js +1 -1
- package/dist/analysis/service.js +5 -5
- package/dist/analysis/types.d.ts +6 -0
- package/dist/browser-control/client.js +16 -16
- package/dist/browser-control/dispatcher.js +4 -4
- package/dist/browser-control/protocol.js +14 -14
- package/dist/browser-control/recording-transfer.js +4 -4
- package/dist/browser-control/transport.js +7 -7
- package/dist/cli/commands/analysis-signals.d.ts +2 -2
- package/dist/cli/commands/analyze.js +19 -16
- package/dist/cli/commands/codex.js +9 -9
- package/dist/cli/commands/comms.js +48 -35
- package/dist/cli/commands/feedback.js +20 -21
- package/dist/cli/commands/migrate.d.ts +4 -0
- package/dist/cli/commands/migrate.js +65 -0
- package/dist/cli/commands/observe.d.ts +0 -1
- package/dist/cli/commands/observe.js +136 -112
- package/dist/cli/commands/run-command.d.ts +10 -0
- package/dist/cli/commands/run-command.js +85 -0
- package/dist/cli/commands/runs.js +136 -139
- package/dist/cli/commands/setup.d.ts +2 -2
- package/dist/cli/commands/setup.js +27 -24
- package/dist/cli/commands/study-format.d.ts +9 -0
- package/dist/cli/commands/{lab-format.js → study-format.js} +39 -27
- package/dist/cli/commands/{lab-route-computer-use.d.ts → study-route-computer-use.d.ts} +6 -6
- package/dist/cli/commands/{lab-route-computer-use.js → study-route-computer-use.js} +17 -18
- package/dist/cli/commands/{lab-route-open.d.ts → study-route-open.d.ts} +4 -4
- package/dist/cli/commands/{lab-route-open.js → study-route-open.js} +2 -2
- package/dist/cli/commands/{lab-route-preview.d.ts → study-route-preview.d.ts} +7 -7
- package/dist/cli/commands/{lab-route-preview.js → study-route-preview.js} +7 -6
- package/dist/cli/commands/study-route-run.d.ts +19 -0
- package/dist/cli/commands/{lab-route-run.js → study-route-run.js} +6 -6
- package/dist/cli/commands/study-route-scripted.d.ts +17 -0
- package/dist/cli/commands/{lab-route-scripted.js → study-route-scripted.js} +4 -4
- package/dist/cli/commands/{lab-route-shared-world.d.ts → study-route-shared-world.d.ts} +5 -5
- package/dist/cli/commands/{lab-route-shared-world.js → study-route-shared-world.js} +10 -10
- package/dist/cli/commands/study-route-terminal.d.ts +17 -0
- package/dist/cli/commands/{lab-route-terminal.js → study-route-terminal.js} +4 -4
- package/dist/cli/commands/study-run.d.ts +9 -0
- package/dist/cli/commands/{lab-run.js → study-run.js} +44 -45
- package/dist/cli/commands/{lab-scorer.d.ts → study-scorer.d.ts} +7 -7
- package/dist/cli/commands/{lab-scorer.js → study-scorer.js} +2 -2
- package/dist/cli/commands/study.d.ts +3 -0
- package/dist/cli/commands/study.js +146 -0
- package/dist/cli/commands/tui.d.ts +4 -4
- package/dist/cli/commands/tui.js +31 -31
- package/dist/cli/commands/watch.js +71 -121
- package/dist/cli/deprecations.d.ts +6 -0
- package/dist/cli/deprecations.js +18 -0
- package/dist/cli/doctor.d.ts +18 -8
- package/dist/cli/doctor.js +114 -59
- package/dist/cli/first-run-path.d.ts +15 -13
- package/dist/cli/first-run-path.js +35 -33
- package/dist/cli/io.d.ts +44 -9
- package/dist/cli/io.js +98 -23
- package/dist/cli/observer-follow.d.ts +3 -3
- package/dist/cli/observer-follow.js +6 -7
- package/dist/cli/orientation.d.ts +10 -7
- package/dist/cli/orientation.js +89 -47
- package/dist/cli/product-sentence.d.ts +6 -0
- package/dist/cli/product-sentence.js +6 -0
- package/dist/cli/program.d.ts +6 -1
- package/dist/cli/program.js +111 -43
- package/dist/cli/telemetry.d.ts +16 -16
- package/dist/cli/telemetry.js +46 -44
- package/dist/cli.js +1 -1
- package/dist/comms/capture-surface.d.ts +13 -12
- package/dist/comms/capture-surface.js +23 -22
- package/dist/comms/catch-host.d.ts +7 -7
- package/dist/comms/catch-host.js +17 -17
- package/dist/comms/connections.d.ts +1 -1
- package/dist/comms/connections.js +2 -2
- package/dist/comms/email-catch.d.ts +3 -3
- package/dist/comms/email-catch.js +8 -8
- package/dist/comms/external-evidence.d.ts +5 -5
- package/dist/comms/external-evidence.js +5 -4
- package/dist/comms/extract.js +2 -2
- package/dist/comms/fake-inbox.d.ts +3 -3
- package/dist/comms/fake-inbox.js +3 -3
- package/dist/comms/receiving-render.d.ts +1 -1
- package/dist/comms/receiving-render.js +2 -2
- package/dist/comms/receiving-runtime.d.ts +4 -4
- package/dist/comms/sandbox-catch-script.d.ts +6 -6
- package/dist/comms/sandbox-catch-script.js +6 -6
- package/dist/comms/sandbox-catch.d.ts +38 -39
- package/dist/comms/sandbox-catch.js +36 -37
- package/dist/comms/setup.d.ts +2 -2
- package/dist/comms/setup.js +56 -25
- package/dist/comms/thread-evidence.d.ts +1 -1
- package/dist/comms/thread-evidence.js +6 -6
- package/dist/comms/types.d.ts +6 -6
- package/dist/comms/types.js +3 -3
- package/dist/evidence/frame-signature.d.ts +1 -1
- package/dist/evidence/frame-signature.js +4 -4
- package/dist/evidence/redaction.d.ts +3 -3
- package/dist/evidence/redaction.js +4 -4
- package/dist/feedback/draft.js +38 -80
- package/dist/feedback/export-bundle.js +1 -1
- package/dist/feedback/export.d.ts +5 -1
- package/dist/feedback/export.js +10 -10
- package/dist/feedback/feedback.d.ts +2 -2
- package/dist/feedback/feedback.js +21 -3
- package/dist/feedback/proof.d.ts +1 -1
- package/dist/feedback/proof.js +2 -2
- package/dist/guest/bootstrap.js +6 -6
- package/dist/guest/browser-tools.js +8 -8
- package/dist/guest/chromium-text.js +19 -19
- package/dist/guest/desktop-executor.d.ts +2 -2
- package/dist/guest/desktop-executor.js +20 -20
- package/dist/guest/desktop-keys.js +4 -4
- package/dist/guest/desktop-media.d.ts +2 -2
- package/dist/guest/desktop-media.js +16 -16
- package/dist/guest/desktop-native.d.ts +1 -1
- package/dist/guest/desktop-native.js +10 -10
- package/dist/guest/runtime-desktop.js +2 -2
- package/dist/guest/runtime-processes.d.ts +2 -2
- package/dist/guest/runtime-processes.js +2 -2
- package/dist/guest/runtime.js +6 -6
- package/dist/index.d.ts +14 -14
- package/dist/index.js +8 -8
- package/dist/keys/key-resolution.d.ts +5 -5
- package/dist/keys/key-resolution.js +23 -23
- package/dist/observer/artifact.d.ts +2 -2
- package/dist/observer/artifact.js +7 -7
- package/dist/observer/data.d.ts +6 -6
- package/dist/observer/data.js +5 -4
- package/dist/observer/exposure.d.ts +1 -1
- package/dist/observer/exposure.js +11 -11
- package/dist/observer/http.js +1 -1
- package/dist/observer/library.d.ts +3 -1
- package/dist/observer/library.js +8 -3
- package/dist/observer/listen.js +5 -4
- package/dist/observer/render.d.ts +1 -1
- package/dist/observer/render.js +17 -6
- package/dist/observer/run-routes.d.ts +4 -3
- package/dist/observer/run-routes.js +38 -13
- package/dist/observer/serve.d.ts +1 -1
- package/dist/observer/serve.js +7 -3
- package/dist/observer/tunnel.js +1 -1
- package/dist/observer-app.html +4 -4
- package/dist/routes/computer-use/bundle-parts.d.ts +9 -9
- package/dist/routes/computer-use/bundle-parts.js +18 -17
- package/dist/routes/computer-use/bundle.d.ts +2 -2
- package/dist/routes/computer-use/bundle.js +10 -9
- package/dist/routes/computer-use/e2b-desktop/comms.d.ts +6 -6
- package/dist/routes/computer-use/e2b-desktop/comms.js +18 -17
- package/dist/routes/computer-use/e2b-desktop/desktop.js +4 -4
- package/dist/routes/computer-use/e2b-desktop/fidelity.d.ts +5 -4
- package/dist/routes/computer-use/e2b-desktop/fidelity.js +11 -10
- package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +1 -1
- package/dist/routes/computer-use/e2b-desktop/prepare.js +10 -10
- package/dist/routes/computer-use/e2b-desktop/start.js +7 -7
- package/dist/routes/computer-use/e2b-desktop/state.d.ts +2 -2
- package/dist/routes/computer-use/e2b-desktop/teardown.js +2 -2
- package/dist/routes/computer-use/external-comms.d.ts +4 -4
- package/dist/routes/computer-use/external-comms.js +1 -1
- package/dist/routes/computer-use/fanout-bundle.js +27 -26
- package/dist/routes/computer-use/fanout-records.js +12 -13
- package/dist/routes/computer-use/live-flush.d.ts +4 -4
- package/dist/routes/computer-use/live-flush.js +6 -6
- package/dist/routes/computer-use/live-phase.d.ts +5 -4
- package/dist/routes/computer-use/live-phase.js +8 -8
- package/dist/routes/computer-use/local-tree-pack.d.ts +5 -5
- package/dist/routes/computer-use/local-tree-pack.js +1 -1
- package/dist/routes/computer-use/local-vm.d.ts +8 -8
- package/dist/routes/computer-use/local-vm.js +3 -3
- package/dist/routes/computer-use/participant-desktop.d.ts +7 -7
- package/dist/routes/computer-use/participant-desktop.js +5 -5
- package/dist/routes/computer-use/participant-execution.d.ts +4 -4
- package/dist/routes/computer-use/participant-execution.js +10 -10
- package/dist/routes/computer-use/participant-model.d.ts +6 -0
- package/dist/routes/computer-use/participant-model.js +33 -15
- package/dist/routes/computer-use/participant-prompt.d.ts +15 -15
- package/dist/routes/computer-use/participant-prompt.js +9 -9
- package/dist/routes/computer-use/participant-runs.d.ts +16 -16
- package/dist/routes/computer-use/participant-runs.js +26 -25
- package/dist/routes/computer-use/plan.d.ts +12 -12
- package/dist/routes/computer-use/plan.js +42 -42
- package/dist/routes/computer-use/preflight.d.ts +5 -5
- package/dist/routes/computer-use/preflight.js +25 -25
- package/dist/routes/computer-use/rerun-selection.d.ts +1 -1
- package/dist/routes/computer-use/rerun-selection.js +4 -3
- package/dist/routes/computer-use/result.d.ts +4 -4
- package/dist/routes/computer-use/result.js +22 -21
- package/dist/routes/computer-use/route.d.ts +12 -11
- package/dist/routes/computer-use/route.js +31 -30
- package/dist/routes/computer-use/self-report.d.ts +7 -7
- package/dist/routes/computer-use/self-report.js +32 -32
- package/dist/routes/computer-use/setup.d.ts +23 -23
- package/dist/routes/computer-use/setup.js +28 -29
- package/dist/routes/computer-use/single-bundle.d.ts +11 -11
- package/dist/routes/computer-use/single-bundle.js +40 -41
- package/dist/routes/computer-use/subject-projection.d.ts +4 -4
- package/dist/routes/computer-use/subject-projection.js +6 -6
- package/dist/routes/computer-use/types.d.ts +71 -73
- package/dist/routes/computer-use/types.js +12 -12
- package/dist/routes/preview.d.ts +13 -7
- package/dist/routes/preview.js +11 -7
- package/dist/routes/scripted/bundle.d.ts +7 -7
- package/dist/routes/scripted/bundle.js +20 -18
- package/dist/routes/scripted/plan.d.ts +8 -8
- package/dist/routes/scripted/plan.js +13 -13
- package/dist/routes/scripted/result.d.ts +3 -3
- package/dist/routes/scripted/result.js +13 -13
- package/dist/routes/scripted/route.d.ts +9 -9
- package/dist/routes/scripted/route.js +15 -16
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +5 -5
- package/dist/routes/scripted/setup.d.ts +6 -6
- package/dist/routes/scripted/setup.js +8 -8
- package/dist/routes/scripted/subject.d.ts +5 -5
- package/dist/routes/scripted/subject.js +4 -4
- package/dist/routes/scripted/surface-records.d.ts +1 -1
- package/dist/routes/scripted/surface-records.js +4 -4
- package/dist/routes/scripted/surface-sessions.d.ts +3 -3
- package/dist/routes/scripted/types.d.ts +18 -20
- package/dist/routes/scripted/types.js +2 -2
- package/dist/routes/shared-world/bundle-records.js +11 -10
- package/dist/routes/shared-world/bundle.d.ts +5 -5
- package/dist/routes/shared-world/bundle.js +46 -43
- package/dist/routes/shared-world/checkpoints.d.ts +4 -4
- package/dist/routes/shared-world/checkpoints.js +2 -2
- package/dist/routes/shared-world/comms.d.ts +14 -14
- package/dist/routes/shared-world/comms.js +19 -18
- package/dist/routes/shared-world/external-public.d.ts +4 -3
- package/dist/routes/shared-world/external-public.js +16 -12
- package/dist/routes/shared-world/handoff.d.ts +7 -6
- package/dist/routes/shared-world/handoff.js +65 -57
- package/dist/routes/shared-world/lobby-code.d.ts +2 -2
- package/dist/routes/shared-world/lobby-code.js +9 -9
- package/dist/routes/shared-world/participant-specs.d.ts +10 -9
- package/dist/routes/shared-world/participant-specs.js +38 -31
- package/dist/routes/shared-world/plan.d.ts +9 -9
- package/dist/routes/shared-world/plan.js +16 -16
- package/dist/routes/shared-world/provenance.d.ts +2 -2
- package/dist/routes/shared-world/provenance.js +2 -2
- package/dist/routes/shared-world/provisioned.d.ts +8 -8
- package/dist/routes/shared-world/provisioned.js +18 -17
- package/dist/routes/shared-world/result.d.ts +6 -6
- package/dist/routes/shared-world/result.js +29 -29
- package/dist/routes/shared-world/route.d.ts +11 -11
- package/dist/routes/shared-world/route.js +40 -39
- package/dist/routes/shared-world/setup.d.ts +16 -16
- package/dist/routes/shared-world/setup.js +23 -23
- package/dist/routes/shared-world/types.d.ts +55 -53
- package/dist/routes/shared-world/types.js +4 -4
- package/dist/routes/terminal/adapter.d.ts +5 -5
- package/dist/routes/terminal/adapter.js +9 -9
- package/dist/routes/terminal/bundle.d.ts +13 -13
- package/dist/routes/terminal/bundle.js +28 -27
- package/dist/routes/terminal/credentials.d.ts +8 -8
- package/dist/routes/terminal/credentials.js +12 -12
- package/dist/routes/terminal/dry-run.d.ts +4 -4
- package/dist/routes/terminal/dry-run.js +12 -12
- package/dist/routes/terminal/encoding.js +2 -2
- package/dist/routes/terminal/ledger.d.ts +5 -5
- package/dist/routes/terminal/ledger.js +14 -14
- package/dist/routes/terminal/lifetime.d.ts +1 -1
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.d.ts +2 -2
- package/dist/routes/terminal/live-finish.js +10 -10
- package/dist/routes/terminal/live-sandbox.d.ts +2 -2
- package/dist/routes/terminal/live-sandbox.js +15 -15
- package/dist/routes/terminal/participant-text.js +2 -1
- package/dist/routes/terminal/persona.d.ts +2 -2
- package/dist/routes/terminal/persona.js +3 -3
- package/dist/routes/terminal/plan.d.ts +7 -7
- package/dist/routes/terminal/plan.js +15 -15
- package/dist/routes/terminal/recorder.js +2 -2
- package/dist/routes/terminal/result.d.ts +5 -5
- package/dist/routes/terminal/result.js +14 -14
- package/dist/routes/terminal/route.d.ts +9 -9
- package/dist/routes/terminal/route.js +19 -20
- package/dist/routes/terminal/sandbox.d.ts +1 -1
- package/dist/routes/terminal/sandbox.js +1 -1
- package/dist/routes/terminal/session.d.ts +5 -5
- package/dist/routes/terminal/session.js +14 -13
- package/dist/routes/terminal/token-usage.d.ts +1 -1
- package/dist/routes/terminal/token-usage.js +7 -8
- package/dist/routes/terminal/trace.d.ts +4 -4
- package/dist/routes/terminal/trace.js +2 -2
- package/dist/routes/terminal/types.d.ts +48 -50
- package/dist/routes/terminal/types.js +5 -5
- package/dist/run/bundle-shape.js +3 -3
- package/dist/run/bundle.d.ts +49 -49
- package/dist/run/bundle.js +3 -2
- package/dist/run/cost-summary.js +3 -3
- package/dist/run/costs.d.ts +23 -0
- package/dist/run/costs.js +79 -44
- package/dist/run/detail.d.ts +6 -6
- package/dist/run/detail.js +4 -4
- package/dist/run/dry-run-inputs.d.ts +1 -1
- package/dist/run/dry-run-inputs.js +2 -2
- package/dist/run/dry-run.js +20 -20
- package/dist/run/judge.d.ts +19 -13
- package/dist/run/judge.js +18 -8
- package/dist/run/outcomes.d.ts +1 -1
- package/dist/run/participant.d.ts +1 -1
- package/dist/run/paths.d.ts +1 -1
- package/dist/run/paths.js +1 -1
- package/dist/run/preflight-receipts.js +1 -1
- package/dist/run/pricing.d.ts +11 -6
- package/dist/run/pricing.js +22 -15
- package/dist/run/projection.d.ts +46 -46
- package/dist/run/projection.js +57 -57
- package/dist/run/reclaim.d.ts +1 -1
- package/dist/run/reclaim.js +3 -3
- package/dist/run/results.d.ts +5 -5
- package/dist/run/run-cost.d.ts +53 -0
- package/dist/run/run-cost.js +105 -0
- package/dist/run/run-index-analysis.d.ts +15 -0
- package/dist/run/run-index-analysis.js +77 -0
- package/dist/run/run-index.d.ts +19 -7
- package/dist/run/run-index.js +77 -64
- package/dist/run/run-not-found.d.ts +5 -0
- package/dist/run/run-not-found.js +15 -0
- package/dist/run/run.d.ts +6 -5
- package/dist/run/run.js +3 -3
- package/dist/run/sandbox-receipts.d.ts +2 -2
- package/dist/run/sandbox-receipts.js +3 -3
- package/dist/run/shared-world-evidence.d.ts +37 -36
- package/dist/run/shared-world-evidence.js +4 -4
- package/dist/run/shared-world-shape.d.ts +1 -1
- package/dist/run/shared-world-shape.js +7 -7
- package/dist/run/stats.d.ts +14 -8
- package/dist/run/stats.js +121 -63
- package/dist/run/status.d.ts +16 -29
- package/dist/run/status.js +10 -29
- package/dist/run/stored-runs.js +2 -1
- package/dist/run/stream-shape.js +1 -1
- package/dist/run/streams.d.ts +7 -7
- package/dist/run/study-provenance.d.ts +49 -0
- package/dist/run/study-provenance.js +66 -0
- package/dist/run/study-result.d.ts +11 -0
- package/dist/run/study-result.js +9 -0
- package/dist/run/text.d.ts +2 -0
- package/dist/run/text.js +4 -0
- package/dist/run-study.d.ts +112 -0
- package/dist/{run-lab.js → run-study.js} +52 -47
- package/dist/{lab → study}/adapter-extension.d.ts +15 -27
- package/dist/{lab → study}/adapter-extension.js +10 -45
- package/dist/{lab → study}/adapter-scorer-loader.d.ts +17 -17
- package/dist/{lab → study}/adapter-scorer-loader.js +34 -34
- package/dist/study/composition-rules.d.ts +7 -0
- package/dist/{lab → study}/composition-rules.js +53 -53
- package/dist/study/config.d.ts +15 -0
- package/dist/{lab → study}/config.js +98 -41
- package/dist/study/convert.d.ts +33 -0
- package/dist/study/convert.js +432 -0
- package/dist/{lab → study}/device-presets.d.ts +10 -10
- package/dist/{lab → study}/device-presets.js +11 -11
- package/dist/study/discover.d.ts +75 -0
- package/dist/{lab → study}/discover.js +134 -95
- package/dist/{lab → study}/doctor.d.ts +19 -12
- package/dist/{lab → study}/doctor.js +58 -20
- package/dist/study/field-paths.d.ts +12 -0
- package/dist/study/field-paths.js +126 -0
- package/dist/study/files.d.ts +26 -0
- package/dist/study/files.js +44 -0
- package/dist/{lab → study}/init-templates.d.ts +2 -2
- package/dist/study/init-templates.js +421 -0
- package/dist/{lab → study}/init.d.ts +2 -2
- package/dist/{lab → study}/init.js +46 -11
- package/dist/study/keys.d.ts +4 -0
- package/dist/{lab → study}/keys.js +46 -12
- package/dist/study/migrate.d.ts +48 -0
- package/dist/study/migrate.js +408 -0
- package/dist/study/owned-files.d.ts +38 -0
- package/dist/study/owned-files.js +182 -0
- package/dist/{lab → study}/parse/actors.d.ts +5 -5
- package/dist/{lab → study}/parse/actors.js +14 -14
- package/dist/study/parse/comms.d.ts +12 -0
- package/dist/{lab → study}/parse/comms.js +5 -5
- package/dist/study/parse/execution.d.ts +19 -0
- package/dist/{lab → study}/parse/execution.js +13 -13
- package/dist/study/parse/study-v3.d.ts +21 -0
- package/dist/study/parse/study-v3.js +259 -0
- package/dist/study/parse/subject-state.d.ts +30 -0
- package/dist/{lab → study}/parse/subject-state.js +19 -19
- package/dist/{lab → study}/parse/subject.d.ts +3 -3
- package/dist/{lab → study}/parse/subject.js +34 -34
- package/dist/{lab → study}/parse/values.d.ts +3 -3
- package/dist/{lab → study}/parse/values.js +3 -3
- package/dist/{lab → study}/persona-resolve.d.ts +8 -8
- package/dist/{lab → study}/persona-resolve.js +6 -6
- package/dist/{lab → study}/plan-base.d.ts +7 -7
- package/dist/{lab → study}/plan-base.js +1 -1
- package/dist/{lab → study}/plan-participants.d.ts +12 -12
- package/dist/{lab → study}/plan-participants.js +4 -4
- package/dist/{lab → study}/plan-types.d.ts +31 -31
- package/dist/{lab → study}/plan-types.js +2 -2
- package/dist/study/plan.d.ts +13 -0
- package/dist/study/plan.js +101 -0
- package/dist/study/preflight-probes.d.ts +3 -0
- package/dist/{lab → study}/preflight-probes.js +20 -19
- package/dist/study/preflight-result.d.ts +8 -0
- package/dist/{lab → study}/preflight-result.js +5 -4
- package/dist/study/preflight.d.ts +94 -0
- package/dist/{lab → study}/preflight.js +12 -12
- package/dist/{lab → study}/requirements.d.ts +3 -3
- package/dist/{lab → study}/requirements.js +1 -1
- package/dist/study/route-inputs.d.ts +28 -0
- package/dist/{lab → study}/route-inputs.js +1 -1
- package/dist/{lab → study}/routing.d.ts +39 -33
- package/dist/{lab → study}/routing.js +60 -29
- package/dist/{lab/run-lab-events.d.ts → study/run-study-events.d.ts} +10 -13
- package/dist/{lab/run-lab-events.js → study/run-study-events.js} +3 -9
- package/dist/{lab/run-lab-homes.d.ts → study/run-study-homes.d.ts} +21 -18
- package/dist/{lab/run-lab-homes.js → study/run-study-homes.js} +1 -1
- package/dist/study/run-study-options.d.ts +28 -0
- package/dist/{lab/run-lab-options.js → study/run-study-options.js} +29 -25
- package/dist/{lab/lab-deps.d.ts → study/study-deps.d.ts} +8 -13
- package/dist/{lab/lab-deps.js → study/study-deps.js} +2 -2
- package/dist/{lab → study}/summary.d.ts +16 -15
- package/dist/{lab → study}/summary.js +21 -21
- package/dist/{lab → study}/tasks.d.ts +18 -18
- package/dist/{lab → study}/tasks.js +14 -14
- package/dist/{lab → study}/types.d.ts +202 -196
- package/dist/{lab → study}/types.js +3 -1
- package/dist/{lab → study}/validation.d.ts +27 -27
- package/dist/{lab → study}/validation.js +53 -51
- package/dist/study/warnings.d.ts +12 -0
- package/dist/{lab → study}/warnings.js +62 -34
- package/dist/subject/clone.d.ts +7 -7
- package/dist/subject/clone.js +2 -2
- package/dist/subject/desktop-cli.d.ts +1 -1
- package/dist/subject/desktop-cli.js +2 -2
- package/dist/subject/local-tree-archive.d.ts +2 -2
- package/dist/subject/local-tree-archive.js +4 -4
- package/dist/subject/local-tree.d.ts +4 -4
- package/dist/subject/node-bootstrap.js +1 -1
- package/dist/subject/runtime.js +9 -10
- package/dist/subject/serve.d.ts +5 -5
- package/dist/subject/serve.js +3 -3
- package/dist/subject/state.d.ts +4 -4
- package/dist/subject/state.js +1 -1
- package/dist/subject/steps.d.ts +6 -6
- package/dist/subject/steps.js +5 -5
- package/dist/substrates/command-failure.d.ts +3 -3
- package/dist/substrates/command-failure.js +3 -3
- package/dist/substrates/desktop-session.js +2 -2
- package/dist/substrates/detached.d.ts +6 -6
- package/dist/substrates/detached.js +7 -7
- package/dist/substrates/e2b/cdp-probe.d.ts +9 -9
- package/dist/substrates/e2b/cdp-probe.js +14 -17
- package/dist/substrates/e2b/desktop-browser.d.ts +6 -6
- package/dist/substrates/e2b/desktop-browser.js +9 -9
- package/dist/substrates/e2b/desktop-cdp.d.ts +10 -10
- package/dist/substrates/e2b/desktop-cdp.js +9 -9
- package/dist/substrates/e2b/desktop-executor.d.ts +2 -2
- package/dist/substrates/e2b/desktop-executor.js +13 -13
- package/dist/substrates/e2b/desktop-geometry.d.ts +4 -4
- package/dist/substrates/e2b/desktop-geometry.js +8 -8
- package/dist/substrates/e2b/desktop-media.d.ts +4 -4
- package/dist/substrates/e2b/desktop-media.js +1 -1
- package/dist/substrates/e2b/desktop-screenshot-cleanup.d.ts +3 -3
- package/dist/substrates/e2b/desktop-screenshot-cleanup.js +3 -3
- package/dist/substrates/e2b/lifetime.js +1 -1
- package/dist/substrates/e2b/sandbox.d.ts +4 -4
- package/dist/substrates/e2b/sandbox.js +2 -2
- package/dist/substrates/e2b/sdk.d.ts +12 -12
- package/dist/substrates/e2b/sdk.js +7 -7
- package/dist/substrates/local/runtime-config.d.ts +4 -4
- package/dist/substrates/local/runtime-config.js +4 -4
- package/dist/tui/actions.d.ts +1 -1
- package/dist/tui/actions.js +13 -13
- package/dist/tui/contract.d.ts +23 -23
- package/dist/tui/contract.js +11 -11
- package/dist/tui/launch.d.ts +7 -7
- package/dist/tui/launch.js +36 -31
- package/dist/tui/project.js +5 -5
- package/dist/tui-app.js +139 -139
- package/dist/verify/actor.d.ts +8 -8
- package/dist/verify/actor.js +36 -36
- package/dist/verify/artifacts.d.ts +2 -2
- package/dist/verify/artifacts.js +3 -3
- package/dist/verify/context-growth.d.ts +9 -0
- package/dist/verify/context-growth.js +62 -0
- package/dist/verify/costs.d.ts +6 -6
- package/dist/verify/costs.js +8 -8
- package/dist/verify/shared-world-concurrent.d.ts +2 -2
- package/dist/verify/shared-world-concurrent.js +79 -76
- package/dist/verify/shared-world.d.ts +2 -2
- package/dist/verify/shared-world.js +16 -16
- package/dist/verify/subject.d.ts +5 -5
- package/dist/verify/subject.js +19 -19
- package/dist/verify/verify.js +94 -44
- package/docs/README.md +25 -32
- package/docs/architecture/actor-contract.md +60 -91
- package/docs/architecture/browser-control.md +1 -1
- package/docs/architecture/desktop-sessions.md +7 -7
- package/docs/architecture/external-public-shared-world.md +68 -65
- package/docs/architecture/github-feedback-loop.md +2 -5
- package/docs/architecture/guest-desktop.md +2 -2
- package/docs/architecture/observer.md +34 -34
- package/docs/architecture/restricted-codex-analysis.md +34 -28
- package/docs/architecture/serve.md +34 -37
- package/docs/architecture/state-driven-executor.md +55 -58
- package/docs/architecture/task-protocol-support.md +6 -6
- package/docs/architecture/terminal-product-route.md +70 -110
- package/docs/contracts/README.md +24 -0
- package/docs/contracts/adapter-admission.md +3 -3
- package/docs/contracts/adapter-fixtures.md +4 -7
- package/docs/contracts/core.md +1 -3
- package/docs/contracts/errors.md +62 -0
- package/docs/contracts/feedback.md +8 -9
- package/docs/contracts/policy.md +7 -12
- package/docs/contracts/run-bundle.md +42 -44
- package/docs/contracts/schemas.md +446 -368
- package/docs/contracts/study-analysis.md +7 -4
- package/docs/contracts/study-costs.md +28 -9
- package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +1 -1
- package/docs/decisions/0002-observer-is-one-self-contained-file.md +2 -2
- package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +3 -3
- package/docs/principles/actor-fidelity.md +4 -4
- package/docs/principles/engineering.md +3 -3
- package/docs/principles/invariants-and-defaults.md +38 -38
- package/docs/principles/self-driving-harness.md +1 -5
- package/docs/principles/three-roles.md +1 -1
- package/docs/product/automatic-analysis.md +7 -7
- package/examples/README.md +1 -1
- package/examples/participant/README.md +5 -5
- package/examples/participant/run.mjs +11 -10
- package/examples/scorer/README.md +3 -3
- package/examples/scorer/run.mjs +11 -12
- package/examples/scorer/scorer.mjs +3 -3
- package/package.json +16 -17
- package/skills/humanish/SKILL.md +64 -60
- package/skills/humanish/agents/openai.yaml +1 -1
- package/AGENTS.md +0 -99
- package/CONTRIBUTING.md +0 -148
- package/dist/actors/codex/qualified-versions.d.ts +0 -72
- package/dist/actors/codex/qualified-versions.js +0 -132
- package/dist/cli/commands/lab-format.d.ts +0 -8
- package/dist/cli/commands/lab-route-run.d.ts +0 -19
- package/dist/cli/commands/lab-route-scripted.d.ts +0 -17
- package/dist/cli/commands/lab-route-terminal.d.ts +0 -17
- package/dist/cli/commands/lab-run.d.ts +0 -9
- package/dist/cli/commands/lab.d.ts +0 -3
- package/dist/cli/commands/lab.js +0 -199
- package/dist/cli/renamed-options.d.ts +0 -11
- package/dist/cli/renamed-options.js +0 -18
- package/dist/lab/composition-rules.d.ts +0 -7
- package/dist/lab/config.d.ts +0 -7
- package/dist/lab/discover.d.ts +0 -65
- package/dist/lab/init-templates.js +0 -416
- package/dist/lab/keys.d.ts +0 -2
- package/dist/lab/parse/comms.d.ts +0 -12
- package/dist/lab/parse/execution.d.ts +0 -19
- package/dist/lab/parse/subject-state.d.ts +0 -30
- package/dist/lab/plan.d.ts +0 -19
- package/dist/lab/plan.js +0 -132
- package/dist/lab/preflight-probes.d.ts +0 -3
- package/dist/lab/preflight-result.d.ts +0 -7
- package/dist/lab/preflight.d.ts +0 -92
- package/dist/lab/route-inputs.d.ts +0 -28
- package/dist/lab/run-lab-options.d.ts +0 -28
- package/dist/lab/warnings.d.ts +0 -2
- package/dist/observer/static.d.ts +0 -39
- package/dist/observer/static.js +0 -369
- package/dist/run-lab.d.ts +0 -112
- package/docs/architecture/comms-inbox.md +0 -122
- package/docs/architecture/desktop-recording.md +0 -80
- package/docs/architecture/local-browser-runtime.md +0 -178
- package/docs/architecture/observer-review.md +0 -144
- package/docs/architecture/participant-media.md +0 -90
- package/docs/architecture/project-layout.md +0 -62
- package/docs/architecture/real-email-receiving.md +0 -136
- package/docs/assets/humanish-drawdb-hero.png +0 -0
- package/docs/assets/humanish-observer-hero.png +0 -0
- package/docs/goals/current.md +0 -207
- package/docs/product/lobby-trivia-3player-external-public.md +0 -77
- package/docs/product/open-source-install-experience.md +0 -240
- package/docs/ramp/README.md +0 -295
- package/docs/release/open-source-readiness.md +0 -222
- package/docs/release/public-readiness-standard.md +0 -210
- package/docs/roadmap/world-class-open-source-v0.md +0 -311
- /package/dist/{lab → study}/init-paths.d.ts +0 -0
- /package/dist/{lab → study}/init-paths.js +0 -0
- /package/dist/{lab → study}/init-plan.d.ts +0 -0
- /package/dist/{lab → study}/init-plan.js +0 -0
- /package/dist/{lab → study}/participant-assignment.d.ts +0 -0
- /package/dist/{lab → study}/participant-assignment.js +0 -0
- /package/dist/{lab → study}/persona.d.ts +0 -0
- /package/dist/{lab → study}/persona.js +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { StudyRoute } from "./routing.js";
|
|
2
|
+
import { type DroppedKey, type MovedKey } from "./convert.js";
|
|
3
|
+
/** The schema of `humanish migrate --json`. */
|
|
4
|
+
declare const MIGRATE_SCHEMA = "humanish.migrate-result.v1";
|
|
5
|
+
/** One file migrate looked at. Paths are relative to the project. */
|
|
6
|
+
export interface MigrateFile {
|
|
7
|
+
readonly source: string;
|
|
8
|
+
/** `move` to a studies/ directory, `rewrite` in place, or `skip` a v3 file. */
|
|
9
|
+
readonly action: "move" | "rewrite" | "skip";
|
|
10
|
+
readonly destination?: string;
|
|
11
|
+
readonly route?: StudyRoute;
|
|
12
|
+
readonly moved: readonly MovedKey[];
|
|
13
|
+
readonly dropped: readonly DroppedKey[];
|
|
14
|
+
}
|
|
15
|
+
/** What migrate did, or would do with `--dry-run`. */
|
|
16
|
+
export interface MigrateResult {
|
|
17
|
+
readonly schema: typeof MIGRATE_SCHEMA;
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly cwd: string;
|
|
20
|
+
readonly dryRun: boolean;
|
|
21
|
+
readonly files: readonly MigrateFile[];
|
|
22
|
+
/** Files left behind after a clean-up step found them changed: sources, backups or temp files. */
|
|
23
|
+
readonly unresolved?: readonly string[];
|
|
24
|
+
readonly error?: {
|
|
25
|
+
readonly code: "HUMANISH_MIGRATE_REFUSED" | "HUMANISH_MIGRATE_FAILED";
|
|
26
|
+
readonly phase: "plan" | "stage" | "commit" | "clean-up";
|
|
27
|
+
readonly file?: string;
|
|
28
|
+
readonly message: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** How to run migrate. */
|
|
32
|
+
export interface MigrateOptions {
|
|
33
|
+
readonly cwd: string;
|
|
34
|
+
/** Files to convert. Without any, the six study directories are scanned. */
|
|
35
|
+
readonly paths?: readonly string[];
|
|
36
|
+
readonly dryRun?: boolean;
|
|
37
|
+
/** Called with the plan before anything is written, so a caller can print every destination. */
|
|
38
|
+
readonly onPlan?: (files: readonly MigrateFile[]) => void;
|
|
39
|
+
/** Replaces `fs.link`. Tests use it to stand in for a filesystem without hard links. */
|
|
40
|
+
readonly link?: (from: string, to: string) => Promise<void>;
|
|
41
|
+
/** Called between stage and commit, and between commit and clean-up. Tests change files there. */
|
|
42
|
+
readonly onPhase?: (finished: "stage" | "commit") => void;
|
|
43
|
+
/** Called for each file after its last check and before its commit changes anything. Tests race it. */
|
|
44
|
+
readonly onCommit?: (source: string) => Promise<void> | void;
|
|
45
|
+
}
|
|
46
|
+
/** Convert the selected v2 study files to v3. */
|
|
47
|
+
export declare function migrateStudies(options: MigrateOptions): Promise<MigrateResult>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// `humanish migrate`: convert humanish.lab.v2 study files to humanish.study.v3. A file under a
|
|
2
|
+
// labs/ directory moves to the matching studies/ directory; any other file is rewritten in place.
|
|
3
|
+
// It runs in four phases, and a phase that fails leaves every source file where it was:
|
|
4
|
+
// 1. Plan: read, convert and plan-check every file, and record each source's identity. No writes.
|
|
5
|
+
// 2. Stage: write each output to a new temp file beside its destination. A failure removes them.
|
|
6
|
+
// 3. Commit: recheck each source. A rewritten source is first renamed to a random hidden name and
|
|
7
|
+
// checked there. Then the new file is linked into place, which fails if anything holds that
|
|
8
|
+
// name, so no file this run did not write is ever overwritten. A failure undoes the commits.
|
|
9
|
+
// 4. Clean up: delete each moved source and each rewrite's original, once checked (owned-files.ts).
|
|
10
|
+
// A file that changed since the plan is kept and listed; a rewrite's original as <name>.v2.bak.
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { link, lstat, readdir, readFile, realpath, rename, rmdir } from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { parseDocument } from "yaml";
|
|
15
|
+
import { isNodeError } from "../run/type-guards.js";
|
|
16
|
+
import { convertStudyText, isStudyV3 } from "./convert.js";
|
|
17
|
+
import { otherStudyFiles, STUDY_DIRECTORIES, studyFileStem } from "./files.js";
|
|
18
|
+
import { assertDirectory, bindDirectory, exists, fileIdentity, NO_HARD_LINKS, removeChecked, removeOwned, restoreName, sameFile, sha256, writeOwned, } from "./owned-files.js";
|
|
19
|
+
/** The schema of `humanish migrate --json`. */
|
|
20
|
+
const MIGRATE_SCHEMA = "humanish.migrate-result.v1";
|
|
21
|
+
class MigrateError extends Error {
|
|
22
|
+
phase;
|
|
23
|
+
file;
|
|
24
|
+
refused;
|
|
25
|
+
unresolved;
|
|
26
|
+
constructor(phase, file, message, refused = false, unresolved = []) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.phase = phase;
|
|
29
|
+
this.file = file;
|
|
30
|
+
this.refused = refused;
|
|
31
|
+
this.unresolved = unresolved;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const LABS_TO_STUDIES = new Map(STUDY_DIRECTORIES.filter((directory) => directory.family === "labs").map((directory) => [
|
|
35
|
+
directory.relativeDir,
|
|
36
|
+
directory.relativeDir.replace(/labs$/, "studies"),
|
|
37
|
+
]));
|
|
38
|
+
async function scanTargets(root) {
|
|
39
|
+
const found = [];
|
|
40
|
+
for (const directory of STUDY_DIRECTORIES) {
|
|
41
|
+
const bound = await bindDirectory(root, directory.relativeDir);
|
|
42
|
+
if (bound === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
for (const name of (await readdir(bound.physicalPath)).sort()) {
|
|
45
|
+
if (studyFileStem(name) !== undefined)
|
|
46
|
+
found.push(path.join(directory.relativeDir, name));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
51
|
+
async function givenTargets(root, paths) {
|
|
52
|
+
const seen = new Map();
|
|
53
|
+
for (const given of paths) {
|
|
54
|
+
const absolute = path.resolve(root, given);
|
|
55
|
+
const relative = path.relative(root, absolute);
|
|
56
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
57
|
+
throw new MigrateError("plan", given, `${given} is outside the project.`, true);
|
|
58
|
+
let real;
|
|
59
|
+
try {
|
|
60
|
+
real = await realpath(absolute);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new MigrateError("plan", given, `${given} does not exist.`, true);
|
|
64
|
+
}
|
|
65
|
+
if (!seen.has(real))
|
|
66
|
+
seen.set(real, relative);
|
|
67
|
+
}
|
|
68
|
+
return [...seen.values()];
|
|
69
|
+
}
|
|
70
|
+
async function planWrites(root, options) {
|
|
71
|
+
const targets = options.paths && options.paths.length > 0
|
|
72
|
+
? await givenTargets(root, options.paths)
|
|
73
|
+
: await scanTargets(root);
|
|
74
|
+
const files = [];
|
|
75
|
+
const writes = [];
|
|
76
|
+
for (const source of targets) {
|
|
77
|
+
const sourcePath = path.join(root, source);
|
|
78
|
+
let sourceDirectory;
|
|
79
|
+
let identity;
|
|
80
|
+
let mode;
|
|
81
|
+
let bytes;
|
|
82
|
+
try {
|
|
83
|
+
sourceDirectory = await bindDirectory(root, path.dirname(source));
|
|
84
|
+
if (sourceDirectory === undefined)
|
|
85
|
+
throw new Error("its directory is missing");
|
|
86
|
+
identity = await fileIdentity(sourcePath);
|
|
87
|
+
mode = Number((await lstat(sourcePath)).mode & 0o777);
|
|
88
|
+
bytes = await readFile(sourcePath);
|
|
89
|
+
if (sha256(bytes) !== identity.sha256)
|
|
90
|
+
throw new Error("it changed while it was read");
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw new MigrateError("plan", source, `${source}: ${error.message}.`, true);
|
|
94
|
+
}
|
|
95
|
+
const text = bytes.toString("utf8");
|
|
96
|
+
if (isStudyV3(parseDocument(text).toJS())) {
|
|
97
|
+
files.push({ source, action: "skip", moved: [], dropped: [] });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const converted = convertStudyText(text, root);
|
|
101
|
+
if (!converted.ok)
|
|
102
|
+
throw new MigrateError("plan", source, `${source} was not converted: ${converted.reason}`, true);
|
|
103
|
+
const studiesDir = LABS_TO_STUDIES.get(path.dirname(source));
|
|
104
|
+
const destination = studiesDir === undefined ? source : path.join(studiesDir, path.basename(source));
|
|
105
|
+
const file = {
|
|
106
|
+
source,
|
|
107
|
+
action: studiesDir === undefined ? "rewrite" : "move",
|
|
108
|
+
destination,
|
|
109
|
+
route: converted.conversion.route,
|
|
110
|
+
moved: converted.conversion.moved,
|
|
111
|
+
dropped: converted.conversion.dropped,
|
|
112
|
+
};
|
|
113
|
+
files.push(file);
|
|
114
|
+
writes.push({
|
|
115
|
+
file,
|
|
116
|
+
sourcePath,
|
|
117
|
+
sourceDirectory: sourceDirectory,
|
|
118
|
+
sourceIdentity: identity,
|
|
119
|
+
sourceMode: mode,
|
|
120
|
+
destinationPath: path.join(root, destination),
|
|
121
|
+
asidePath: path.join(path.dirname(sourcePath), `.${path.basename(sourcePath)}.migrate-${randomUUID()}.orig`),
|
|
122
|
+
backupPath: `${sourcePath}.v2.bak`,
|
|
123
|
+
text: converted.conversion.text,
|
|
124
|
+
expected: sha256(Buffer.from(converted.conversion.text)),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
await refuseCollisions(root, writes);
|
|
128
|
+
return { files, writes };
|
|
129
|
+
}
|
|
130
|
+
// A destination two files share, or a name another file already uses, would leave discovery two
|
|
131
|
+
// files for one study. Files that move together are not collisions with each other.
|
|
132
|
+
async function refuseCollisions(root, writes) {
|
|
133
|
+
const sources = new Set(writes.map((write) => write.file.source));
|
|
134
|
+
const destinations = new Map();
|
|
135
|
+
for (const write of writes) {
|
|
136
|
+
const destination = write.file.destination;
|
|
137
|
+
const stem = studyFileStem(path.basename(destination));
|
|
138
|
+
// A file without a study extension has no stem, so only its exact path can collide.
|
|
139
|
+
const sameStem = stem === undefined
|
|
140
|
+
? undefined
|
|
141
|
+
: [...destinations.entries()].find(([other]) => path.dirname(other) === path.dirname(destination) &&
|
|
142
|
+
studyFileStem(path.basename(other)) === stem);
|
|
143
|
+
if (destinations.has(destination) || sameStem) {
|
|
144
|
+
const other = destinations.get(destination) ?? sameStem[1];
|
|
145
|
+
const clash = destinations.has(destination)
|
|
146
|
+
? `would both become ${destination}`
|
|
147
|
+
: `would share the name ${studyFileStem(path.basename(destination))} in ${path.dirname(destination)}`;
|
|
148
|
+
throw new MigrateError("plan", write.file.source, `${write.file.source} and ${other} ${clash}. Remove one, then migrate.`, true);
|
|
149
|
+
}
|
|
150
|
+
destinations.set(destination, write.file.source);
|
|
151
|
+
const others = await collisions(root, write, sources);
|
|
152
|
+
const blocking = write.file.action === "move" && (await exists(write.destinationPath))
|
|
153
|
+
? [write.file.destination, ...others]
|
|
154
|
+
: others;
|
|
155
|
+
if (blocking.length > 0) {
|
|
156
|
+
throw new MigrateError("plan", write.file.source, `${write.file.source} and ${blocking[0]} share the name ${stem ?? path.basename(destination)}. Keep one, then migrate.`, true);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function stage(root, writes, created) {
|
|
161
|
+
for (const write of writes) {
|
|
162
|
+
try {
|
|
163
|
+
write.destinationDirectory = await bindDirectory(root, path.dirname(write.file.destination), created);
|
|
164
|
+
write.temp = {
|
|
165
|
+
path: path.join(path.dirname(write.destinationPath), `.${path.basename(write.destinationPath)}.migrate-${randomUUID()}.tmp`),
|
|
166
|
+
};
|
|
167
|
+
await writeOwned(write.temp, write.text, write.sourceMode);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new MigrateError("stage", write.file.source, `${write.file.source}: ${error.message}.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// After a failed stage or commit: remove each temp file. One this run did not write, or that
|
|
175
|
+
// changed, is kept and listed.
|
|
176
|
+
async function unstage(root, writes, created, linkFile) {
|
|
177
|
+
const kept = [];
|
|
178
|
+
for (const write of writes) {
|
|
179
|
+
if (write.temp === undefined)
|
|
180
|
+
continue;
|
|
181
|
+
const left = await removeOwned(write.temp, linkFile).catch(() => write.temp.path);
|
|
182
|
+
if (left !== undefined)
|
|
183
|
+
kept.push(path.relative(root, left));
|
|
184
|
+
}
|
|
185
|
+
for (const directory of [...created].reverse())
|
|
186
|
+
await rmdir(directory).catch(() => undefined);
|
|
187
|
+
return kept;
|
|
188
|
+
}
|
|
189
|
+
// Put the staged file at its destination without replacing anything there.
|
|
190
|
+
async function placeNew(root, write, linkFile) {
|
|
191
|
+
const taken = () => new Error(`${path.relative(root, write.destinationPath)} appeared while ${write.file.source} was migrated; both are kept`);
|
|
192
|
+
try {
|
|
193
|
+
await linkFile(write.temp.path, write.destinationPath);
|
|
194
|
+
// The temp file's name goes once every file is committed (commit), checked like any removal.
|
|
195
|
+
write.committed = write.temp.identity;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (write.committed !== undefined)
|
|
200
|
+
throw error;
|
|
201
|
+
if (isNodeError(error) && error.code === "EEXIST")
|
|
202
|
+
throw taken();
|
|
203
|
+
if (!(isNodeError(error) && NO_HARD_LINKS.has(error.code ?? "")))
|
|
204
|
+
throw error;
|
|
205
|
+
const copy = { path: write.destinationPath };
|
|
206
|
+
try {
|
|
207
|
+
await writeOwned(copy, write.text, write.sourceMode);
|
|
208
|
+
}
|
|
209
|
+
catch (copyError) {
|
|
210
|
+
if (copy.inode === undefined && isNodeError(copyError) && copyError.code === "EEXIST")
|
|
211
|
+
throw taken();
|
|
212
|
+
// A copy this run created but could not finish or read back is removed, if it is unchanged.
|
|
213
|
+
if (copy.inode !== undefined)
|
|
214
|
+
await removeOwned(copy, linkFile).catch(() => undefined);
|
|
215
|
+
throw copyError;
|
|
216
|
+
}
|
|
217
|
+
write.committed = copy.identity;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Every name that would collide with this write's destination, other than the files this run moves
|
|
221
|
+
// or writes. Checked at plan time and again just before each commit.
|
|
222
|
+
async function collisions(root, write, ours) {
|
|
223
|
+
if (!STUDY_DIRECTORIES.some((directory) => directory.relativeDir === path.dirname(write.file.destination)))
|
|
224
|
+
return [];
|
|
225
|
+
// Discovery reads only .yaml and .yml files, so a file without that extension shares no name.
|
|
226
|
+
const stem = studyFileStem(path.basename(write.file.destination));
|
|
227
|
+
if (stem === undefined)
|
|
228
|
+
return [];
|
|
229
|
+
return (await otherStudyFiles(stem, (candidate) => exists(path.join(root, candidate)), write.file.source)).filter((other) => !ours.has(other));
|
|
230
|
+
}
|
|
231
|
+
// Move a rewrite's source to its aside name, then check that the file moved is the one the plan
|
|
232
|
+
// read. An edit saved in between is given its name back, never overwritten.
|
|
233
|
+
async function moveAside(write, linkFile) {
|
|
234
|
+
await rename(write.sourcePath, write.asidePath);
|
|
235
|
+
write.movedAside = true;
|
|
236
|
+
const moved = await fileIdentity(write.asidePath).catch(() => undefined);
|
|
237
|
+
if (moved !== undefined && sameFile(moved, write.sourceIdentity))
|
|
238
|
+
return;
|
|
239
|
+
if (await restoreName(write.asidePath, write.sourcePath, linkFile))
|
|
240
|
+
write.movedAside = false;
|
|
241
|
+
throw new Error(`${write.file.source} changed after it was planned`);
|
|
242
|
+
}
|
|
243
|
+
// A rewrite's source that cannot get its name back is kept as <name>.v2.bak, or under its aside
|
|
244
|
+
// name when that is taken too. Returns where it is.
|
|
245
|
+
async function keepOriginal(write, linkFile) {
|
|
246
|
+
const kept = (await restoreName(write.asidePath, write.backupPath, linkFile).catch(() => false))
|
|
247
|
+
? write.backupPath
|
|
248
|
+
: write.asidePath;
|
|
249
|
+
write.movedAside = false;
|
|
250
|
+
return kept;
|
|
251
|
+
}
|
|
252
|
+
async function commit(root, writes, linkFile, onCommit) {
|
|
253
|
+
const ours = new Set(writes.flatMap((write) => [write.file.source, write.file.destination]));
|
|
254
|
+
try {
|
|
255
|
+
for (const write of writes) {
|
|
256
|
+
await assertDirectory(write.sourceDirectory, root);
|
|
257
|
+
await assertDirectory(write.destinationDirectory, root);
|
|
258
|
+
if (!sameFile(await fileIdentity(write.temp.path, true), write.temp.identity))
|
|
259
|
+
throw new Error(`the staged copy of ${write.file.source} changed`);
|
|
260
|
+
const other = (await collisions(root, write, ours))[0];
|
|
261
|
+
if (other !== undefined)
|
|
262
|
+
throw new Error(`${other} appeared with the name of ${write.file.destination}`);
|
|
263
|
+
if (!sameFile(await fileIdentity(write.sourcePath), write.sourceIdentity))
|
|
264
|
+
throw new Error(`${write.file.source} changed after it was planned`);
|
|
265
|
+
await onCommit?.(write.file.source);
|
|
266
|
+
if (write.file.action === "rewrite")
|
|
267
|
+
await moveAside(write, linkFile);
|
|
268
|
+
await placeNew(root, write, linkFile);
|
|
269
|
+
}
|
|
270
|
+
for (const write of writes) {
|
|
271
|
+
if (write.temp === undefined)
|
|
272
|
+
continue;
|
|
273
|
+
write.leftover = await removeOwned(write.temp, linkFile).catch(() => write.temp.path);
|
|
274
|
+
write.temp = undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
const unrestored = await undoCommits(root, writes, linkFile);
|
|
279
|
+
throw new MigrateError("commit", undefined, unrestored.length === 0
|
|
280
|
+
? `${error.message}. Every source is where it was.`
|
|
281
|
+
: `${error.message}. These files could not be put back: ${unrestored.join(", ")}.`, false, unrestored);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Undo each commit, last first: remove each new file that is still the one this run wrote, and give
|
|
285
|
+
// each rewritten source its name back. A name another file now holds is left alone, and the source
|
|
286
|
+
// is kept beside it. One failure does not stop the rest. Returns the paths it could not put back.
|
|
287
|
+
async function undoCommits(root, writes, linkFile) {
|
|
288
|
+
const unrestored = [];
|
|
289
|
+
const keep = (file) => unrestored.push(path.relative(root, file));
|
|
290
|
+
for (const write of [...writes].reverse()) {
|
|
291
|
+
try {
|
|
292
|
+
const committed = write.committed;
|
|
293
|
+
if (committed !== undefined) {
|
|
294
|
+
const kept = await removeChecked(write.destinationPath, (current) => sameFile(current, committed), linkFile);
|
|
295
|
+
write.committed = undefined;
|
|
296
|
+
if (kept !== undefined) {
|
|
297
|
+
keep(kept);
|
|
298
|
+
if (write.movedAside === true)
|
|
299
|
+
keep(await keepOriginal(write, linkFile));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (write.movedAside !== true)
|
|
304
|
+
continue;
|
|
305
|
+
if (await restoreName(write.asidePath, write.sourcePath, linkFile))
|
|
306
|
+
write.movedAside = false;
|
|
307
|
+
else
|
|
308
|
+
keep(await keepOriginal(write, linkFile));
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
keep(write.committed !== undefined
|
|
312
|
+
? write.destinationPath
|
|
313
|
+
: write.movedAside === true
|
|
314
|
+
? write.asidePath
|
|
315
|
+
: write.sourcePath);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return unrestored;
|
|
319
|
+
}
|
|
320
|
+
// Delete each moved source and each rewrite's original, but only once the v3 file is in place as
|
|
321
|
+
// written, and only if the old file is still the one the plan read (removeChecked). Anything kept
|
|
322
|
+
// is listed; a rewrite's original is kept as <name>.v2.bak.
|
|
323
|
+
async function cleanUp(root, writes, linkFile) {
|
|
324
|
+
const unresolved = [];
|
|
325
|
+
for (const write of writes) {
|
|
326
|
+
if (write.leftover !== undefined)
|
|
327
|
+
unresolved.push(path.relative(root, write.leftover));
|
|
328
|
+
const old = write.file.action === "move" ? write.sourcePath : write.asidePath;
|
|
329
|
+
let kept = old;
|
|
330
|
+
try {
|
|
331
|
+
const placed = await fileIdentity(write.destinationPath, true);
|
|
332
|
+
if (!sameFile(placed, write.committed))
|
|
333
|
+
throw new Error("the new file changed");
|
|
334
|
+
await assertDirectory(write.sourceDirectory, root);
|
|
335
|
+
// A rename keeps the inode, so a rewrite's original still has its source's identity.
|
|
336
|
+
kept = await removeChecked(old, (current) => sameFile(current, write.sourceIdentity), linkFile);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
kept = old;
|
|
340
|
+
}
|
|
341
|
+
if (kept === undefined)
|
|
342
|
+
continue;
|
|
343
|
+
if (kept === write.asidePath)
|
|
344
|
+
kept = await keepOriginal(write, linkFile);
|
|
345
|
+
unresolved.push(path.relative(root, kept));
|
|
346
|
+
}
|
|
347
|
+
return unresolved;
|
|
348
|
+
}
|
|
349
|
+
/** Convert the selected v2 study files to v3. */
|
|
350
|
+
export async function migrateStudies(options) {
|
|
351
|
+
const root = await realpath(path.resolve(options.cwd)).catch(() => path.resolve(options.cwd));
|
|
352
|
+
const base = { schema: MIGRATE_SCHEMA, cwd: root, dryRun: options.dryRun === true };
|
|
353
|
+
let files = [];
|
|
354
|
+
const created = [];
|
|
355
|
+
let writes = [];
|
|
356
|
+
const linkFile = options.link ?? link;
|
|
357
|
+
try {
|
|
358
|
+
({ files, writes } = await planWrites(root, options));
|
|
359
|
+
options.onPlan?.(files);
|
|
360
|
+
if (options.dryRun === true || writes.length === 0)
|
|
361
|
+
return { ...base, ok: true, files };
|
|
362
|
+
try {
|
|
363
|
+
await stage(root, writes, created);
|
|
364
|
+
options.onPhase?.("stage");
|
|
365
|
+
await commit(root, writes, linkFile, options.onCommit);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
const kept = await unstage(root, writes, created, linkFile);
|
|
369
|
+
if (kept.length > 0 && error instanceof MigrateError) {
|
|
370
|
+
throw new MigrateError(error.phase, error.file, `${error.message} Kept for you to check: ${kept.join(", ")}.`, error.refused, [...error.unresolved, ...kept]);
|
|
371
|
+
}
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
options.onPhase?.("commit");
|
|
375
|
+
const unresolved = await cleanUp(root, writes, linkFile);
|
|
376
|
+
if (unresolved.length > 0) {
|
|
377
|
+
return {
|
|
378
|
+
...base,
|
|
379
|
+
ok: false,
|
|
380
|
+
files,
|
|
381
|
+
unresolved,
|
|
382
|
+
error: {
|
|
383
|
+
code: "HUMANISH_MIGRATE_FAILED",
|
|
384
|
+
phase: "clean-up",
|
|
385
|
+
message: `Every study was written, but these files changed since the plan and were kept: ${unresolved.join(", ")}. Check them, then delete them.`,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return { ...base, ok: true, files };
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
const failure = error instanceof MigrateError
|
|
393
|
+
? error
|
|
394
|
+
: new MigrateError("plan", undefined, error.message);
|
|
395
|
+
return {
|
|
396
|
+
...base,
|
|
397
|
+
ok: false,
|
|
398
|
+
files,
|
|
399
|
+
...(failure.unresolved.length === 0 ? {} : { unresolved: failure.unresolved }),
|
|
400
|
+
error: {
|
|
401
|
+
code: failure.refused ? "HUMANISH_MIGRATE_REFUSED" : "HUMANISH_MIGRATE_FAILED",
|
|
402
|
+
phase: failure.phase,
|
|
403
|
+
...(failure.file === undefined ? {} : { file: failure.file }),
|
|
404
|
+
message: failure.message,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface FileIdentity {
|
|
2
|
+
readonly dev: bigint;
|
|
3
|
+
readonly ino: bigint;
|
|
4
|
+
readonly size: bigint;
|
|
5
|
+
readonly sha256: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DirectoryIdentity {
|
|
8
|
+
readonly physicalPath: string;
|
|
9
|
+
readonly dev: bigint;
|
|
10
|
+
readonly ino: bigint;
|
|
11
|
+
}
|
|
12
|
+
interface Inode {
|
|
13
|
+
readonly dev: bigint;
|
|
14
|
+
readonly ino: bigint;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A file this run creates. `inode` is set the moment the exclusive create succeeds, so a name that
|
|
18
|
+
* was already taken is never treated as this run's. `identity` is set once the bytes are written
|
|
19
|
+
* and read back through the same descriptor.
|
|
20
|
+
*/
|
|
21
|
+
export interface OwnedFile {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
inode?: Inode | undefined;
|
|
24
|
+
identity?: FileIdentity | undefined;
|
|
25
|
+
}
|
|
26
|
+
export type LinkFile = (from: string, to: string) => Promise<void>;
|
|
27
|
+
export declare function sha256(bytes: Buffer): string;
|
|
28
|
+
export declare function fileIdentity(absolutePath: string, ours?: boolean): Promise<FileIdentity>;
|
|
29
|
+
export declare function sameFile(left: FileIdentity, right: FileIdentity): boolean;
|
|
30
|
+
export declare function bindDirectory(root: string, relativeDir: string, created?: string[]): Promise<DirectoryIdentity | undefined>;
|
|
31
|
+
export declare function assertDirectory(directory: DirectoryIdentity, root: string): Promise<void>;
|
|
32
|
+
export declare function exists(absolutePath: string): Promise<boolean>;
|
|
33
|
+
export declare function writeOwned(owned: OwnedFile, bytes: Buffer | string, mode: number): Promise<void>;
|
|
34
|
+
export declare function removeChecked(file: string, matches: (current: FileIdentity) => boolean, linkFile: LinkFile): Promise<string | undefined>;
|
|
35
|
+
export declare function removeOwned(owned: OwnedFile, linkFile: LinkFile): Promise<string | undefined>;
|
|
36
|
+
export declare const NO_HARD_LINKS: Set<string>;
|
|
37
|
+
export declare function restoreName(from: string, to: string, linkFile: LinkFile): Promise<boolean>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// File operations for humanish migrate that never replace or delete a file the run did not create.
|
|
2
|
+
// A new name is taken with an exclusive create or link(), which fail when the name is in use. A
|
|
3
|
+
// file is removed by renaming it to a random name first and checking it there, so a file saved over
|
|
4
|
+
// the name in the meantime keeps it.
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { lstat, mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { isNodeError } from "../run/type-guards.js";
|
|
10
|
+
export function sha256(bytes) {
|
|
11
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
// A source must be a single-link regular file. A file this run wrote may briefly have two links
|
|
14
|
+
// while a move is committed, so `ours` relaxes that.
|
|
15
|
+
export async function fileIdentity(absolutePath, ours = false) {
|
|
16
|
+
const stats = await lstat(absolutePath, { bigint: true });
|
|
17
|
+
if (!stats.isFile() || (!ours && stats.nlink !== 1n))
|
|
18
|
+
throw new Error("not a single-link regular file");
|
|
19
|
+
return {
|
|
20
|
+
dev: stats.dev,
|
|
21
|
+
ino: stats.ino,
|
|
22
|
+
size: stats.size,
|
|
23
|
+
sha256: sha256(await readFile(absolutePath)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function sameFile(left, right) {
|
|
27
|
+
return (left.dev === right.dev &&
|
|
28
|
+
left.ino === right.ino &&
|
|
29
|
+
left.size === right.size &&
|
|
30
|
+
left.sha256 === right.sha256);
|
|
31
|
+
}
|
|
32
|
+
// Walk a directory below the project root one segment at a time, refusing a symbolic link or a
|
|
33
|
+
// non-directory. `create` makes missing segments and records each one it made.
|
|
34
|
+
export async function bindDirectory(root, relativeDir, created) {
|
|
35
|
+
let current = root;
|
|
36
|
+
for (const segment of relativeDir.split(path.sep).filter(Boolean)) {
|
|
37
|
+
current = path.join(current, segment);
|
|
38
|
+
let stats;
|
|
39
|
+
try {
|
|
40
|
+
stats = await lstat(current, { bigint: true });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (!(isNodeError(error) && error.code === "ENOENT") || created === undefined) {
|
|
44
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
45
|
+
return undefined;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
await mkdir(current, { mode: 0o755 });
|
|
49
|
+
created.push(current);
|
|
50
|
+
stats = await lstat(current, { bigint: true });
|
|
51
|
+
}
|
|
52
|
+
if (stats.isSymbolicLink() || !stats.isDirectory())
|
|
53
|
+
throw new Error(`${path.relative(root, current)} is not a plain directory`);
|
|
54
|
+
}
|
|
55
|
+
const stats = await lstat(current, { bigint: true });
|
|
56
|
+
return { physicalPath: current, dev: stats.dev, ino: stats.ino };
|
|
57
|
+
}
|
|
58
|
+
export async function assertDirectory(directory, root) {
|
|
59
|
+
const again = await bindDirectory(root, path.relative(root, directory.physicalPath));
|
|
60
|
+
if (again === undefined || again.dev !== directory.dev || again.ino !== directory.ino)
|
|
61
|
+
throw new Error(`${path.relative(root, directory.physicalPath)} changed`);
|
|
62
|
+
}
|
|
63
|
+
export async function exists(absolutePath) {
|
|
64
|
+
try {
|
|
65
|
+
await lstat(absolutePath);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Create a file exclusively and write it. Its inode is recorded the moment the create succeeds, so
|
|
75
|
+
// a failed write is still known to be this run's and a taken name never is. The bytes are read back
|
|
76
|
+
// through the same descriptor, so the identity recorded is this file's, whatever holds the name.
|
|
77
|
+
export async function writeOwned(owned, bytes, mode) {
|
|
78
|
+
const data = Buffer.from(bytes);
|
|
79
|
+
const handle = await open(owned.path, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, mode);
|
|
80
|
+
try {
|
|
81
|
+
const opened = await handle.stat({ bigint: true });
|
|
82
|
+
owned.inode = { dev: opened.dev, ino: opened.ino };
|
|
83
|
+
await handle.writeFile(data);
|
|
84
|
+
await handle.sync();
|
|
85
|
+
const stats = await handle.stat({ bigint: true });
|
|
86
|
+
const back = Buffer.alloc(Number(stats.size));
|
|
87
|
+
await handle.read(back, 0, back.length, 0);
|
|
88
|
+
owned.identity = { dev: stats.dev, ino: stats.ino, size: stats.size, sha256: sha256(back) };
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await handle.close();
|
|
92
|
+
}
|
|
93
|
+
if (owned.identity.sha256 !== sha256(data))
|
|
94
|
+
throw new Error(`${path.basename(owned.path)} did not read back as written`);
|
|
95
|
+
}
|
|
96
|
+
// Remove the file at `file` only if `matches` says it is the expected one. The name is first renamed
|
|
97
|
+
// to a random one and the file checked there, so a file saved over the name in between is never
|
|
98
|
+
// deleted: it gets its name back, or keeps the random one. Returns the path kept, or undefined once
|
|
99
|
+
// the file is removed or was already gone.
|
|
100
|
+
export async function removeChecked(file, matches, linkFile) {
|
|
101
|
+
const aside = `${file}.migrate-${randomUUID()}.del`;
|
|
102
|
+
try {
|
|
103
|
+
await rename(file, aside);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
107
|
+
return undefined;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
const current = await fileIdentity(aside, true).catch(() => undefined);
|
|
111
|
+
if (current !== undefined && matches(current)) {
|
|
112
|
+
await unlink(aside);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return (await restoreName(aside, file, linkFile)) ? file : aside;
|
|
116
|
+
}
|
|
117
|
+
// Remove a file this run created if it still holds this run's bytes, or was cut short while being
|
|
118
|
+
// written. Returns the path kept, if any.
|
|
119
|
+
export async function removeOwned(owned, linkFile) {
|
|
120
|
+
const inode = owned.inode;
|
|
121
|
+
if (inode === undefined)
|
|
122
|
+
return undefined;
|
|
123
|
+
return removeChecked(owned.path, (current) => current.dev === inode.dev &&
|
|
124
|
+
current.ino === inode.ino &&
|
|
125
|
+
(owned.identity === undefined || sameFile(current, owned.identity)), linkFile);
|
|
126
|
+
}
|
|
127
|
+
// Hard links need filesystem support. Without it a file is created exclusively and read back.
|
|
128
|
+
export const NO_HARD_LINKS = new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "EXDEV"]);
|
|
129
|
+
// Give the file at `from` the name `to` without replacing anything there: link() fails on a taken
|
|
130
|
+
// name, and so does the exclusive create a filesystem without hard links gets instead. True once
|
|
131
|
+
// `to` holds the file and `from` is gone. False when `to` was taken, or was replaced before `from`
|
|
132
|
+
// could go, which leaves `from` as the file's only name.
|
|
133
|
+
export async function restoreName(from, to, linkFile) {
|
|
134
|
+
let placed;
|
|
135
|
+
let copied;
|
|
136
|
+
let owned;
|
|
137
|
+
try {
|
|
138
|
+
await linkFile(from, to);
|
|
139
|
+
const stats = await lstat(from, { bigint: true });
|
|
140
|
+
placed = { dev: stats.dev, ino: stats.ino };
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (isNodeError(error) && error.code === "EEXIST")
|
|
144
|
+
return false;
|
|
145
|
+
if (!(isNodeError(error) && NO_HARD_LINKS.has(error.code ?? "")))
|
|
146
|
+
throw error;
|
|
147
|
+
// Without hard links the file is copied. What was copied is recorded, so an edit made to
|
|
148
|
+
// `from` during the copy keeps `from`.
|
|
149
|
+
const stats = await lstat(from, { bigint: true });
|
|
150
|
+
// Only a regular file is copied; anything else keeps the name it has.
|
|
151
|
+
if (!stats.isFile())
|
|
152
|
+
return false;
|
|
153
|
+
const bytes = await readFile(from);
|
|
154
|
+
copied = { dev: stats.dev, ino: stats.ino, size: BigInt(bytes.length), sha256: sha256(bytes) };
|
|
155
|
+
const copy = { path: to };
|
|
156
|
+
try {
|
|
157
|
+
await writeOwned(copy, bytes, Number(stats.mode & 511n));
|
|
158
|
+
}
|
|
159
|
+
catch (copyError) {
|
|
160
|
+
if (copy.inode === undefined && isNodeError(copyError) && copyError.code === "EEXIST")
|
|
161
|
+
return false;
|
|
162
|
+
if (copy.inode !== undefined)
|
|
163
|
+
await removeOwned(copy, linkFile).catch(() => undefined);
|
|
164
|
+
throw copyError;
|
|
165
|
+
}
|
|
166
|
+
placed = copy.inode;
|
|
167
|
+
owned = copy;
|
|
168
|
+
}
|
|
169
|
+
const now = await lstat(to, { bigint: true }).catch(() => undefined);
|
|
170
|
+
if (now === undefined || now.dev !== placed.dev || now.ino !== placed.ino)
|
|
171
|
+
return false;
|
|
172
|
+
if (copied === undefined) {
|
|
173
|
+
await unlink(from);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
const expected = copied;
|
|
177
|
+
if ((await removeChecked(from, (current) => sameFile(current, expected), linkFile)) === undefined)
|
|
178
|
+
return true;
|
|
179
|
+
// `from` changed during the copy: it stays, and the stale copy goes.
|
|
180
|
+
await removeOwned(owned, linkFile).catch(() => undefined);
|
|
181
|
+
return false;
|
|
182
|
+
}
|