oh-my-codex 0.20.1 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +10 -1
- package/dist/adapt/__tests__/hermes.test.js.map +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
- package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
- package/dist/auth/__tests__/hotswap.test.js +183 -0
- package/dist/auth/__tests__/hotswap.test.js.map +1 -0
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +17 -4
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/paths.js.map +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +4 -4
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +1 -1
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/capabilities/lockfile.js.map +1 -1
- package/dist/catalog/reader.js.map +1 -1
- package/dist/catalog/skill-mirror.js.map +1 -1
- package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/capabilities.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +650 -15
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +216 -11
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +49 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +84 -12
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +128 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +8 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +187 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +129 -0
- package/dist/cli/__tests__/ralplan.test.js.map +1 -0
- package/dist/cli/__tests__/resume.test.js +6 -1
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
- package/dist/cli/__tests__/setup-install-mode.test.js +2261 -14
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-preferences.test.js +79 -0
- package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
- package/dist/cli/__tests__/setup-refresh.test.js +33 -2
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +46 -3
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +37 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +61 -1
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +1771 -24
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +58 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/url.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/adapt.js.map +1 -1
- package/dist/cli/agents-init.js.map +1 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/ask.d.ts +1 -1
- package/dist/cli/ask.d.ts.map +1 -1
- package/dist/cli/ask.js.map +1 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch-intake.js.map +1 -1
- package/dist/cli/capabilities.js.map +1 -1
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +18 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +594 -130
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +11 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +108 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-serve.js.map +1 -1
- package/dist/cli/mission.js.map +1 -1
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +18 -0
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
- package/dist/cli/native-hook-claim-journal.js +239 -0
- package/dist/cli/native-hook-claim-journal.js.map +1 -0
- package/dist/cli/omx.js.map +1 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/question.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +3 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +14 -0
- package/dist/cli/ralplan.d.ts.map +1 -0
- package/dist/cli/ralplan.js +203 -0
- package/dist/cli/ralplan.js.map +1 -0
- package/dist/cli/setup-preferences.d.ts +11 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +32 -0
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +53 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1401 -398
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/star-prompt.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +2 -1
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +5 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +34 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +8 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +944 -95
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +8 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/url.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +5 -1
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +1287 -11
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +850 -32
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +6 -1
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +124 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +2142 -265
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +21 -16
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +991 -137
- package/dist/config/generator.js.map +1 -1
- package/dist/config/mcp-registry.js.map +1 -1
- package/dist/config/models.d.ts +2 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js.map +1 -1
- package/dist/config/omx-first-party-mcp.js.map +1 -1
- package/dist/config/team-mode.d.ts.map +1 -1
- package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
- package/dist/document-refresh/enforcer.js.map +1 -1
- package/dist/exec/followup.js.map +1 -1
- package/dist/goal-workflows/artifacts.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +1372 -2
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +14 -0
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +669 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +8 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +532 -11
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +5 -5
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/code-simplifier/index.js.map +1 -1
- package/dist/hooks/explore-routing.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/events.js.map +1 -1
- package/dist/hooks/extensibility/loader.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
- package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +33 -7
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +2228 -119
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts +12 -0
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +41 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-session-provenance.d.ts +87 -0
- package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
- package/dist/hooks/prompt-session-provenance.js +180 -0
- package/dist/hooks/prompt-session-provenance.js.map +1 -0
- package/dist/hooks/session.d.ts +139 -34
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1239 -317
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/task-size-detector.js.map +1 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +103 -1
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +26 -5
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +109 -3
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +98 -1
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +58 -58
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/hermes-server.d.ts +70 -70
- package/dist/mcp/hermes-server.d.ts.map +1 -1
- package/dist/mcp/lifecycle-telemetry.js.map +1 -1
- package/dist/mcp/memory-server.d.ts +34 -34
- package/dist/mcp/memory-server.d.ts.map +1 -1
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +14 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +96 -20
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +47 -47
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.d.ts +4 -4
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +23 -23
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +53 -1
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +5 -5
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
- package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/index.d.ts +4 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +3 -9
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/lifecycle-dedupe.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/session-registry.js.map +1 -1
- package/dist/notifications/session-status.js.map +1 -1
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/notifications/tmux-detector.js.map +1 -1
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/openclaw/config.js.map +1 -1
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +8 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/pipeline/stages/ultraqa.js.map +1 -1
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
- package/dist/planning/artifact-names.js.map +1 -1
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/client.d.ts.map +1 -1
- package/dist/question/client.js.map +1 -1
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/events.js.map +1 -1
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js.map +1 -1
- package/dist/question/ui.js.map +1 -1
- package/dist/ralph/contract.d.ts +1 -1
- package/dist/ralph/contract.d.ts.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +403 -8
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +112 -14
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +3 -3
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +155 -60
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +2 -2
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +1 -2
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/process-tree.js.map +1 -1
- package/dist/runtime/run-loop.js.map +1 -1
- package/dist/runtime/run-outcome.d.ts +3 -3
- package/dist/runtime/run-outcome.d.ts.map +1 -1
- package/dist/runtime/run-outcome.js.map +1 -1
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +4121 -592
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +205 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +175 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +848 -4
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
- package/dist/scripts/build-api.js.map +1 -1
- package/dist/scripts/build-explore-harness.js.map +1 -1
- package/dist/scripts/build-sparkshell.js.map +1 -1
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-execution-surface.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +689 -156
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/generate-release-body.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
- package/dist/scripts/hook-payload-guard.js.map +1 -1
- package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
- package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
- package/dist/scripts/notify-fallback-delivery.js +419 -0
- package/dist/scripts/notify-fallback-delivery.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +136 -30
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +39 -13
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +122 -12
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
- package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts +17 -0
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +73 -11
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -4
- package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +2 -2
- package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +25 -16
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.js +15 -13
- package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
- package/dist/scripts/notify-hook.d.ts +24 -1
- package/dist/scripts/notify-hook.d.ts.map +1 -1
- package/dist/scripts/notify-hook.js +266 -90
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +879 -2
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +1528 -7
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/test-reply-listener-live.js.map +1 -1
- package/dist/scripts/test-sparkshell.js.map +1 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/session-history/friction.js.map +1 -1
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
- package/dist/sidecar/collector.js.map +1 -1
- package/dist/sidecar/index.js.map +1 -1
- package/dist/sidecar/render.js.map +1 -1
- package/dist/sidecar/tmux.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +64 -15
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +19 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +61 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +49 -55
- package/dist/state/operations.js.map +1 -1
- package/dist/state/paths.d.ts +1 -1
- package/dist/state/paths.d.ts.map +1 -1
- package/dist/state/paths.js +1 -1
- package/dist/state/paths.js.map +1 -1
- package/dist/state/skill-active.d.ts +13 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +43 -4
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/terminal-normalization.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +20 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +2 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.js +988 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +273 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
- package/dist/subagents/__tests__/tracker.test.js +1550 -10
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/adapted-role-binding.d.ts +17 -0
- package/dist/subagents/adapted-role-binding.d.ts.map +1 -0
- package/dist/subagents/adapted-role-binding.js +75 -0
- package/dist/subagents/adapted-role-binding.js.map +1 -0
- package/dist/subagents/role-routing-marker.d.ts +10 -0
- package/dist/subagents/role-routing-marker.d.ts.map +1 -0
- package/dist/subagents/role-routing-marker.js +136 -0
- package/dist/subagents/role-routing-marker.js.map +1 -0
- package/dist/subagents/tracker.d.ts +172 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +1077 -61
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/approved-execution.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +231 -1
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +401 -1
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +181 -0
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +434 -4
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/api-interop.d.ts +2 -2
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/commit-hygiene.js.map +1 -1
- package/dist/team/contracts.d.ts +5 -5
- package/dist/team/contracts.d.ts.map +1 -1
- package/dist/team/delivery-log.js.map +1 -1
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/goal-workflow.js.map +1 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/model-contract.d.ts +20 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +267 -30
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/pane-status.js.map +1 -1
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/reminder-intents.d.ts +1 -1
- package/dist/team/reminder-intents.d.ts.map +1 -1
- package/dist/team/reminder-intents.js.map +1 -1
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +83 -52
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +9 -7
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +69 -28
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch-lock.js.map +1 -1
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/events.js.map +1 -1
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts.map +1 -1
- package/dist/team/team-identity.js.map +1 -1
- package/dist/team/tmux-session.d.ts +2 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +90 -42
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +4 -1
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/index.js.map +1 -1
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/utils/package.js.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/repo-deps.js.map +1 -1
- package/dist/utils/version.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/verifier.js.map +1 -1
- package/dist/visual/constants.d.ts +1 -1
- package/dist/visual/constants.d.ts.map +1 -1
- package/dist/vscode/__tests__/index.test.js.map +1 -1
- package/dist/vscode/index.js.map +1 -1
- package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
- package/dist/wiki/__tests__/lint.test.js.map +1 -1
- package/dist/wiki/__tests__/query.test.js.map +1 -1
- package/dist/wiki/__tests__/storage.test.js.map +1 -1
- package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
- package/dist/wiki/lint.js.map +1 -1
- package/dist/wiki/query.js.map +1 -1
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +4 -2
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -2
- package/skills/omx-setup/SKILL.md +10 -5
- package/skills/ralph/SKILL.md +4 -2
- package/skills/ralplan/SKILL.md +5 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +17522 -10630
- package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
- package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +215 -0
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +193 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +991 -4
- package/src/scripts/codex-native-hook.ts +779 -159
- package/src/scripts/notify-fallback-delivery.ts +416 -0
- package/src/scripts/notify-fallback-watcher.ts +132 -40
- package/src/scripts/notify-hook/auto-nudge.ts +43 -12
- package/src/scripts/notify-hook/managed-tmux.ts +149 -12
- package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
- package/src/scripts/notify-hook/state-io.ts +100 -13
- package/src/scripts/notify-hook/tmux-injection.ts +38 -21
- package/src/scripts/notify-hook/visual-verdict.ts +22 -12
- package/src/scripts/notify-hook.ts +335 -90
- package/src/scripts/smoke-packed-install.ts +1829 -6
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import {
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { buildManagedCodexHooksConfig } from '../../config/codex-hooks.js';
|
|
9
|
+
import { buildManagedCodexHookTrustState, buildManagedCodexHooksConfig, buildManagedCodexNativeHookWindowsShimContent, buildManagedCodexNativeHookWindowsShimPath, } from '../../config/codex-hooks.js';
|
|
10
|
+
import { uninstall } from '../uninstall.js';
|
|
10
11
|
import TOML from '@iarna/toml';
|
|
11
12
|
function runOmx(cwd, argv, envOverrides = {}) {
|
|
12
13
|
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -30,7 +31,45 @@ function runOmx(cwd, argv, envOverrides = {}) {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
function shouldSkipForSpawnPermissions(err) {
|
|
33
|
-
return
|
|
34
|
+
return /(EPERM|EACCES)/i.test(err);
|
|
35
|
+
}
|
|
36
|
+
async function withCwd(cwd, run) {
|
|
37
|
+
const previousCwd = process.cwd();
|
|
38
|
+
process.chdir(cwd);
|
|
39
|
+
try {
|
|
40
|
+
return await run();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
process.chdir(previousCwd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function withDriveQualifiedWindowsCodexHome(wd, run) {
|
|
47
|
+
const previousHome = process.env.HOME;
|
|
48
|
+
const previousCodexHome = process.env.CODEX_HOME;
|
|
49
|
+
const codexHomeDir = 'C:\\Users\\omx\\.codex';
|
|
50
|
+
process.env.HOME = wd;
|
|
51
|
+
process.env.CODEX_HOME = codexHomeDir;
|
|
52
|
+
try {
|
|
53
|
+
return await withCwd(wd, async () => {
|
|
54
|
+
await mkdir(codexHomeDir, { recursive: true });
|
|
55
|
+
return run(codexHomeDir);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (typeof previousHome === 'string')
|
|
60
|
+
process.env.HOME = previousHome;
|
|
61
|
+
else
|
|
62
|
+
delete process.env.HOME;
|
|
63
|
+
if (typeof previousCodexHome === 'string') {
|
|
64
|
+
process.env.CODEX_HOME = previousCodexHome;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
delete process.env.CODEX_HOME;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function packageRoot() {
|
|
72
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
34
73
|
}
|
|
35
74
|
/** Build a realistic OMX config.toml for testing */
|
|
36
75
|
function buildOmxConfig() {
|
|
@@ -358,6 +397,8 @@ describe('omx uninstall', () => {
|
|
|
358
397
|
managedBy: 'oh-my-codex',
|
|
359
398
|
version: 1,
|
|
360
399
|
previousNotify: ['node', staleDispatcher, '--metadata', metadataPath],
|
|
400
|
+
omxNotify: ['node', join(stalePkgRoot, 'dist', 'scripts', 'notify-hook.js')],
|
|
401
|
+
dispatcherNotify: ['node', staleDispatcher, '--metadata', metadataPath],
|
|
361
402
|
}));
|
|
362
403
|
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
363
404
|
if (shouldSkipForSpawnPermissions(res.error))
|
|
@@ -407,41 +448,1493 @@ describe('omx uninstall', () => {
|
|
|
407
448
|
await rm(wd, { recursive: true, force: true });
|
|
408
449
|
}
|
|
409
450
|
});
|
|
410
|
-
it('
|
|
411
|
-
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-'));
|
|
451
|
+
it('fails closed for unsupported root metadata in hooks.json', async () => {
|
|
452
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-invalid-hooks-root-'));
|
|
453
|
+
try {
|
|
454
|
+
const home = join(wd, 'home');
|
|
455
|
+
const codexDir = join(home, '.codex');
|
|
456
|
+
const configPath = join(codexDir, 'config.toml');
|
|
457
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
458
|
+
const config = buildOmxConfig().replace(/^hooks = true$/m, 'codex_hooks = true');
|
|
459
|
+
const hooks = JSON.stringify({
|
|
460
|
+
version: 1,
|
|
461
|
+
hooks: {
|
|
462
|
+
SessionStart: [{
|
|
463
|
+
hooks: [{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' }],
|
|
464
|
+
}],
|
|
465
|
+
},
|
|
466
|
+
}, null, 2) + '\n';
|
|
467
|
+
await mkdir(codexDir, { recursive: true });
|
|
468
|
+
await writeFile(configPath, config);
|
|
469
|
+
await writeFile(hooksPath, hooks);
|
|
470
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
471
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
472
|
+
return;
|
|
473
|
+
assert.equal(res.status, 1, res.stderr || res.stdout);
|
|
474
|
+
assert.match(res.stderr, /unknown root field version/);
|
|
475
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
476
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
await rm(wd, { recursive: true, force: true });
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
it('removes safely positioned managed wrappers while preserving foreign hooks and the native feature flag', async () => {
|
|
483
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-safe-foreign-hooks-'));
|
|
412
484
|
try {
|
|
413
485
|
const home = join(wd, 'home');
|
|
414
486
|
const codexDir = join(home, '.codex');
|
|
487
|
+
const configPath = join(codexDir, 'config.toml');
|
|
488
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
415
489
|
await mkdir(codexDir, { recursive: true });
|
|
416
|
-
await writeFile(
|
|
417
|
-
await writeFile(
|
|
490
|
+
await writeFile(configPath, buildOmxConfig().replace(/^hooks = true$/m, 'codex_hooks = true'));
|
|
491
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
418
492
|
hooks: {
|
|
419
493
|
SessionStart: [
|
|
420
|
-
{
|
|
494
|
+
{ hooks: [{ type: 'command', command: 'echo keep-me' }] },
|
|
495
|
+
{ matcher: 'startup|resume|clear', hooks: [{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' }] },
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
}, null, 2) + '\n');
|
|
499
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
500
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
501
|
+
return;
|
|
502
|
+
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
503
|
+
const hooks = await readFile(hooksPath, 'utf-8');
|
|
504
|
+
assert.match(hooks, /echo keep-me/);
|
|
505
|
+
assert.doesNotMatch(hooks, /codex-native-hook\.js/);
|
|
506
|
+
const config = await readFile(configPath, 'utf-8');
|
|
507
|
+
assert.match(config, /^hooks = true$/m);
|
|
508
|
+
assert.doesNotMatch(config, /^codex_hooks\s*=/m);
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
await rm(wd, { recursive: true, force: true });
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
it('fails closed without removing a shell-expanding foreign command that resembles an OMX hook', async () => {
|
|
515
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-shell-expanding-hook-'));
|
|
516
|
+
try {
|
|
517
|
+
const home = join(wd, 'home');
|
|
518
|
+
const codexDir = join(home, '.codex');
|
|
519
|
+
const configPath = join(codexDir, 'config.toml');
|
|
520
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
521
|
+
const config = buildOmxConfig();
|
|
522
|
+
const hooks = JSON.stringify({
|
|
523
|
+
hooks: {
|
|
524
|
+
SessionStart: [{
|
|
525
|
+
matcher: 'startup|resume|clear',
|
|
526
|
+
hooks: [{ type: 'command', command: 'node "$HOME/repo/dist/scripts/codex-native-hook.js"' }],
|
|
527
|
+
}],
|
|
528
|
+
},
|
|
529
|
+
}, null, 2) + '\n';
|
|
530
|
+
await mkdir(codexDir, { recursive: true });
|
|
531
|
+
await writeFile(configPath, config);
|
|
532
|
+
await writeFile(hooksPath, hooks);
|
|
533
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
534
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
535
|
+
return;
|
|
536
|
+
assert.equal(res.status, 1, res.stderr || res.stdout);
|
|
537
|
+
assert.match(res.stderr, /ambiguous_managed_handler|does not match the managed command grammar/i);
|
|
538
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
539
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
await rm(wd, { recursive: true, force: true });
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
it('fails closed without cleaning config when managed removal would shift a foreign handler', async () => {
|
|
546
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-unsafe-foreign-hooks-'));
|
|
547
|
+
try {
|
|
548
|
+
const home = join(wd, 'home');
|
|
549
|
+
const codexDir = join(home, '.codex');
|
|
550
|
+
const configPath = join(codexDir, 'config.toml');
|
|
551
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
552
|
+
const config = buildOmxConfig();
|
|
553
|
+
const hooks = JSON.stringify({
|
|
554
|
+
hooks: {
|
|
555
|
+
SessionStart: [{
|
|
556
|
+
matcher: 'startup|resume|clear',
|
|
421
557
|
hooks: [
|
|
422
558
|
{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' },
|
|
423
559
|
{ type: 'command', command: 'echo keep-me' },
|
|
424
560
|
],
|
|
561
|
+
}],
|
|
562
|
+
},
|
|
563
|
+
}, null, 2) + '\n';
|
|
564
|
+
await mkdir(codexDir, { recursive: true });
|
|
565
|
+
await writeFile(configPath, config);
|
|
566
|
+
await writeFile(hooksPath, hooks);
|
|
567
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
568
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
569
|
+
return;
|
|
570
|
+
assert.equal(res.status, 1, res.stderr || res.stdout);
|
|
571
|
+
assert.match(res.stderr, /unsafe_managed_removal|shift a foreign coordinate/i);
|
|
572
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
573
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
await rm(wd, { recursive: true, force: true });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
it('fails before uninstall writes when marker-wrapped trust state has foreign siblings', async () => {
|
|
580
|
+
const representations = [
|
|
581
|
+
{
|
|
582
|
+
name: 'inline',
|
|
583
|
+
render: (key, hash) => [
|
|
584
|
+
`hooks = { state = { ${JSON.stringify(key)} = { trusted_hash = ${JSON.stringify(hash)}, foreign = true } } }`,
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: 'dotted',
|
|
589
|
+
render: (key, hash) => [
|
|
590
|
+
`hooks.state.${JSON.stringify(key)}.trusted_hash = ${JSON.stringify(hash)}`,
|
|
591
|
+
`hooks.state.${JSON.stringify(key)}.foreign = true`,
|
|
592
|
+
],
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: 'table',
|
|
596
|
+
render: (key, hash) => [
|
|
597
|
+
`[hooks.state.${JSON.stringify(key)}]`,
|
|
598
|
+
`trusted_hash = ${JSON.stringify(hash)}`,
|
|
599
|
+
'foreign = true',
|
|
600
|
+
],
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: 'separate-foreign-table',
|
|
604
|
+
render: (key, hash) => [
|
|
605
|
+
`[hooks.state.${JSON.stringify(key)}]`,
|
|
606
|
+
`trusted_hash = ${JSON.stringify(hash)}`,
|
|
607
|
+
'',
|
|
608
|
+
'[hooks.state."foreign-key"]',
|
|
609
|
+
'trusted_hash = "sha256:foreign"',
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
];
|
|
613
|
+
for (const representation of representations) {
|
|
614
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-trust-${representation.name}-`));
|
|
615
|
+
try {
|
|
616
|
+
await withCwd(wd, async () => {
|
|
617
|
+
const codexDir = join(wd, '.codex');
|
|
618
|
+
const configPath = join(codexDir, 'config.toml');
|
|
619
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
620
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
621
|
+
const metadataPath = join(codexDir, '.omx', 'notify-dispatch.json');
|
|
622
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
623
|
+
platform: 'win32',
|
|
624
|
+
codexHomeDir: codexDir,
|
|
625
|
+
}), null, 2)}\n`;
|
|
626
|
+
const [key, trust] = Object.entries(buildManagedCodexHookTrustState(hooksPath, packageRoot(), {
|
|
627
|
+
platform: 'win32',
|
|
628
|
+
codexHomeDir: codexDir,
|
|
629
|
+
hooksContent: hooks,
|
|
630
|
+
}))[0] ?? [];
|
|
631
|
+
assert.ok(key);
|
|
632
|
+
assert.ok(trust);
|
|
633
|
+
const config = [
|
|
634
|
+
'model = "foreign"',
|
|
635
|
+
'',
|
|
636
|
+
'# ============================================================',
|
|
637
|
+
'# oh-my-codex (OMX) Configuration',
|
|
638
|
+
'# Managed by omx setup - manual edits preserved on next setup',
|
|
639
|
+
'# ============================================================',
|
|
640
|
+
'',
|
|
641
|
+
...representation.render(key, trust.trusted_hash),
|
|
642
|
+
'',
|
|
643
|
+
'# ============================================================',
|
|
644
|
+
'# End oh-my-codex',
|
|
645
|
+
'',
|
|
646
|
+
].join('\n');
|
|
647
|
+
const metadata = Buffer.from('{"managedBy":"foreign"}\n', 'utf-8');
|
|
648
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot()), 'utf-8');
|
|
649
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
650
|
+
await writeFile(configPath, config);
|
|
651
|
+
await writeFile(hooksPath, hooks);
|
|
652
|
+
await writeFile(shimPath, shim);
|
|
653
|
+
await writeFile(metadataPath, metadata);
|
|
654
|
+
await assert.rejects(uninstall({ scope: 'project', transactionPlatform: 'win32' }), (error) => typeof error === 'object' &&
|
|
655
|
+
error !== null &&
|
|
656
|
+
'code' in error &&
|
|
657
|
+
error.code === 'managed_trust_key_conflict');
|
|
658
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, 'utf-8'));
|
|
659
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(hooks, 'utf-8'));
|
|
660
|
+
assert.deepEqual(await readFile(shimPath), shim);
|
|
661
|
+
assert.deepEqual(await readFile(metadataPath), metadata);
|
|
662
|
+
assert.deepEqual((await readdir(codexDir)).filter((entry) => entry.includes('.omx-')), []);
|
|
663
|
+
assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes('.omx-')), []);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
await rm(wd, { recursive: true, force: true });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
it('fails before uninstall writes when marker trust has absent or non-managed hooks expectations', async () => {
|
|
672
|
+
const fixtures = [
|
|
673
|
+
{ name: 'absent', hooks: null },
|
|
674
|
+
{
|
|
675
|
+
name: 'non-managed',
|
|
676
|
+
hooks: `${JSON.stringify({
|
|
677
|
+
hooks: {
|
|
678
|
+
UserPromptSubmit: [{ hooks: [{ type: 'command', command: 'echo foreign' }] }],
|
|
679
|
+
},
|
|
680
|
+
}, null, 2)}\n`,
|
|
681
|
+
},
|
|
682
|
+
];
|
|
683
|
+
for (const fixture of fixtures) {
|
|
684
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-marker-${fixture.name}-`));
|
|
685
|
+
try {
|
|
686
|
+
await withCwd(wd, async () => {
|
|
687
|
+
const codexDir = join(wd, '.codex');
|
|
688
|
+
const configPath = join(codexDir, 'config.toml');
|
|
689
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
690
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
691
|
+
const metadataPath = join(codexDir, '.omx', 'notify-dispatch.json');
|
|
692
|
+
const config = buildOmxConfig().replace('# End oh-my-codex', '[hooks.state."foreign-key"]\ntrusted_hash = "sha256:foreign"\n\n# End oh-my-codex');
|
|
693
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot()), 'utf-8');
|
|
694
|
+
const metadata = Buffer.from('{"managedBy":"foreign"}\n', 'utf-8');
|
|
695
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
696
|
+
await writeFile(configPath, config);
|
|
697
|
+
if (fixture.hooks !== null)
|
|
698
|
+
await writeFile(hooksPath, fixture.hooks);
|
|
699
|
+
await writeFile(shimPath, shim);
|
|
700
|
+
await writeFile(metadataPath, metadata);
|
|
701
|
+
await assert.rejects(uninstall({ scope: 'project', transactionPlatform: 'win32' }), (error) => typeof error === 'object' &&
|
|
702
|
+
error !== null &&
|
|
703
|
+
'code' in error &&
|
|
704
|
+
error.code === 'managed_trust_key_conflict');
|
|
705
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, 'utf-8'));
|
|
706
|
+
assert.deepEqual(await readFile(shimPath), shim);
|
|
707
|
+
assert.deepEqual(await readFile(metadataPath), metadata);
|
|
708
|
+
if (fixture.hooks === null) {
|
|
709
|
+
assert.equal(existsSync(hooksPath), false);
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(fixture.hooks, 'utf-8'));
|
|
713
|
+
}
|
|
714
|
+
assert.deepEqual((await readdir(codexDir)).filter((entry) => entry.includes('.omx-')), []);
|
|
715
|
+
assert.deepEqual((await readdir(wd)).filter((entry) => entry.includes('.omx-')), []);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
finally {
|
|
719
|
+
await rm(wd, { recursive: true, force: true });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
it('preserves a shim referenced by a Unicode-escaped future hook event', async () => {
|
|
724
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-future-shim-'));
|
|
725
|
+
try {
|
|
726
|
+
await withCwd(wd, async () => {
|
|
727
|
+
const codexDir = join(wd, '.codex');
|
|
728
|
+
const configPath = join(codexDir, 'config.toml');
|
|
729
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
730
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
731
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
732
|
+
platform: 'win32',
|
|
733
|
+
codexHomeDir: codexDir,
|
|
734
|
+
});
|
|
735
|
+
const sentinel = '__OMX_ESCAPED_SHIM_COMMAND__';
|
|
736
|
+
const hooks = JSON.stringify({
|
|
737
|
+
...managed,
|
|
738
|
+
hooks: {
|
|
739
|
+
...managed.hooks,
|
|
740
|
+
FutureEvent: [{ hooks: [{ type: 'command', command: sentinel }] }],
|
|
741
|
+
},
|
|
742
|
+
}, null, 2).replace(JSON.stringify(sentinel), JSON.stringify(`& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimPath}'`).replace(/\\\\/g, '\\u005c')) + '\n';
|
|
743
|
+
await mkdir(codexDir, { recursive: true });
|
|
744
|
+
await writeFile(configPath, buildOmxConfig());
|
|
745
|
+
await writeFile(hooksPath, hooks);
|
|
746
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
747
|
+
await uninstall({ scope: 'project', transactionPlatform: 'win32' });
|
|
748
|
+
assert.equal(existsSync(shimPath), true);
|
|
749
|
+
const finalHooks = await readFile(hooksPath, 'utf-8');
|
|
750
|
+
assert.match(finalHooks, /FutureEvent/);
|
|
751
|
+
assert.match(finalHooks, /\\u005c/);
|
|
752
|
+
assert.doesNotMatch(finalHooks, /codex-native-hook\.js/);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
await rm(wd, { recursive: true, force: true });
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
it('preserves a drive-qualified shim referenced by a drive-less future hook event', async () => {
|
|
760
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-drive-qualified-future-shim-'));
|
|
761
|
+
try {
|
|
762
|
+
await withDriveQualifiedWindowsCodexHome(wd, async (codexHomeDir) => {
|
|
763
|
+
const configPath = join(codexHomeDir, 'config.toml');
|
|
764
|
+
const hooksPath = join(codexHomeDir, 'hooks.json');
|
|
765
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexHomeDir);
|
|
766
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
767
|
+
platform: 'win32',
|
|
768
|
+
codexHomeDir,
|
|
769
|
+
});
|
|
770
|
+
const driveLessFutureShimPath = '\\Users\\omx\\.codex\\hooks\\omx-native-hook-windows-shim.ps1';
|
|
771
|
+
const hooks = `${JSON.stringify({
|
|
772
|
+
...managed,
|
|
773
|
+
hooks: {
|
|
774
|
+
...managed.hooks,
|
|
775
|
+
FutureEvent: [{
|
|
776
|
+
hooks: [{
|
|
777
|
+
type: 'command',
|
|
778
|
+
command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${driveLessFutureShimPath}'`,
|
|
779
|
+
}],
|
|
780
|
+
}],
|
|
781
|
+
},
|
|
782
|
+
}, null, 2)}\n`;
|
|
783
|
+
await writeFile(configPath, buildOmxConfig());
|
|
784
|
+
await writeFile(hooksPath, hooks);
|
|
785
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
786
|
+
await uninstall({ scope: 'user', transactionPlatform: 'win32' });
|
|
787
|
+
assert.equal(existsSync(shimPath), true);
|
|
788
|
+
const finalHooks = await readFile(hooksPath, 'utf-8');
|
|
789
|
+
assert.match(finalHooks, /FutureEvent/);
|
|
790
|
+
assert.doesNotMatch(finalHooks, /codex-native-hook\.js/);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
finally {
|
|
794
|
+
await rm(wd, { recursive: true, force: true });
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
it('preserves a shim referenced through a normalized Windows path alias', async () => {
|
|
798
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-shim-path-alias-'));
|
|
799
|
+
try {
|
|
800
|
+
await withCwd(wd, async () => {
|
|
801
|
+
const codexDir = join(wd, '.codex');
|
|
802
|
+
const configPath = join(codexDir, 'config.toml');
|
|
803
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
804
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
805
|
+
const shimAlias = shimPath
|
|
806
|
+
.replace(/[\\/]+hooks[\\/]+/i, '\\hooks\\\\.\\')
|
|
807
|
+
.replace(/omx-native-hook-windows-shim\.ps1$/i, 'OMX-NATIVE-HOOK-WINDOWS-SHIM.PS1')
|
|
808
|
+
.replace(/\\/g, '/');
|
|
809
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
810
|
+
platform: 'win32',
|
|
811
|
+
codexHomeDir: codexDir,
|
|
812
|
+
});
|
|
813
|
+
const hooks = `${JSON.stringify({
|
|
814
|
+
...managed,
|
|
815
|
+
hooks: {
|
|
816
|
+
...managed.hooks,
|
|
817
|
+
FutureEvent: [{
|
|
818
|
+
hooks: [{
|
|
819
|
+
type: 'command',
|
|
820
|
+
command: `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimAlias}'`,
|
|
821
|
+
}],
|
|
822
|
+
}],
|
|
823
|
+
},
|
|
824
|
+
}, null, 2)}\n`;
|
|
825
|
+
await mkdir(codexDir, { recursive: true });
|
|
826
|
+
await writeFile(configPath, buildOmxConfig());
|
|
827
|
+
await writeFile(hooksPath, hooks);
|
|
828
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
829
|
+
await uninstall({ scope: 'project', transactionPlatform: 'win32' });
|
|
830
|
+
assert.equal(existsSync(shimPath), true);
|
|
831
|
+
assert.match(await readFile(hooksPath, 'utf-8'), /FutureEvent/);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
finally {
|
|
835
|
+
await rm(wd, { recursive: true, force: true });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
it('preserves a shim for a distinct absolute -File target', async () => {
|
|
839
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-quoted-inert-shim-'));
|
|
840
|
+
try {
|
|
841
|
+
await withCwd(wd, async () => {
|
|
842
|
+
const codexDir = join(wd, '.codex');
|
|
843
|
+
const configPath = join(codexDir, 'config.toml');
|
|
844
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
845
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
846
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
847
|
+
platform: 'win32',
|
|
848
|
+
codexHomeDir: codexDir,
|
|
849
|
+
});
|
|
850
|
+
const hooks = `${JSON.stringify({
|
|
851
|
+
...managed,
|
|
852
|
+
hooks: {
|
|
853
|
+
...managed.hooks,
|
|
854
|
+
FutureEvent: [{
|
|
855
|
+
hooks: [{
|
|
856
|
+
type: 'command',
|
|
857
|
+
command: "& 'C:\\%OMX_INERT%\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\tools\\unrelated-$env:INERT.ps1'",
|
|
858
|
+
}],
|
|
859
|
+
}],
|
|
860
|
+
},
|
|
861
|
+
}, null, 2)}\n`;
|
|
862
|
+
await mkdir(codexDir, { recursive: true });
|
|
863
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
864
|
+
await writeFile(configPath, buildOmxConfig());
|
|
865
|
+
await writeFile(hooksPath, hooks);
|
|
866
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
867
|
+
await uninstall({ scope: 'project', transactionPlatform: 'win32' });
|
|
868
|
+
assert.equal(existsSync(shimPath), true);
|
|
869
|
+
assert.match(await readFile(hooksPath, 'utf-8'), /FutureEvent/);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
finally {
|
|
873
|
+
await rm(wd, { recursive: true, force: true });
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
it('treats exact and all ambiguous preserved shim references as read-only transaction preconditions', async () => {
|
|
877
|
+
const fixtures = [
|
|
878
|
+
{ name: 'normalized-alias-deletion', reference: 'exact', mutation: 'delete' },
|
|
879
|
+
{ name: 'basename-ambiguity-replacement', reference: 'basename', mutation: 'replace' },
|
|
880
|
+
{ name: 'environment-indirection-replacement', reference: 'environment', mutation: 'replace' },
|
|
881
|
+
{ name: 'alternate-absolute-alias-deletion', reference: 'alternate-absolute', mutation: 'delete' },
|
|
882
|
+
{ name: 'rooted-short-name-deletion', reference: 'rooted-short-name', mutation: 'delete' },
|
|
883
|
+
{ name: 'drive-relative-short-name-deletion', reference: 'drive-relative-short-name', mutation: 'delete' },
|
|
884
|
+
{ name: 'distinct-basename-absolute-deletion', reference: 'distinct-basename-absolute', mutation: 'delete' },
|
|
885
|
+
];
|
|
886
|
+
for (const fixture of fixtures) {
|
|
887
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-preserved-shim-${fixture.name}-`));
|
|
888
|
+
try {
|
|
889
|
+
await withCwd(wd, async () => {
|
|
890
|
+
const codexDir = join(wd, '.codex');
|
|
891
|
+
const configPath = join(codexDir, 'config.toml');
|
|
892
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
893
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
894
|
+
const shimAlias = shimPath
|
|
895
|
+
.replace(/[\\/]+hooks[\\/]+/i, '\\hooks\\\\.\\')
|
|
896
|
+
.replace(/omx-native-hook-windows-shim\.ps1$/i, 'OMX-NATIVE-HOOK-WINDOWS-SHIM.PS1')
|
|
897
|
+
.replace(/\\/g, '/');
|
|
898
|
+
const command = fixture.reference === 'exact'
|
|
899
|
+
? `& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '${shimAlias}'`
|
|
900
|
+
: fixture.reference === 'environment'
|
|
901
|
+
? '& $env:OMX_SHIM'
|
|
902
|
+
: fixture.reference === 'alternate-absolute'
|
|
903
|
+
? "& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\aliases\\omx-native-hook-windows-shim.ps1'"
|
|
904
|
+
: fixture.reference === 'rooted-short-name'
|
|
905
|
+
? "& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File '\\Users\\ALICE~1\\.codex\\hooks\\OMX-NA~1.PS1'"
|
|
906
|
+
: fixture.reference === 'drive-relative-short-name'
|
|
907
|
+
? "& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'C:Users\\ALICE~1\\.codex\\hooks\\OMX-NA~1.PS1'"
|
|
908
|
+
: fixture.reference === 'distinct-basename-absolute'
|
|
909
|
+
? "& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' -NoProfile -ExecutionPolicy Bypass -File 'D:\\aliases\\foreign-hook-alias.ps1'"
|
|
910
|
+
: 'echo omx-native-hook-windows-shim.ps1';
|
|
911
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
912
|
+
platform: 'win32',
|
|
913
|
+
codexHomeDir: codexDir,
|
|
914
|
+
});
|
|
915
|
+
const hooks = `${JSON.stringify({
|
|
916
|
+
...managed,
|
|
917
|
+
hooks: {
|
|
918
|
+
...managed.hooks,
|
|
919
|
+
FutureEvent: [{ hooks: [{ type: 'command', command }] }],
|
|
425
920
|
},
|
|
426
|
-
|
|
921
|
+
}, null, 2)}\n`;
|
|
922
|
+
const config = buildOmxConfig();
|
|
923
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot()), 'utf-8');
|
|
924
|
+
const replacement = Buffer.from('concurrent shim replacement\n', 'utf-8');
|
|
925
|
+
await mkdir(codexDir, { recursive: true });
|
|
926
|
+
await writeFile(configPath, config);
|
|
927
|
+
await writeFile(hooksPath, hooks);
|
|
928
|
+
await writeFile(shimPath, shim);
|
|
929
|
+
await assert.rejects(uninstall({
|
|
930
|
+
scope: 'project',
|
|
931
|
+
transactionPlatform: 'win32',
|
|
932
|
+
transactionFailureInjector: async (stage) => {
|
|
933
|
+
if (stage !== 'before-config-commit')
|
|
934
|
+
return;
|
|
935
|
+
if (fixture.mutation === 'delete') {
|
|
936
|
+
await rm(shimPath);
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
await writeFile(shimPath, replacement);
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
943
|
+
assert.deepEqual(await readFile(configPath), Buffer.from(config, 'utf-8'));
|
|
944
|
+
assert.deepEqual(await readFile(hooksPath), Buffer.from(hooks, 'utf-8'));
|
|
945
|
+
if (fixture.mutation === 'delete') {
|
|
946
|
+
assert.equal(existsSync(shimPath), false);
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
assert.deepEqual(await readFile(shimPath), replacement);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
await rm(wd, { recursive: true, force: true });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
it('rolls back hooks when a preserved environment-indirected shim drifts under --keep-config', async () => {
|
|
959
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-keep-config-preserved-shim-'));
|
|
960
|
+
try {
|
|
961
|
+
await withCwd(wd, async () => {
|
|
962
|
+
const codexDir = join(wd, '.codex');
|
|
963
|
+
const configPath = join(codexDir, 'config.toml');
|
|
964
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
965
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
966
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
967
|
+
platform: 'win32',
|
|
968
|
+
codexHomeDir: codexDir,
|
|
969
|
+
});
|
|
970
|
+
const hooks = `${JSON.stringify({
|
|
971
|
+
...managed,
|
|
972
|
+
hooks: {
|
|
973
|
+
...managed.hooks,
|
|
974
|
+
FutureEvent: [{ hooks: [{ type: 'command', command: '& $env:OMX_SHIM' }] }],
|
|
975
|
+
},
|
|
976
|
+
}, null, 2)}\n`;
|
|
977
|
+
const config = buildOmxConfig();
|
|
978
|
+
const replacement = Buffer.from('concurrent preserved shim replacement\n', 'utf-8');
|
|
979
|
+
await mkdir(codexDir, { recursive: true });
|
|
980
|
+
await writeFile(configPath, config);
|
|
981
|
+
await writeFile(hooksPath, hooks);
|
|
982
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
983
|
+
let injected = false;
|
|
984
|
+
await assert.rejects(uninstall({
|
|
985
|
+
scope: 'project',
|
|
986
|
+
keepConfig: true,
|
|
987
|
+
transactionPlatform: 'win32',
|
|
988
|
+
transactionFailureInjector: async (stage) => {
|
|
989
|
+
if (stage !== 'before-rename' || injected)
|
|
990
|
+
return;
|
|
991
|
+
injected = true;
|
|
992
|
+
await writeFile(shimPath, replacement);
|
|
993
|
+
},
|
|
994
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
995
|
+
assert.equal(injected, true);
|
|
996
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
997
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
998
|
+
assert.deepEqual(await readFile(shimPath), replacement);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
finally {
|
|
1002
|
+
await rm(wd, { recursive: true, force: true });
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
it('rolls back when a preserved Windows shim drifts after staged-cleanup finalization', async () => {
|
|
1006
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-preserved-shim-finalization-'));
|
|
1007
|
+
try {
|
|
1008
|
+
await withCwd(wd, async () => {
|
|
1009
|
+
const codexDir = join(wd, '.codex');
|
|
1010
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1011
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1012
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1013
|
+
const managed = buildManagedCodexHooksConfig(packageRoot(), {
|
|
1014
|
+
platform: 'win32',
|
|
1015
|
+
codexHomeDir: codexDir,
|
|
1016
|
+
});
|
|
1017
|
+
const hooks = `${JSON.stringify({
|
|
1018
|
+
...managed,
|
|
1019
|
+
hooks: {
|
|
1020
|
+
...managed.hooks,
|
|
1021
|
+
FutureEvent: [{ hooks: [{ type: 'command', command: '& $env:OMX_SHIM' }] }],
|
|
1022
|
+
},
|
|
1023
|
+
}, null, 2)}\n`;
|
|
1024
|
+
const config = buildOmxConfig();
|
|
1025
|
+
const replacement = Buffer.from('concurrent finalization shim replacement\n', 'utf-8');
|
|
1026
|
+
await mkdir(codexDir, { recursive: true });
|
|
1027
|
+
await writeFile(configPath, config);
|
|
1028
|
+
await writeFile(hooksPath, hooks);
|
|
1029
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
1030
|
+
let injected = false;
|
|
1031
|
+
await assert.rejects(uninstall({
|
|
1032
|
+
scope: 'project',
|
|
1033
|
+
transactionPlatform: 'win32',
|
|
1034
|
+
transactionFailureInjector: async (stage) => {
|
|
1035
|
+
if (stage !== 'after-staged-cleanup' || injected)
|
|
1036
|
+
return;
|
|
1037
|
+
injected = true;
|
|
1038
|
+
await writeFile(shimPath, replacement);
|
|
1039
|
+
},
|
|
1040
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1041
|
+
assert.equal(injected, true);
|
|
1042
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1043
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1044
|
+
assert.deepEqual(await readFile(shimPath), replacement);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
finally {
|
|
1048
|
+
await rm(wd, { recursive: true, force: true });
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
it('rolls back hooks, historical proof-owned shim, and config when shim removal fails', async () => {
|
|
1052
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-shim-rollback-'));
|
|
1053
|
+
try {
|
|
1054
|
+
await withCwd(wd, async () => {
|
|
1055
|
+
const codexDir = join(wd, '.codex');
|
|
1056
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1057
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1058
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1059
|
+
const config = buildOmxConfig();
|
|
1060
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1061
|
+
platform: 'win32',
|
|
1062
|
+
codexHomeDir: codexDir,
|
|
1063
|
+
}), null, 2)}\n`;
|
|
1064
|
+
const shim = buildManagedCodexNativeHookWindowsShimContent('C:\\Historical Install\\oh-my-codex', { nodePath: 'D:\\Historical Node\\node.exe' });
|
|
1065
|
+
await mkdir(codexDir, { recursive: true });
|
|
1066
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1067
|
+
await writeFile(configPath, config);
|
|
1068
|
+
await writeFile(hooksPath, hooks);
|
|
1069
|
+
await writeFile(shimPath, shim);
|
|
1070
|
+
await assert.rejects(uninstall({
|
|
1071
|
+
scope: 'project',
|
|
1072
|
+
transactionPlatform: 'win32',
|
|
1073
|
+
transactionFailureInjector: async (stage) => {
|
|
1074
|
+
if (stage !== 'before-shim-removal')
|
|
1075
|
+
return;
|
|
1076
|
+
assert.equal(existsSync(hooksPath), false, 'hooks must mutate before shim removal');
|
|
1077
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1078
|
+
throw new Error('simulated shim removal failure');
|
|
1079
|
+
},
|
|
1080
|
+
}), /simulated shim removal failure/);
|
|
1081
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1082
|
+
assert.equal(await readFile(shimPath, 'utf-8'), shim);
|
|
1083
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
await rm(wd, { recursive: true, force: true });
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
it('rejects a modified Windows shim before any uninstall artifact write', async () => {
|
|
1091
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-modified-shim-'));
|
|
1092
|
+
try {
|
|
1093
|
+
await withCwd(wd, async () => {
|
|
1094
|
+
const codexDir = join(wd, '.codex');
|
|
1095
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1096
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1097
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1098
|
+
const config = buildOmxConfig();
|
|
1099
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1100
|
+
platform: 'win32',
|
|
1101
|
+
codexHomeDir: codexDir,
|
|
1102
|
+
}), null, 2)}\n`;
|
|
1103
|
+
const shim = `${buildManagedCodexNativeHookWindowsShimContent(packageRoot())}\n# user edit\n`;
|
|
1104
|
+
await mkdir(codexDir, { recursive: true });
|
|
1105
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1106
|
+
await writeFile(configPath, config);
|
|
1107
|
+
await writeFile(hooksPath, hooks);
|
|
1108
|
+
await writeFile(shimPath, shim);
|
|
1109
|
+
await assert.rejects(uninstall({ scope: 'project', transactionPlatform: 'win32' }), /modified native hook Windows shim/);
|
|
1110
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1111
|
+
assert.equal(await readFile(shimPath, 'utf-8'), shim);
|
|
1112
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
await rm(wd, { recursive: true, force: true });
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
it('rolls back hooks, shim, and config when config commit fails after hook mutation', async () => {
|
|
1120
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-config-rollback-'));
|
|
1121
|
+
try {
|
|
1122
|
+
await withCwd(wd, async () => {
|
|
1123
|
+
const codexDir = join(wd, '.codex');
|
|
1124
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1125
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1126
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1127
|
+
const config = buildOmxConfig();
|
|
1128
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1129
|
+
platform: 'win32',
|
|
1130
|
+
codexHomeDir: codexDir,
|
|
1131
|
+
}), null, 2)}\n`;
|
|
1132
|
+
const shim = buildManagedCodexNativeHookWindowsShimContent(packageRoot());
|
|
1133
|
+
await mkdir(codexDir, { recursive: true });
|
|
1134
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1135
|
+
await writeFile(configPath, config);
|
|
1136
|
+
await writeFile(hooksPath, hooks);
|
|
1137
|
+
await writeFile(shimPath, shim);
|
|
1138
|
+
await assert.rejects(uninstall({
|
|
1139
|
+
scope: 'project',
|
|
1140
|
+
transactionPlatform: 'win32',
|
|
1141
|
+
transactionFailureInjector: async (stage) => {
|
|
1142
|
+
if (stage !== 'after-config-commit')
|
|
1143
|
+
return;
|
|
1144
|
+
assert.equal(existsSync(hooksPath), false, 'hooks must mutate before config commit');
|
|
1145
|
+
assert.equal(existsSync(shimPath), false, 'shim must mutate before config commit');
|
|
1146
|
+
assert.notEqual(await readFile(configPath, 'utf-8'), config);
|
|
1147
|
+
throw new Error('simulated config commit failure');
|
|
1148
|
+
},
|
|
1149
|
+
}), /simulated config commit failure/);
|
|
1150
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1151
|
+
assert.equal(await readFile(shimPath, 'utf-8'), shim);
|
|
1152
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
finally {
|
|
1156
|
+
await rm(wd, { recursive: true, force: true });
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
it('rejects invalid UTF-8 hooks.json and config.toml before changing either artifact', async () => {
|
|
1160
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-invalid-utf8-'));
|
|
1161
|
+
try {
|
|
1162
|
+
await withCwd(wd, async () => {
|
|
1163
|
+
const codexDir = join(wd, '.codex');
|
|
1164
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1165
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1166
|
+
const hooks = Buffer.from([0x7b, 0xff, 0x7d]);
|
|
1167
|
+
const config = Buffer.from([0x6f, 0x6d, 0x78, 0x5f, 0xff]);
|
|
1168
|
+
await mkdir(codexDir, { recursive: true });
|
|
1169
|
+
await writeFile(configPath, buildOmxConfig());
|
|
1170
|
+
await writeFile(hooksPath, hooks);
|
|
1171
|
+
await assert.rejects(uninstall({ scope: 'project' }), /not valid UTF-8/);
|
|
1172
|
+
assert.deepEqual(await readFile(hooksPath), hooks);
|
|
1173
|
+
await writeFile(hooksPath, JSON.stringify(buildManagedCodexHooksConfig(packageRoot())));
|
|
1174
|
+
await writeFile(configPath, config);
|
|
1175
|
+
await assert.rejects(uninstall({ scope: 'project' }), /not valid UTF-8/);
|
|
1176
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1177
|
+
const bomHooks = Buffer.concat([
|
|
1178
|
+
Buffer.from([0xef, 0xbb, 0xbf]),
|
|
1179
|
+
Buffer.from('{"hooks":{}}\n', 'utf-8'),
|
|
1180
|
+
]);
|
|
1181
|
+
await writeFile(configPath, buildOmxConfig());
|
|
1182
|
+
await writeFile(hooksPath, bomHooks);
|
|
1183
|
+
await assert.rejects(uninstall({ scope: 'project' }), /must contain a JSON object/);
|
|
1184
|
+
assert.deepEqual(await readFile(hooksPath), bomHooks);
|
|
1185
|
+
assert.equal(existsSync(hooksPath), true);
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
finally {
|
|
1189
|
+
await rm(wd, { recursive: true, force: true });
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
it('rejects symlinked native hook artifacts without following their targets', async () => {
|
|
1193
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-symlink-hooks-'));
|
|
1194
|
+
try {
|
|
1195
|
+
await withCwd(wd, async () => {
|
|
1196
|
+
const codexDir = join(wd, '.codex');
|
|
1197
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1198
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1199
|
+
const targetPath = join(wd, 'user-hooks.json');
|
|
1200
|
+
const config = buildOmxConfig();
|
|
1201
|
+
const target = '{"user":"hooks"}\n';
|
|
1202
|
+
await mkdir(codexDir, { recursive: true });
|
|
1203
|
+
await writeFile(configPath, config);
|
|
1204
|
+
await writeFile(targetPath, target);
|
|
1205
|
+
await symlink(targetPath, hooksPath);
|
|
1206
|
+
await assert.rejects(uninstall({ scope: 'project' }), /expected a regular file, not a symbolic link/);
|
|
1207
|
+
assert.equal((await lstat(hooksPath)).isSymbolicLink(), true);
|
|
1208
|
+
assert.equal(await readFile(targetPath, 'utf-8'), target);
|
|
1209
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
finally {
|
|
1213
|
+
await rm(wd, { recursive: true, force: true });
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
it('aborts before the first removal when any planned artifact becomes stale', async () => {
|
|
1217
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-stale-snapshot-'));
|
|
1218
|
+
try {
|
|
1219
|
+
await withCwd(wd, async () => {
|
|
1220
|
+
const codexDir = join(wd, '.codex');
|
|
1221
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1222
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1223
|
+
const config = buildOmxConfig();
|
|
1224
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1225
|
+
const staleConfig = `${config}# concurrent user edit\n`;
|
|
1226
|
+
await mkdir(codexDir, { recursive: true });
|
|
1227
|
+
await writeFile(configPath, config);
|
|
1228
|
+
await writeFile(hooksPath, hooks);
|
|
1229
|
+
await assert.rejects(uninstall({
|
|
1230
|
+
scope: 'project',
|
|
1231
|
+
transactionFailureInjector: async (stage) => {
|
|
1232
|
+
if (stage === 'before-hooks-commit')
|
|
1233
|
+
await writeFile(configPath, staleConfig);
|
|
1234
|
+
},
|
|
1235
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1236
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1237
|
+
assert.equal(await readFile(configPath, 'utf-8'), staleConfig);
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
finally {
|
|
1241
|
+
await rm(wd, { recursive: true, force: true });
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
it('never replaces a concurrent symlink while rolling back a failed uninstall transaction', async () => {
|
|
1245
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-stale-rollback-link-'));
|
|
1246
|
+
try {
|
|
1247
|
+
await withCwd(wd, async () => {
|
|
1248
|
+
const codexDir = join(wd, '.codex');
|
|
1249
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1250
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1251
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1252
|
+
const replacementPath = join(wd, 'concurrent-user-hooks.json');
|
|
1253
|
+
const config = buildOmxConfig();
|
|
1254
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1255
|
+
platform: 'win32',
|
|
1256
|
+
codexHomeDir: codexDir,
|
|
1257
|
+
}), null, 2)}\n`;
|
|
1258
|
+
const replacement = '{"user":"replacement"}\n';
|
|
1259
|
+
await mkdir(codexDir, { recursive: true });
|
|
1260
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1261
|
+
await writeFile(configPath, config);
|
|
1262
|
+
await writeFile(hooksPath, hooks);
|
|
1263
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
1264
|
+
await writeFile(replacementPath, replacement);
|
|
1265
|
+
await assert.rejects(uninstall({
|
|
1266
|
+
scope: 'project',
|
|
1267
|
+
transactionPlatform: 'win32',
|
|
1268
|
+
transactionFailureInjector: async (stage) => {
|
|
1269
|
+
if (stage !== 'before-shim-removal')
|
|
1270
|
+
return;
|
|
1271
|
+
await symlink(replacementPath, hooksPath);
|
|
1272
|
+
throw new Error('simulated concurrent hook replacement');
|
|
1273
|
+
},
|
|
1274
|
+
}), /Uninstall artifact rollback failed.*(?:Refusing stale rollback|expected a regular file, not a symbolic link)/);
|
|
1275
|
+
assert.equal((await lstat(hooksPath)).isSymbolicLink(), true);
|
|
1276
|
+
assert.equal(await readFile(replacementPath, 'utf-8'), replacement);
|
|
1277
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
finally {
|
|
1281
|
+
await rm(wd, { recursive: true, force: true });
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
it('rejects a symlinked controlled ancestor before reading an escaped artifact', async () => {
|
|
1285
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-ancestor-link-'));
|
|
1286
|
+
try {
|
|
1287
|
+
await withCwd(wd, async () => {
|
|
1288
|
+
const codexDir = join(wd, '.codex');
|
|
1289
|
+
const escapedDir = join(wd, 'escaped-codex');
|
|
1290
|
+
const configPath = join(escapedDir, 'config.toml');
|
|
1291
|
+
const config = buildOmxConfig();
|
|
1292
|
+
await mkdir(escapedDir, { recursive: true });
|
|
1293
|
+
await writeFile(configPath, config);
|
|
1294
|
+
await symlink(escapedDir, codexDir, process.platform === 'win32' ? 'junction' : 'dir');
|
|
1295
|
+
await assert.rejects(uninstall({ scope: 'project' }), /controlled ancestor .* must be a non-symbolic-link directory/);
|
|
1296
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
finally {
|
|
1300
|
+
await rm(wd, { recursive: true, force: true });
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
it('does not truncate or follow regular and symlink replacement temporary collisions', async () => {
|
|
1304
|
+
for (const collisionKind of ['regular', 'symlink']) {
|
|
1305
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-temp-${collisionKind}-`));
|
|
1306
|
+
try {
|
|
1307
|
+
await withCwd(wd, async () => {
|
|
1308
|
+
const codexDir = join(wd, '.codex');
|
|
1309
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1310
|
+
const collisionPath = join(codexDir, '.config.toml.collision');
|
|
1311
|
+
const collisionTarget = join(wd, 'collision-target');
|
|
1312
|
+
const config = buildOmxConfig();
|
|
1313
|
+
const collision = 'foreign temporary collision\n';
|
|
1314
|
+
await mkdir(codexDir, { recursive: true });
|
|
1315
|
+
await writeFile(configPath, config);
|
|
1316
|
+
if (collisionKind === 'regular') {
|
|
1317
|
+
await writeFile(collisionPath, collision);
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
await writeFile(collisionTarget, collision);
|
|
1321
|
+
await symlink(collisionTarget, collisionPath);
|
|
1322
|
+
}
|
|
1323
|
+
await assert.rejects(uninstall({
|
|
1324
|
+
scope: 'project',
|
|
1325
|
+
transactionTemporaryPath: () => collisionPath,
|
|
1326
|
+
}), /EEXIST/);
|
|
1327
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1328
|
+
assert.equal(await readFile(collisionKind === 'regular' ? collisionPath : collisionTarget, 'utf-8'), collision);
|
|
1329
|
+
if (collisionKind === 'symlink') {
|
|
1330
|
+
assert.equal((await lstat(collisionPath)).isSymbolicLink(), true);
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
finally {
|
|
1335
|
+
await rm(wd, { recursive: true, force: true });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
it('rejects stale snapshots immediately before forward rename and remove mutations', async () => {
|
|
1340
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-stale-forward-'));
|
|
1341
|
+
try {
|
|
1342
|
+
await withCwd(wd, async () => {
|
|
1343
|
+
const codexDir = join(wd, '.codex');
|
|
1344
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1345
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1346
|
+
const config = buildOmxConfig();
|
|
1347
|
+
const concurrentConfig = `${config}# concurrent replacement\n`;
|
|
1348
|
+
await mkdir(codexDir, { recursive: true });
|
|
1349
|
+
await writeFile(configPath, config);
|
|
1350
|
+
await assert.rejects(uninstall({
|
|
1351
|
+
scope: 'project',
|
|
1352
|
+
transactionFailureInjector: async (stage) => {
|
|
1353
|
+
if (stage === 'before-rename')
|
|
1354
|
+
await writeFile(configPath, concurrentConfig);
|
|
1355
|
+
},
|
|
1356
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1357
|
+
assert.equal(await readFile(configPath, 'utf-8'), concurrentConfig);
|
|
1358
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1359
|
+
const concurrentHooks = '{"foreign":"replacement"}\n';
|
|
1360
|
+
await writeFile(configPath, config);
|
|
1361
|
+
await writeFile(hooksPath, hooks);
|
|
1362
|
+
await assert.rejects(uninstall({
|
|
1363
|
+
scope: 'project',
|
|
1364
|
+
transactionFailureInjector: async (stage) => {
|
|
1365
|
+
if (stage === 'before-remove')
|
|
1366
|
+
await writeFile(hooksPath, concurrentHooks);
|
|
1367
|
+
},
|
|
1368
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1369
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), concurrentHooks);
|
|
1370
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
finally {
|
|
1374
|
+
await rm(wd, { recursive: true, force: true });
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
it('fails closed for non-throwing regular and symlink write-temporary replacements', async () => {
|
|
1378
|
+
for (const replacementKind of ['regular', 'symlink']) {
|
|
1379
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-write-temp-${replacementKind}-`));
|
|
1380
|
+
try {
|
|
1381
|
+
await withCwd(wd, async () => {
|
|
1382
|
+
const codexDir = join(wd, '.codex');
|
|
1383
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1384
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1385
|
+
const temporaryPath = join(codexDir, '.config.toml.write-temporary');
|
|
1386
|
+
const replacementTarget = join(wd, 'foreign-write-temporary');
|
|
1387
|
+
const config = Buffer.from(buildOmxConfig());
|
|
1388
|
+
const hooks = Buffer.from('{}\n');
|
|
1389
|
+
const replacement = Buffer.from('foreign write temporary replacement\n');
|
|
1390
|
+
await mkdir(codexDir, { recursive: true });
|
|
1391
|
+
await writeFile(configPath, config);
|
|
1392
|
+
await writeFile(hooksPath, hooks);
|
|
1393
|
+
await assert.rejects(uninstall({
|
|
1394
|
+
scope: 'project',
|
|
1395
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'write' ? temporaryPath : join(codexDir, '.hooks.json.staged'),
|
|
1396
|
+
transactionFailureInjector: async (stage) => {
|
|
1397
|
+
if (stage !== 'before-rename')
|
|
1398
|
+
return;
|
|
1399
|
+
await rm(temporaryPath, { force: true });
|
|
1400
|
+
if (replacementKind === 'regular') {
|
|
1401
|
+
await writeFile(temporaryPath, replacement);
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
await writeFile(replacementTarget, replacement);
|
|
1405
|
+
await symlink(replacementTarget, temporaryPath);
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1408
|
+
}), (error) => {
|
|
1409
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1410
|
+
assert.match(message, /planned artifact .* changed, was created, or was removed after planning|expected a regular file, not a symbolic link/);
|
|
1411
|
+
assert.ok(message.includes(temporaryPath));
|
|
1412
|
+
assert.match(message, /preserved temporary .*manual recovery after cleanup verification failed/);
|
|
1413
|
+
return true;
|
|
1414
|
+
});
|
|
1415
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1416
|
+
assert.deepEqual(await readFile(hooksPath), hooks);
|
|
1417
|
+
if (replacementKind === 'regular') {
|
|
1418
|
+
assert.deepEqual(await readFile(temporaryPath), replacement);
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
assert.equal((await lstat(temporaryPath)).isSymbolicLink(), true);
|
|
1422
|
+
assert.deepEqual(await readFile(replacementTarget), replacement);
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
finally {
|
|
1427
|
+
await rm(wd, { recursive: true, force: true });
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
it('fails closed for non-throwing regular and symlink staged-tombstone replacements', async () => {
|
|
1432
|
+
for (const replacementKind of ['regular', 'symlink']) {
|
|
1433
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-staged-tombstone-${replacementKind}-`));
|
|
1434
|
+
try {
|
|
1435
|
+
await withCwd(wd, async () => {
|
|
1436
|
+
const codexDir = join(wd, '.codex');
|
|
1437
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1438
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1439
|
+
const stagedPath = join(codexDir, '.hooks.json.staged-tombstone');
|
|
1440
|
+
const replacementTarget = join(wd, 'foreign-staged-tombstone');
|
|
1441
|
+
const config = Buffer.from(buildOmxConfig());
|
|
1442
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`);
|
|
1443
|
+
const replacement = Buffer.from('foreign staged tombstone replacement\n');
|
|
1444
|
+
await mkdir(codexDir, { recursive: true });
|
|
1445
|
+
await writeFile(configPath, config);
|
|
1446
|
+
await writeFile(hooksPath, hooks);
|
|
1447
|
+
await assert.rejects(uninstall({
|
|
1448
|
+
scope: 'project',
|
|
1449
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1450
|
+
transactionFailureInjector: async (stage) => {
|
|
1451
|
+
if (stage !== 'before-remove')
|
|
1452
|
+
return;
|
|
1453
|
+
await rm(stagedPath, { force: true });
|
|
1454
|
+
if (replacementKind === 'regular') {
|
|
1455
|
+
await writeFile(stagedPath, replacement);
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
await writeFile(replacementTarget, replacement);
|
|
1459
|
+
await symlink(replacementTarget, stagedPath);
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
}), (error) => {
|
|
1463
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1464
|
+
assert.match(message, /planned artifact .* changed, was created, or was removed after planning|expected a regular file, not a symbolic link/);
|
|
1465
|
+
assert.ok(message.includes(stagedPath));
|
|
1466
|
+
assert.match(message, /preserved staged deletion .*manual recovery after cleanup verification failed/);
|
|
1467
|
+
return true;
|
|
1468
|
+
});
|
|
1469
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1470
|
+
assert.deepEqual(await readFile(hooksPath), hooks);
|
|
1471
|
+
if (replacementKind === 'regular') {
|
|
1472
|
+
assert.deepEqual(await readFile(stagedPath), replacement);
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
assert.equal((await lstat(stagedPath)).isSymbolicLink(), true);
|
|
1476
|
+
assert.deepEqual(await readFile(replacementTarget), replacement);
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
finally {
|
|
1481
|
+
await rm(wd, { recursive: true, force: true });
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
it('rejects stale snapshots immediately before rollback rename and staged-copy removal', async () => {
|
|
1486
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-stale-rollback-primitives-'));
|
|
1487
|
+
try {
|
|
1488
|
+
await withCwd(wd, async () => {
|
|
1489
|
+
const codexDir = join(wd, '.codex');
|
|
1490
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1491
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1492
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1493
|
+
const config = buildOmxConfig();
|
|
1494
|
+
await mkdir(codexDir, { recursive: true });
|
|
1495
|
+
await writeFile(configPath, config);
|
|
1496
|
+
await writeFile(hooksPath, hooks);
|
|
1497
|
+
const concurrentHooks = '{"foreign":"rollback rename"}\n';
|
|
1498
|
+
await assert.rejects(uninstall({
|
|
1499
|
+
scope: 'project',
|
|
1500
|
+
transactionFailureInjector: async (stage) => {
|
|
1501
|
+
if (stage === 'before-config-commit')
|
|
1502
|
+
throw new Error('stop after hook deletion');
|
|
1503
|
+
if (stage === 'before-rollback-rename') {
|
|
1504
|
+
await writeFile(hooksPath, concurrentHooks);
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
}), /Uninstall artifact rollback failed.*Refusing uninstall because planned artifact/);
|
|
1508
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), concurrentHooks);
|
|
1509
|
+
await writeFile(hooksPath, hooks);
|
|
1510
|
+
const stagedPath = join(codexDir, '.hooks.json.rollback-stage');
|
|
1511
|
+
const staleStagedCopy = 'foreign staged copy\n';
|
|
1512
|
+
await assert.rejects(uninstall({
|
|
1513
|
+
scope: 'project',
|
|
1514
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1515
|
+
transactionFailureInjector: async (stage) => {
|
|
1516
|
+
if (stage === 'before-config-commit')
|
|
1517
|
+
throw new Error('stop after hook deletion');
|
|
1518
|
+
if (stage === 'before-rollback-remove') {
|
|
1519
|
+
await writeFile(stagedPath, staleStagedCopy);
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
}), /Uninstall artifact rollback failed.*staged deletion cleanup/);
|
|
1523
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1524
|
+
assert.equal(await readFile(stagedPath, 'utf-8'), staleStagedCopy);
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
finally {
|
|
1528
|
+
await rm(wd, { recursive: true, force: true });
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
it('rolls back when staged-deletion cleanup fails before any staged copy is committed', async () => {
|
|
1532
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-staged-deletion-'));
|
|
1533
|
+
try {
|
|
1534
|
+
await withCwd(wd, async () => {
|
|
1535
|
+
const codexDir = join(wd, '.codex');
|
|
1536
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1537
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1538
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1539
|
+
const config = buildOmxConfig();
|
|
1540
|
+
const stagedPath = join(codexDir, '.hooks.json.staged');
|
|
1541
|
+
await mkdir(codexDir, { recursive: true });
|
|
1542
|
+
await writeFile(configPath, config);
|
|
1543
|
+
await writeFile(hooksPath, hooks);
|
|
1544
|
+
await uninstall({
|
|
1545
|
+
scope: 'project',
|
|
1546
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1547
|
+
});
|
|
1548
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1549
|
+
assert.equal(existsSync(stagedPath), false, 'successful commit leaves no staged tombstone');
|
|
1550
|
+
await writeFile(configPath, config);
|
|
1551
|
+
await writeFile(hooksPath, hooks);
|
|
1552
|
+
let interrupted = false;
|
|
1553
|
+
await assert.rejects(uninstall({
|
|
1554
|
+
scope: 'project',
|
|
1555
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1556
|
+
transactionFailureInjector: (stage) => {
|
|
1557
|
+
if (stage === 'before-staged-cleanup' && !interrupted) {
|
|
1558
|
+
interrupted = true;
|
|
1559
|
+
throw new Error('staged deletion cleanup interrupted');
|
|
1560
|
+
}
|
|
1561
|
+
},
|
|
1562
|
+
}), /staged deletion cleanup interrupted/);
|
|
1563
|
+
assert.equal(interrupted, true);
|
|
1564
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1565
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1566
|
+
assert.equal(existsSync(stagedPath), false);
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
finally {
|
|
1570
|
+
await rm(wd, { recursive: true, force: true });
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
it('does not roll back when preserved metadata drifts after staged-deletion cleanup finalization', async () => {
|
|
1574
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-second-staged-drift-'));
|
|
1575
|
+
try {
|
|
1576
|
+
await withCwd(wd, async () => {
|
|
1577
|
+
const codexDir = join(wd, '.codex');
|
|
1578
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1579
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1580
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1581
|
+
const metadataPath = join(codexDir, '.omx', 'notify-dispatch.json');
|
|
1582
|
+
const dispatcherPath = join(packageRoot(), 'dist', 'scripts', 'notify-dispatcher.js');
|
|
1583
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1584
|
+
platform: 'win32',
|
|
1585
|
+
codexHomeDir: codexDir,
|
|
1586
|
+
}), null, 2)}\n`;
|
|
1587
|
+
const metadata = {
|
|
1588
|
+
managedBy: 'oh-my-codex',
|
|
1589
|
+
version: 1,
|
|
1590
|
+
previousNotify: ['node', '/tmp/user-notify.js'],
|
|
1591
|
+
omxNotify: ['node', join(packageRoot(), 'dist', 'scripts', 'notify-hook.js')],
|
|
1592
|
+
dispatcherNotify: ['node', dispatcherPath, '--metadata', metadataPath],
|
|
1593
|
+
};
|
|
1594
|
+
const foreignMetadata = JSON.stringify({
|
|
1595
|
+
...metadata,
|
|
1596
|
+
previousNotify: ['node', '/tmp/concurrent-notify.js'],
|
|
1597
|
+
});
|
|
1598
|
+
const config = buildOmxConfig().replace(/^notify = .*$/m, `notify = ${JSON.stringify(metadata.dispatcherNotify)}`);
|
|
1599
|
+
const stagedDeletionPath = (path, purpose) => join(dirname(path), `.${basename(path)}.cleanup-${purpose}`);
|
|
1600
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
1601
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1602
|
+
await writeFile(configPath, config);
|
|
1603
|
+
await writeFile(hooksPath, hooks);
|
|
1604
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
1605
|
+
await writeFile(metadataPath, JSON.stringify(metadata));
|
|
1606
|
+
let injected = false;
|
|
1607
|
+
await assert.rejects(uninstall({
|
|
1608
|
+
scope: 'project',
|
|
1609
|
+
transactionPlatform: 'win32',
|
|
1610
|
+
transactionTemporaryPath: stagedDeletionPath,
|
|
1611
|
+
transactionFailureInjector: async (stage) => {
|
|
1612
|
+
if (stage !== 'after-staged-cleanup' || injected)
|
|
1613
|
+
return;
|
|
1614
|
+
injected = true;
|
|
1615
|
+
await writeFile(metadataPath, foreignMetadata);
|
|
1616
|
+
},
|
|
1617
|
+
}), /committed but staged deletion cleanup failed/);
|
|
1618
|
+
assert.equal(injected, true);
|
|
1619
|
+
assert.equal(await readFile(metadataPath, 'utf-8'), foreignMetadata);
|
|
1620
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1621
|
+
assert.equal(existsSync(shimPath), false);
|
|
1622
|
+
assert.notEqual(await readFile(configPath, 'utf-8'), config);
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
finally {
|
|
1626
|
+
await rm(wd, { recursive: true, force: true });
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
it('treats applied hook, shim, and config snapshots as CAS through commit finalization', async () => {
|
|
1630
|
+
const fixtures = [
|
|
1631
|
+
{ name: 'later commit', stage: 'before-shim-removal', drift: 'hooks', committed: false },
|
|
1632
|
+
{ name: 'staged cleanup', stage: 'before-staged-cleanup', drift: 'shim', committed: false },
|
|
1633
|
+
{ name: 'finalization', stage: 'after-staged-cleanup', drift: 'config', committed: true },
|
|
1634
|
+
];
|
|
1635
|
+
for (const fixture of fixtures) {
|
|
1636
|
+
const wd = await mkdtemp(join(tmpdir(), `omx-uninstall-applied-${fixture.name}-`));
|
|
1637
|
+
try {
|
|
1638
|
+
await withCwd(wd, async () => {
|
|
1639
|
+
const codexDir = join(wd, '.codex');
|
|
1640
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1641
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1642
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
1643
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
1644
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
1645
|
+
platform: 'win32',
|
|
1646
|
+
codexHomeDir: codexDir,
|
|
1647
|
+
}), null, 2)}\n`, 'utf-8');
|
|
1648
|
+
const shim = Buffer.from(buildManagedCodexNativeHookWindowsShimContent(packageRoot()), 'utf-8');
|
|
1649
|
+
const paths = { config: configPath, hooks: hooksPath, shim: shimPath };
|
|
1650
|
+
const before = { config, hooks, shim };
|
|
1651
|
+
const foreign = Buffer.from(`foreign applied ${fixture.drift} ${fixture.name}\n`, 'utf-8');
|
|
1652
|
+
await mkdir(codexDir, { recursive: true });
|
|
1653
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
1654
|
+
await writeFile(configPath, config);
|
|
1655
|
+
await writeFile(hooksPath, hooks);
|
|
1656
|
+
await writeFile(shimPath, shim);
|
|
1657
|
+
let injected = false;
|
|
1658
|
+
await assert.rejects(uninstall({
|
|
1659
|
+
scope: 'project',
|
|
1660
|
+
transactionPlatform: 'win32',
|
|
1661
|
+
transactionFailureInjector: async (stage) => {
|
|
1662
|
+
if (stage !== fixture.stage || injected)
|
|
1663
|
+
return;
|
|
1664
|
+
injected = true;
|
|
1665
|
+
await writeFile(paths[fixture.drift], foreign);
|
|
1666
|
+
},
|
|
1667
|
+
}), fixture.committed
|
|
1668
|
+
? /committed but staged deletion cleanup failed during finalization/
|
|
1669
|
+
: /Uninstall artifact rollback failed/);
|
|
1670
|
+
assert.equal(injected, true, fixture.name);
|
|
1671
|
+
if (fixture.name === 'staged cleanup') {
|
|
1672
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1673
|
+
assert.deepEqual(await readFile(shimPath), foreign);
|
|
1674
|
+
assert.notDeepEqual(await readFile(configPath), before.config);
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
if (fixture.committed) {
|
|
1678
|
+
assert.equal(existsSync(hooksPath), false);
|
|
1679
|
+
assert.equal(existsSync(shimPath), false);
|
|
1680
|
+
assert.deepEqual(await readFile(configPath), foreign);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
assert.deepEqual(await readFile(hooksPath), fixture.drift === 'hooks' ? foreign : before.hooks);
|
|
1684
|
+
assert.deepEqual(await readFile(shimPath), before.shim);
|
|
1685
|
+
assert.deepEqual(await readFile(configPath), before.config);
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
finally {
|
|
1689
|
+
await rm(wd, { recursive: true, force: true });
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
it('rolls back registered destinations when post-rename or post-remove verification fails', async () => {
|
|
1694
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-post-apply-recovery-'));
|
|
1695
|
+
try {
|
|
1696
|
+
await withCwd(wd, async () => {
|
|
1697
|
+
const codexDir = join(wd, '.codex');
|
|
1698
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1699
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1700
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
1701
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`, 'utf-8');
|
|
1702
|
+
await mkdir(codexDir, { recursive: true });
|
|
1703
|
+
await writeFile(configPath, config);
|
|
1704
|
+
await assert.rejects(uninstall({
|
|
1705
|
+
scope: 'project',
|
|
1706
|
+
transactionFailureInjector: (stage) => {
|
|
1707
|
+
if (stage === 'after-rename')
|
|
1708
|
+
throw new Error('post-rename verification failure');
|
|
1709
|
+
},
|
|
1710
|
+
}), /post-rename verification failure/);
|
|
1711
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1712
|
+
await writeFile(configPath, config);
|
|
1713
|
+
await writeFile(hooksPath, hooks);
|
|
1714
|
+
await assert.rejects(uninstall({
|
|
1715
|
+
scope: 'project',
|
|
1716
|
+
transactionFailureInjector: (stage) => {
|
|
1717
|
+
if (stage === 'after-remove')
|
|
1718
|
+
throw new Error('post-remove verification failure');
|
|
1719
|
+
},
|
|
1720
|
+
}), /post-remove verification failure/);
|
|
1721
|
+
assert.deepEqual(await readFile(hooksPath), hooks);
|
|
1722
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
finally {
|
|
1726
|
+
await rm(wd, { recursive: true, force: true });
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
it('strictly validates dispatcher metadata and treats it as a stale read-only transaction precondition', async () => {
|
|
1730
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-notify-metadata-'));
|
|
1731
|
+
try {
|
|
1732
|
+
await withCwd(wd, async () => {
|
|
1733
|
+
const codexDir = join(wd, '.codex');
|
|
1734
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1735
|
+
const metadataPath = join(codexDir, '.omx', 'notify-dispatch.json');
|
|
1736
|
+
const dispatcherPath = join(packageRoot(), 'dist', 'scripts', 'notify-dispatcher.js');
|
|
1737
|
+
const config = buildOmxConfig().replace(/^notify = .*$/m, `notify = ${JSON.stringify(['node', dispatcherPath, '--metadata', metadataPath])}`);
|
|
1738
|
+
const metadata = {
|
|
1739
|
+
managedBy: 'oh-my-codex',
|
|
1740
|
+
version: 1,
|
|
1741
|
+
previousNotify: ['node', '/tmp/user-notify.js'],
|
|
1742
|
+
omxNotify: ['node', join(packageRoot(), 'dist', 'scripts', 'notify-hook.js')],
|
|
1743
|
+
dispatcherNotify: ['node', dispatcherPath, '--metadata', metadataPath],
|
|
1744
|
+
};
|
|
1745
|
+
await mkdir(dirname(metadataPath), { recursive: true });
|
|
1746
|
+
await writeFile(configPath, config);
|
|
1747
|
+
await writeFile(metadataPath, JSON.stringify({ ...metadata, previousNotify: ['not-a-string', 7] }));
|
|
1748
|
+
await assert.rejects(uninstall({ scope: 'project' }), /previousNotify must be null or an array of strings/);
|
|
1749
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1750
|
+
await writeFile(metadataPath, Buffer.from([0x7b, 0xff, 0x7d]));
|
|
1751
|
+
await assert.rejects(uninstall({ scope: 'project' }), /not valid UTF-8/);
|
|
1752
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1753
|
+
await writeFile(metadataPath, JSON.stringify(metadata));
|
|
1754
|
+
await assert.rejects(uninstall({
|
|
1755
|
+
scope: 'project',
|
|
1756
|
+
transactionFailureInjector: async (stage) => {
|
|
1757
|
+
if (stage === 'before-config-commit') {
|
|
1758
|
+
await writeFile(metadataPath, JSON.stringify({ ...metadata, previousNotify: ['node', '/tmp/concurrent.js'] }));
|
|
1759
|
+
}
|
|
1760
|
+
},
|
|
1761
|
+
}), /planned artifact .* changed, was created, or was removed after planning/);
|
|
1762
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
finally {
|
|
1766
|
+
await rm(wd, { recursive: true, force: true });
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
it('does not report stale rollback recovery when failure occurs before destructive removal', async () => {
|
|
1770
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-pre-destructive-'));
|
|
1771
|
+
try {
|
|
1772
|
+
await withCwd(wd, async () => {
|
|
1773
|
+
const codexDir = join(wd, '.codex');
|
|
1774
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1775
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1776
|
+
const stagedPath = join(codexDir, '.hooks.json.pre-destructive');
|
|
1777
|
+
const config = buildOmxConfig();
|
|
1778
|
+
const hooks = `${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`;
|
|
1779
|
+
await mkdir(codexDir, { recursive: true });
|
|
1780
|
+
await writeFile(configPath, config);
|
|
1781
|
+
await writeFile(hooksPath, hooks);
|
|
1782
|
+
await assert.rejects(uninstall({
|
|
1783
|
+
scope: 'project',
|
|
1784
|
+
transactionTemporaryPath: (_path, purpose) => purpose === 'delete' ? stagedPath : join(codexDir, '.write-temporary'),
|
|
1785
|
+
transactionFailureInjector: (stage) => {
|
|
1786
|
+
if (stage === 'before-remove')
|
|
1787
|
+
throw new Error('pre-destructive removal failure');
|
|
1788
|
+
},
|
|
1789
|
+
}), (error) => {
|
|
1790
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1791
|
+
assert.match(message, /pre-destructive removal failure/);
|
|
1792
|
+
assert.doesNotMatch(message, /manual recovery|stale rollback/i);
|
|
1793
|
+
return true;
|
|
1794
|
+
});
|
|
1795
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1796
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1797
|
+
assert.equal(existsSync(stagedPath), false);
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
finally {
|
|
1801
|
+
await rm(wd, { recursive: true, force: true });
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
it('removes exact historical root trust state but preserves nonmatching nested state', async () => {
|
|
1805
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-legacy-hook-state-'));
|
|
1806
|
+
try {
|
|
1807
|
+
const home = join(wd, 'home');
|
|
1808
|
+
const codexDir = join(home, '.codex');
|
|
1809
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1810
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1811
|
+
await mkdir(codexDir, { recursive: true });
|
|
1812
|
+
await writeFile(configPath, buildOmxConfig());
|
|
1813
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
1814
|
+
state: {
|
|
1815
|
+
[`${hooksPath}:stop:0:0`]: { trusted_hash: 'sha256:historical' },
|
|
427
1816
|
},
|
|
428
|
-
version: 1,
|
|
429
1817
|
}, null, 2) + '\n');
|
|
1818
|
+
const rootStateCleanup = runOmx(wd, ['uninstall'], { HOME: home });
|
|
1819
|
+
if (shouldSkipForSpawnPermissions(rootStateCleanup.error))
|
|
1820
|
+
return;
|
|
1821
|
+
assert.equal(rootStateCleanup.status, 0, rootStateCleanup.stderr || rootStateCleanup.stdout);
|
|
1822
|
+
assert.deepEqual(JSON.parse(await readFile(hooksPath, 'utf-8')), {});
|
|
1823
|
+
const nestedState = {
|
|
1824
|
+
retained: { custom: true, trusted_hash: 'sha256:not-omx' },
|
|
1825
|
+
};
|
|
1826
|
+
await writeFile(hooksPath, JSON.stringify({
|
|
1827
|
+
hooks: {
|
|
1828
|
+
state: nestedState,
|
|
1829
|
+
SessionStart: [{
|
|
1830
|
+
hooks: [{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' }],
|
|
1831
|
+
}],
|
|
1832
|
+
},
|
|
1833
|
+
}, null, 2) + '\n');
|
|
1834
|
+
const nestedStateCleanup = runOmx(wd, ['uninstall', '--keep-config'], { HOME: home });
|
|
1835
|
+
assert.equal(nestedStateCleanup.status, 0, nestedStateCleanup.stderr || nestedStateCleanup.stdout);
|
|
1836
|
+
const after = JSON.parse(await readFile(hooksPath, 'utf-8'));
|
|
1837
|
+
assert.deepEqual(after.hooks?.state, nestedState);
|
|
1838
|
+
assert.equal(after.hooks?.SessionStart, undefined);
|
|
1839
|
+
}
|
|
1840
|
+
finally {
|
|
1841
|
+
await rm(wd, { recursive: true, force: true });
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
it('removes only trust state at the managed hook coordinates planned from hooks.json', async () => {
|
|
1845
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-hook-trust-coordinates-'));
|
|
1846
|
+
try {
|
|
1847
|
+
const home = join(wd, 'home');
|
|
1848
|
+
const codexDir = join(home, '.codex');
|
|
1849
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1850
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1851
|
+
const hooks = JSON.stringify({
|
|
1852
|
+
hooks: {
|
|
1853
|
+
SessionStart: [{
|
|
1854
|
+
hooks: [{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' }],
|
|
1855
|
+
}],
|
|
1856
|
+
},
|
|
1857
|
+
}, null, 2) + '\n';
|
|
1858
|
+
const trustState = buildManagedCodexHookTrustState(hooksPath, wd, {
|
|
1859
|
+
hooksContent: hooks,
|
|
1860
|
+
});
|
|
1861
|
+
const [actualKey, actualTrust] = Object.entries(trustState)[0] ?? [];
|
|
1862
|
+
if (!actualKey || !actualTrust) {
|
|
1863
|
+
assert.fail('expected SessionStart managed trust state');
|
|
1864
|
+
}
|
|
1865
|
+
const staleCoordinate = `${hooksPath}:session_start:9:0`;
|
|
1866
|
+
const actualHeader = `[hooks.state.${JSON.stringify(actualKey)}]`;
|
|
1867
|
+
const staleHeader = `[hooks.state.${JSON.stringify(staleCoordinate)}]`;
|
|
1868
|
+
const config = [
|
|
1869
|
+
buildOmxConfig().trimEnd(),
|
|
1870
|
+
'',
|
|
1871
|
+
actualHeader,
|
|
1872
|
+
`trusted_hash = "${actualTrust.trusted_hash}"`,
|
|
1873
|
+
'',
|
|
1874
|
+
staleHeader,
|
|
1875
|
+
`trusted_hash = "${actualTrust.trusted_hash}"`,
|
|
1876
|
+
'',
|
|
1877
|
+
].join('\n');
|
|
1878
|
+
await mkdir(codexDir, { recursive: true });
|
|
1879
|
+
await writeFile(configPath, config);
|
|
1880
|
+
await writeFile(hooksPath, hooks);
|
|
430
1881
|
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
431
1882
|
if (shouldSkipForSpawnPermissions(res.error))
|
|
432
1883
|
return;
|
|
433
1884
|
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
assert.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
1885
|
+
const cleaned = await readFile(configPath, 'utf-8');
|
|
1886
|
+
assert.equal(cleaned.includes(actualHeader), false);
|
|
1887
|
+
assert.equal(cleaned.includes(staleHeader), true);
|
|
1888
|
+
}
|
|
1889
|
+
finally {
|
|
1890
|
+
await rm(wd, { recursive: true, force: true });
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
it('keeps empty hooks.json byte-identical during dry-run and no-op uninstall', async () => {
|
|
1894
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-empty-hooks-'));
|
|
1895
|
+
try {
|
|
1896
|
+
const home = join(wd, 'home');
|
|
1897
|
+
const codexDir = join(home, '.codex');
|
|
1898
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1899
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1900
|
+
const config = buildOmxConfig();
|
|
1901
|
+
const hooks = '{}\n';
|
|
1902
|
+
await mkdir(codexDir, { recursive: true });
|
|
1903
|
+
await writeFile(configPath, config);
|
|
1904
|
+
await writeFile(hooksPath, hooks);
|
|
1905
|
+
const dryRun = runOmx(wd, ['uninstall', '--dry-run'], { HOME: home });
|
|
1906
|
+
if (shouldSkipForSpawnPermissions(dryRun.error))
|
|
1907
|
+
return;
|
|
1908
|
+
assert.equal(dryRun.status, 0, dryRun.stderr || dryRun.stdout);
|
|
1909
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1910
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1911
|
+
const uninstall = runOmx(wd, ['uninstall'], { HOME: home });
|
|
1912
|
+
assert.equal(uninstall.status, 0, uninstall.stderr || uninstall.stdout);
|
|
1913
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
1914
|
+
}
|
|
1915
|
+
finally {
|
|
1916
|
+
await rm(wd, { recursive: true, force: true });
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
it('fails closed for partial hooks.json corruption before config cleanup', async () => {
|
|
1920
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-corrupt-hooks-'));
|
|
1921
|
+
try {
|
|
1922
|
+
const home = join(wd, 'home');
|
|
1923
|
+
const codexDir = join(home, '.codex');
|
|
1924
|
+
const configPath = join(codexDir, 'config.toml');
|
|
1925
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
1926
|
+
const config = buildOmxConfig();
|
|
1927
|
+
const hooks = '{\n "hooks": { "SessionStart": {} }\n}\n';
|
|
1928
|
+
await mkdir(codexDir, { recursive: true });
|
|
1929
|
+
await writeFile(configPath, config);
|
|
1930
|
+
await writeFile(hooksPath, hooks);
|
|
1931
|
+
const res = runOmx(wd, ['uninstall'], { HOME: home });
|
|
1932
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
1933
|
+
return;
|
|
1934
|
+
assert.equal(res.status, 1, res.stderr || res.stdout);
|
|
1935
|
+
assert.match(res.stderr, /SessionStart must be an array/);
|
|
1936
|
+
assert.equal(await readFile(configPath, 'utf-8'), config);
|
|
1937
|
+
assert.equal(await readFile(hooksPath, 'utf-8'), hooks);
|
|
445
1938
|
}
|
|
446
1939
|
finally {
|
|
447
1940
|
await rm(wd, { recursive: true, force: true });
|
|
@@ -457,10 +1950,11 @@ describe('omx uninstall', () => {
|
|
|
457
1950
|
await writeFile(join(codexDir, 'hooks.json'), JSON.stringify({
|
|
458
1951
|
hooks: {
|
|
459
1952
|
SessionStart: [
|
|
1953
|
+
{ hooks: [{ type: 'command', command: 'echo keep-me' }] },
|
|
460
1954
|
{
|
|
1955
|
+
matcher: 'startup|resume|clear',
|
|
461
1956
|
hooks: [
|
|
462
1957
|
{ type: 'command', command: 'node "/repo/dist/scripts/codex-native-hook.js"' },
|
|
463
|
-
{ type: 'command', command: 'echo keep-me' },
|
|
464
1958
|
],
|
|
465
1959
|
},
|
|
466
1960
|
],
|
|
@@ -866,6 +2360,25 @@ describe('omx uninstall', () => {
|
|
|
866
2360
|
await rm(wd, { recursive: true, force: true });
|
|
867
2361
|
}
|
|
868
2362
|
});
|
|
2363
|
+
it('removes OMX-managed AGENTS sections while preserving project guidance', async () => {
|
|
2364
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-merged-agents-'));
|
|
2365
|
+
try {
|
|
2366
|
+
const home = join(wd, 'home');
|
|
2367
|
+
await mkdir(home, { recursive: true });
|
|
2368
|
+
await mkdir(join(wd, '.omx'), { recursive: true });
|
|
2369
|
+
await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
|
|
2370
|
+
const userGuidance = '# User project instructions\n\nPreserve this guidance.\n';
|
|
2371
|
+
await writeFile(join(wd, 'AGENTS.md'), `${userGuidance}\n<!-- OMX:AGENTS:START -->\n<!-- omx:generated:agents-md -->\n# oh-my-codex - Intelligent Multi-Agent Orchestration\n<!-- OMX:AGENTS:END -->\n`);
|
|
2372
|
+
const res = runOmx(wd, ['uninstall', '--keep-config'], { HOME: home });
|
|
2373
|
+
if (shouldSkipForSpawnPermissions(res.error))
|
|
2374
|
+
return;
|
|
2375
|
+
assert.equal(res.status, 0, res.stderr || res.stdout);
|
|
2376
|
+
assert.equal(await readFile(join(wd, 'AGENTS.md'), 'utf-8'), userGuidance);
|
|
2377
|
+
}
|
|
2378
|
+
finally {
|
|
2379
|
+
await rm(wd, { recursive: true, force: true });
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
869
2382
|
it('removes managed user-scope AGENTS.md from CODEX_HOME', async () => {
|
|
870
2383
|
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-'));
|
|
871
2384
|
try {
|
|
@@ -1094,6 +2607,240 @@ describe('omx uninstall', () => {
|
|
|
1094
2607
|
await rm(wd, { recursive: true, force: true });
|
|
1095
2608
|
}
|
|
1096
2609
|
});
|
|
2610
|
+
it('preserves a same-byte foreign replacement made immediately after an uninstall rename', async () => {
|
|
2611
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-immediate-post-rename-'));
|
|
2612
|
+
try {
|
|
2613
|
+
await withCwd(wd, async () => {
|
|
2614
|
+
const codexDir = join(wd, '.codex');
|
|
2615
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2616
|
+
const foreignPath = join(codexDir, '.foreign-config-same-byte');
|
|
2617
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2618
|
+
let replacement;
|
|
2619
|
+
let replacedInode;
|
|
2620
|
+
await mkdir(codexDir, { recursive: true });
|
|
2621
|
+
await writeFile(configPath, config);
|
|
2622
|
+
await assert.rejects(uninstall({
|
|
2623
|
+
scope: 'project',
|
|
2624
|
+
transactionFailureInjector: async (stage) => {
|
|
2625
|
+
if (stage !== 'after-rename')
|
|
2626
|
+
return;
|
|
2627
|
+
replacement = readFileSync(configPath);
|
|
2628
|
+
const ownedInode = lstatSync(configPath).ino;
|
|
2629
|
+
await writeFile(foreignPath, replacement);
|
|
2630
|
+
assert.notEqual(lstatSync(foreignPath).ino, ownedInode);
|
|
2631
|
+
rmSync(configPath);
|
|
2632
|
+
renameSync(foreignPath, configPath);
|
|
2633
|
+
replacedInode = lstatSync(configPath).ino;
|
|
2634
|
+
},
|
|
2635
|
+
}), /Uninstall artifact rollback failed.*recovery preflight/);
|
|
2636
|
+
assert.ok(replacement);
|
|
2637
|
+
assert.ok(replacedInode);
|
|
2638
|
+
assert.deepEqual(await readFile(configPath), replacement);
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
finally {
|
|
2642
|
+
await rm(wd, { recursive: true, force: true });
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
it('preflights every staged uninstall recovery copy before rollback', async () => {
|
|
2646
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-rollback-recovery-preflight-'));
|
|
2647
|
+
try {
|
|
2648
|
+
await withCwd(wd, async () => {
|
|
2649
|
+
const codexDir = join(wd, '.codex');
|
|
2650
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2651
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
2652
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
2653
|
+
const stagedShimPath = join(dirname(shimPath), '.shim-preflight-recovery');
|
|
2654
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2655
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
2656
|
+
platform: 'win32',
|
|
2657
|
+
codexHomeDir: codexDir,
|
|
2658
|
+
}), null, 2)}\n`, 'utf-8');
|
|
2659
|
+
await mkdir(codexDir, { recursive: true });
|
|
2660
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
2661
|
+
await writeFile(configPath, config);
|
|
2662
|
+
await writeFile(hooksPath, hooks);
|
|
2663
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
2664
|
+
await assert.rejects(uninstall({
|
|
2665
|
+
scope: 'project',
|
|
2666
|
+
transactionPlatform: 'win32',
|
|
2667
|
+
transactionTemporaryPath: (path, purpose) => path === shimPath && purpose === 'delete'
|
|
2668
|
+
? stagedShimPath
|
|
2669
|
+
: join(dirname(path), `.${basename(path)}.${purpose}-preflight`),
|
|
2670
|
+
transactionFailureInjector: async (stage) => {
|
|
2671
|
+
if (stage !== 'after-config-commit')
|
|
2672
|
+
return;
|
|
2673
|
+
await rm(stagedShimPath);
|
|
2674
|
+
throw new Error('injected missing staged recovery copy');
|
|
2675
|
+
},
|
|
2676
|
+
}), /Uninstall artifact rollback failed.*recovery preflight/);
|
|
2677
|
+
assert.equal(existsSync(hooksPath), false);
|
|
2678
|
+
assert.equal(existsSync(shimPath), false);
|
|
2679
|
+
assert.notDeepEqual(await readFile(configPath), config);
|
|
2680
|
+
assert.equal(existsSync(stagedShimPath), false);
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
finally {
|
|
2684
|
+
await rm(wd, { recursive: true, force: true });
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
it('stops later uninstall rollback restores when the first restored artifact drifts', async () => {
|
|
2688
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-rollback-restored-drift-'));
|
|
2689
|
+
try {
|
|
2690
|
+
await withCwd(wd, async () => {
|
|
2691
|
+
const codexDir = join(wd, '.codex');
|
|
2692
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2693
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
2694
|
+
const shimPath = buildManagedCodexNativeHookWindowsShimPath(codexDir);
|
|
2695
|
+
const foreignConfigPath = join(codexDir, '.foreign-restored-config');
|
|
2696
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2697
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot(), {
|
|
2698
|
+
platform: 'win32',
|
|
2699
|
+
codexHomeDir: codexDir,
|
|
2700
|
+
}), null, 2)}\n`, 'utf-8');
|
|
2701
|
+
await mkdir(codexDir, { recursive: true });
|
|
2702
|
+
await mkdir(dirname(shimPath), { recursive: true });
|
|
2703
|
+
await writeFile(configPath, config);
|
|
2704
|
+
await writeFile(hooksPath, hooks);
|
|
2705
|
+
await writeFile(shimPath, buildManagedCodexNativeHookWindowsShimContent(packageRoot()));
|
|
2706
|
+
let rollbackRenameCount = 0;
|
|
2707
|
+
let drifted = false;
|
|
2708
|
+
await assert.rejects(uninstall({
|
|
2709
|
+
scope: 'project',
|
|
2710
|
+
transactionPlatform: 'win32',
|
|
2711
|
+
transactionFailureInjector: async (stage) => {
|
|
2712
|
+
if (stage === 'after-config-commit') {
|
|
2713
|
+
throw new Error('injected rollback start failure');
|
|
2714
|
+
}
|
|
2715
|
+
if (stage !== 'before-rollback-rename')
|
|
2716
|
+
return;
|
|
2717
|
+
rollbackRenameCount += 1;
|
|
2718
|
+
if (rollbackRenameCount !== 2)
|
|
2719
|
+
return;
|
|
2720
|
+
const restoredInode = lstatSync(configPath).ino;
|
|
2721
|
+
await writeFile(foreignConfigPath, config);
|
|
2722
|
+
assert.notEqual(lstatSync(foreignConfigPath).ino, restoredInode);
|
|
2723
|
+
rmSync(configPath);
|
|
2724
|
+
renameSync(foreignConfigPath, configPath);
|
|
2725
|
+
drifted = lstatSync(configPath).ino !== restoredInode;
|
|
2726
|
+
},
|
|
2727
|
+
}), /Uninstall artifact rollback failed/);
|
|
2728
|
+
assert.equal(rollbackRenameCount, 2);
|
|
2729
|
+
assert.equal(drifted, true);
|
|
2730
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
2731
|
+
assert.equal(existsSync(shimPath), false);
|
|
2732
|
+
assert.equal(existsSync(hooksPath), false);
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
finally {
|
|
2736
|
+
await rm(wd, { recursive: true, force: true });
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
it('preserves a foreign inode injected after final uninstall rename validation', async () => {
|
|
2740
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-final-rename-claim-'));
|
|
2741
|
+
try {
|
|
2742
|
+
await withCwd(wd, async () => {
|
|
2743
|
+
const codexDir = join(wd, '.codex');
|
|
2744
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2745
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2746
|
+
const foreignConfig = Buffer.from('model = "foreign-final-rename"\n', 'utf-8');
|
|
2747
|
+
await mkdir(codexDir, { recursive: true });
|
|
2748
|
+
await writeFile(configPath, config);
|
|
2749
|
+
let foreignInode;
|
|
2750
|
+
await assert.rejects(uninstall({
|
|
2751
|
+
scope: 'project',
|
|
2752
|
+
transactionFailureInjector: (stage) => {
|
|
2753
|
+
if (stage !== 'after-final-rename-validation')
|
|
2754
|
+
return;
|
|
2755
|
+
const foreignPath = join(codexDir, '.foreign-final-rename-config');
|
|
2756
|
+
writeFileSync(foreignPath, foreignConfig);
|
|
2757
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
2758
|
+
rmSync(configPath);
|
|
2759
|
+
renameSync(foreignPath, configPath);
|
|
2760
|
+
},
|
|
2761
|
+
}), /(?:replacement claim.*not the planned artifact|planned artifact .* changed)/);
|
|
2762
|
+
assert.ok(foreignInode);
|
|
2763
|
+
assert.equal(lstatSync(configPath).ino, foreignInode);
|
|
2764
|
+
assert.deepEqual(await readFile(configPath), foreignConfig);
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
finally {
|
|
2768
|
+
await rm(wd, { recursive: true, force: true });
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
it('preserves a foreign inode injected after final uninstall removal validation', async () => {
|
|
2772
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-final-remove-claim-'));
|
|
2773
|
+
try {
|
|
2774
|
+
await withCwd(wd, async () => {
|
|
2775
|
+
const codexDir = join(wd, '.codex');
|
|
2776
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2777
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
2778
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2779
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`, 'utf-8');
|
|
2780
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', 'utf-8');
|
|
2781
|
+
await mkdir(codexDir, { recursive: true });
|
|
2782
|
+
await writeFile(configPath, config);
|
|
2783
|
+
await writeFile(hooksPath, hooks);
|
|
2784
|
+
let foreignInode;
|
|
2785
|
+
await assert.rejects(uninstall({
|
|
2786
|
+
scope: 'project',
|
|
2787
|
+
transactionFailureInjector: (stage) => {
|
|
2788
|
+
if (stage !== 'after-final-remove-validation')
|
|
2789
|
+
return;
|
|
2790
|
+
const foreignPath = join(codexDir, '.foreign-final-remove-hooks');
|
|
2791
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
2792
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
2793
|
+
rmSync(hooksPath);
|
|
2794
|
+
renameSync(foreignPath, hooksPath);
|
|
2795
|
+
},
|
|
2796
|
+
}), /removal claim.*not the planned artifact/);
|
|
2797
|
+
assert.ok(foreignInode);
|
|
2798
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
2799
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
2800
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
finally {
|
|
2804
|
+
await rm(wd, { recursive: true, force: true });
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
it('preserves a foreign inode injected after final uninstall rollback validation', async () => {
|
|
2808
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-uninstall-final-restore-claim-'));
|
|
2809
|
+
try {
|
|
2810
|
+
await withCwd(wd, async () => {
|
|
2811
|
+
const codexDir = join(wd, '.codex');
|
|
2812
|
+
const configPath = join(codexDir, 'config.toml');
|
|
2813
|
+
const hooksPath = join(codexDir, 'hooks.json');
|
|
2814
|
+
const config = Buffer.from(buildOmxConfig(), 'utf-8');
|
|
2815
|
+
const hooks = Buffer.from(`${JSON.stringify(buildManagedCodexHooksConfig(packageRoot()), null, 2)}\n`, 'utf-8');
|
|
2816
|
+
const foreignHooks = Buffer.from('{"hooks":{"Stop":[]}}\n', 'utf-8');
|
|
2817
|
+
await mkdir(codexDir, { recursive: true });
|
|
2818
|
+
await writeFile(configPath, config);
|
|
2819
|
+
await writeFile(hooksPath, hooks);
|
|
2820
|
+
let foreignInode;
|
|
2821
|
+
await assert.rejects(uninstall({
|
|
2822
|
+
scope: 'project',
|
|
2823
|
+
transactionFailureInjector: (stage) => {
|
|
2824
|
+
if (stage === 'before-config-commit')
|
|
2825
|
+
throw new Error('injected rollback start');
|
|
2826
|
+
if (stage !== 'after-final-restore-validation')
|
|
2827
|
+
return;
|
|
2828
|
+
const foreignPath = join(codexDir, '.foreign-final-restore-hooks');
|
|
2829
|
+
writeFileSync(foreignPath, foreignHooks);
|
|
2830
|
+
foreignInode = lstatSync(foreignPath).ino;
|
|
2831
|
+
renameSync(foreignPath, hooksPath);
|
|
2832
|
+
},
|
|
2833
|
+
}), /Uninstall artifact rollback failed/);
|
|
2834
|
+
assert.ok(foreignInode);
|
|
2835
|
+
assert.equal(lstatSync(hooksPath).ino, foreignInode);
|
|
2836
|
+
assert.deepEqual(await readFile(hooksPath), foreignHooks);
|
|
2837
|
+
assert.deepEqual(await readFile(configPath), config);
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
finally {
|
|
2841
|
+
await rm(wd, { recursive: true, force: true });
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
1097
2844
|
});
|
|
1098
2845
|
describe('stripOmxFeatureFlags', () => {
|
|
1099
2846
|
it('removes OMX feature flags and preserves user flags', async () => {
|