claude-dev-env 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +8 -2
- package/_shared/pr-loop/CLAUDE.md +1 -1
- package/_shared/pr-loop/audit-contract.md +17 -6
- package/_shared/pr-loop/audit-reply-template.md +4 -4
- package/_shared/pr-loop/code-rules-gate.md +3 -5
- package/_shared/pr-loop/fix-protocol.md +2 -3
- package/_shared/pr-loop/gh-payloads.md +1 -1
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/README.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
- package/_shared/pr-loop/scripts/preflight.py +9 -4
- package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
- package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/state-schema.md +5 -14
- package/agents/CLAUDE.md +2 -2
- package/agents/clean-coder.md +62 -562
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +58 -40
- package/audit-rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/audit-categories.json +704 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
- package/bin/CLAUDE.md +16 -5
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/install-plan.mjs +402 -0
- package/bin/install-transaction.mjs +455 -0
- package/bin/install.mjs +593 -147
- package/bin/install.plan.test.mjs +194 -0
- package/bin/install.profile-root.test.mjs +154 -0
- package/bin/install.profiles.test.mjs +253 -0
- package/bin/install.settings-defaults.test.mjs +200 -0
- package/bin/install.transaction.test.mjs +400 -0
- package/bin/install.uninstall-transaction.test.mjs +418 -0
- package/bin/merge_managed_permissions.mjs +130 -0
- package/bin/resolve-install-root.mjs +181 -0
- package/bin/select-install-targets.mjs +401 -0
- package/commands/CLAUDE.md +0 -2
- package/docs/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +3 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/prose-style-enforcement.md +25 -0
- package/docs/references/team-advisor-skill.md +14 -8
- package/hooks/blocking/CLAUDE.md +6 -6
- package/hooks/blocking/_path_setup.py +9 -5
- package/hooks/blocking/code_rules_docstrings.py +124 -30
- package/hooks/blocking/code_rules_enforcer.py +161 -16
- package/hooks/blocking/code_rules_shared.py +40 -23
- package/hooks/blocking/config/CLAUDE.md +3 -5
- package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
- package/hooks/blocking/eli11_reply_enforcer.py +70 -113
- package/hooks/blocking/hedging_language_blocker.py +103 -20
- package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
- package/hooks/blocking/intent_only_ending_blocker.py +6 -0
- package/hooks/blocking/plain_language_blocker.py +139 -20
- package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
- package/hooks/blocking/state_description_blocker.py +7 -1
- package/hooks/blocking/tdd_enforcer.py +8 -0
- package/hooks/blocking/test__path_setup.py +28 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
- package/hooks/blocking/test_hedging_language_blocker.py +120 -1
- package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
- package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
- package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
- package/hooks/blocking/test_plain_language_blocker.py +129 -19
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
- package/hooks/blocking/test_state_description_blocker.py +45 -2
- package/hooks/blocking/test_stop_dispatcher.py +11 -7
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
- package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/CLAUDE.md +14 -3
- package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
- package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/project_paths_reader.py +31 -4
- package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
- package/hooks/hooks_constants/pytest_invocation.py +354 -0
- package/hooks/hooks_constants/session_start_injector.py +163 -0
- package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
- package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
- package/hooks/hooks_constants/shell_command_segments.py +5 -0
- package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
- package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
- package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
- package/hooks/hooks_constants/test_session_start_injector.py +168 -0
- package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
- package/hooks/observability/CLAUDE.md +2 -0
- package/hooks/observability/prose_matcher_advisory.py +237 -0
- package/hooks/observability/test_prose_matcher_advisory.py +143 -0
- package/hooks/session/CLAUDE.md +9 -1
- package/hooks/session/_path_setup.py +13 -0
- package/hooks/session/issue_tracker_session_starter.py +135 -0
- package/hooks/session/orchestrator_auto_starter.py +100 -0
- package/hooks/session/test__path_setup.py +28 -0
- package/hooks/session/test_issue_tracker_session_starter.py +104 -0
- package/hooks/session/test_orchestrator_auto_starter.py +99 -0
- package/hooks/session/test_working_style_prompt.py +47 -0
- package/hooks/session/untracked_repo_detector.py +1 -24
- package/hooks/session/working_style_prompt.py +36 -0
- package/hooks/validators/_path_setup.py +19 -0
- package/hooks/validators/run_all_validators.py +8 -13
- package/installable-surfaces.manifest.json +21 -0
- package/output-styles/CLAUDE.md +15 -0
- package/package.json +5 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/code-standards.md +33 -7
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +7 -2
- package/rules/hedging-claims.md +4 -2
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/opus5-communication-contract.md +45 -0
- package/rules/plain-language.md +2 -2
- package/rules/research-mode.md +1 -1
- package/scripts/CLAUDE.md +13 -2
- package/scripts/Sync-RepoMain.ps1 +215 -0
- package/scripts/active_capability_references.py +218 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
- package/scripts/claude_chain_runner.py +394 -6
- package/scripts/claude_chain_usage.py +1 -1
- package/scripts/codex_compat_materializer.py +105 -85
- package/scripts/dev_env_scripts_constants/CLAUDE.md +3 -1
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
- package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
- package/scripts/spawn_grok_batch.py +556 -9
- package/scripts/test_active_capability_references.py +108 -0
- package/scripts/test_claude_chain_runner.py +414 -82
- package/scripts/test_claude_chain_usage.py +12 -12
- package/scripts/test_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_spawn_grok_batch.py +295 -0
- package/scripts/test_verify_installable_package.py +208 -0
- package/scripts/tests/test_codex_compat_materializer.py +33 -0
- package/scripts/verify_installable_package.py +612 -0
- package/settings.json +10 -0
- package/skills/CLAUDE.md +6 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
- package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
- package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
- package/skills/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -0
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/stop-conditions.md +5 -3
- package/skills/beat-sheet/SKILL.md +54 -0
- package/skills/beat-sheet/reference/visual-beats.md +29 -0
- package/skills/bugteam/CONSTRAINTS.md +4 -4
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/e-code-review/SKILL.md +29 -3
- package/skills/e-code-review/reference/effort-evaluation.md +35 -0
- package/skills/e-code-review/reference/medium.md +15 -4
- package/skills/e-code-review/reference/runner-selection.md +40 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
- package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
- package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
- package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
- package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
- package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
- package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
- package/skills/e-code-review/scripts/grok_code_review.py +230 -0
- package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
- package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +289 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +5 -0
- package/skills/plan-to-pr/scripts/create_packet.py +4 -4
- package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
- package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
- package/skills/plan-to-pr/scripts/validate_run.py +4 -1
- package/skills/pr-converge/scripts/check_convergence.py +21 -19
- package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
- package/skills/pr-converge/scripts/conftest.py +35 -0
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/rebase/SKILL.md +15 -3
- package/skills/reviewer-gates/SKILL.md +2 -2
- package/skills/show/SKILL.md +51 -0
- package/skills/show/references/accessibility.md +7 -0
- package/skills/show/references/art.md +3 -0
- package/skills/show/references/charts.md +3 -0
- package/skills/show/references/core-design.md +14 -0
- package/skills/show/references/erds.md +3 -0
- package/skills/show/references/flowcharts.md +3 -0
- package/skills/show/references/host-and-html.md +3 -0
- package/skills/show/references/illustrative-diagrams.md +10 -0
- package/skills/show/references/interaction.md +3 -0
- package/skills/show/references/mockups.md +3 -0
- package/skills/show/references/quality-gates.md +7 -0
- package/skills/show/references/structural-diagrams.md +3 -0
- package/skills/show/references/subject-inventory.md +21 -0
- package/skills/show/references/svg-contract.md +22 -0
- package/skills/show/routing.yaml +30 -0
- package/skills/show/samples/pr1262-v2.svg +222 -0
- package/skills/show/scripts/README.md +6 -0
- package/skills/show/scripts/validate-artifact.py +91 -0
- package/skills/show/scripts/validate-package.py +18 -0
- package/skills/show/templates/html-widget.html +4 -0
- package/skills/show/templates/svg-base.svg +19 -0
- package/skills/show/tests/fixtures/css-var.svg +6 -0
- package/skills/show/tests/fixtures/dead-ref.svg +7 -0
- package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
- package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
- package/skills/show/tests/fixtures/invalid.svg +1 -0
- package/skills/show/tests/fixtures/large-canvas.svg +21 -0
- package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
- package/skills/show/tests/fixtures/valid.html +1 -0
- package/skills/show/tests/test_validate-artifact.py +74 -0
- package/skills/show/tests/test_validators.py +59 -0
- package/skills/show/workflows/create-visual.md +13 -0
- package/skills/show/workflows/review-visual.md +20 -0
- package/skills/split-pr/SKILL.md +85 -0
- package/skills/split-pr/reference/path-layers.md +16 -0
- package/skills/split-pr/reference/proposal-format.md +15 -0
- package/skills/split-pr/reference/split-further-loop.md +10 -0
- package/skills/split-pr/reference/splitting-principles.md +26 -0
- package/skills/split-pr/scripts/analyze_pr.py +279 -0
- package/skills/split-pr/scripts/categorize_files.py +106 -0
- package/skills/split-pr/scripts/config/__init__.py +1 -0
- package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
- package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
- package/skills/split-pr/scripts/config/packing_constants.py +61 -0
- package/skills/split-pr/scripts/config/plan_constants.py +49 -0
- package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
- package/skills/split-pr/scripts/execute_split_slices.py +82 -0
- package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
- package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
- package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
- package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
- package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
- package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
- package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
- package/skills/split-pr/scripts/split_pr_title.py +41 -0
- package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
- package/skills/split-pr/scripts/test_categorize_files.py +55 -0
- package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
- package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
- package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
- package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
- package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
- package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
- package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
- package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
- package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
- package/skills/split-pr/scripts/test_verify_plan.py +56 -0
- package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
- package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
- package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
- package/skills/split-pr/scripts/verify_plan.py +139 -0
- package/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -44
- package/system-prompts/software-engineer.xml +17 -5
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/skills/test_markdown_link_integrity.py +0 -107
package/bin/CLAUDE.md
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
# bin
|
|
2
2
|
|
|
3
|
-
The installer and its companion modules. Running `npx claude-dev-env` (or `node bin/install.mjs`) copies package files into `~/.claude
|
|
3
|
+
The installer and its companion modules. Running `npx claude-dev-env` (or `node bin/install.mjs`) copies package files into the managed root (`~/.claude/` by default; `CLAUDE_CONFIG_DIR` or `--target` selects another), merges hook entries into that root's `settings.json`, installs Git hooks, and writes `~/.mypy.ini` under the process home.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|---|---|
|
|
9
|
-
| `install.mjs` | Main installer:
|
|
10
|
-
| `install-
|
|
9
|
+
| `install.mjs` | Main installer: builds a read-only plan via `install-plan.mjs`, then runs mutations inside `install-transaction.mjs` recovery (copy content directories, merge hooks, install skills, prune, git hooks, mypy.ini); routes `CLAUDE_HOME`, the manifest path, and `~/.mypy.ini` through `resolve-install-root.mjs`; resolves single or multi-profile targets before mutation and writes one ownership manifest per target |
|
|
10
|
+
| `resolve-install-root.mjs` | Pure install-root resolver: precedence `--target` > `CLAUDE_CONFIG_DIR` > `~/.claude`, separator-boundary containment, and the declared external allowlist for `~/.mypy.ini` |
|
|
11
|
+
| `select-install-targets.mjs` | Pure target selection for main-default, explicit `--target`, and `--profile`/`--profiles`; rejects ambiguous or duplicate targets; builds per-target manifest records with `targetIdentity` and `managedRoot` |
|
|
12
|
+
| `install-plan.mjs` | Read-only install and uninstall plans: install preflight (managed root, source conflicts, Python, settings when hooks install) and uninstall preflight (settings JSON before removal, removable vs skipped manifest records), freezes plans E2/F execute |
|
|
13
|
+
| `install-transaction.mjs` | Install, update, and uninstall transaction journal: captures prior settings, manifest, managed files, and `core.hooksPath`, restores them on failure, and supports fault injection phases for recovery tests |
|
|
14
|
+
| `install.transaction.test.mjs` | Unit and sandbox installer tests for snapshot/restore and fault phases (`after_file_staging`, `after_settings_write`, `after_git_config`, `after_manifest_write`) |
|
|
15
|
+
| `install.uninstall-transaction.test.mjs` | Uninstall plan preflight and recovery: malformed/non-object settings fail before removal, each fault phase restores files/settings/manifest/`core.hooksPath`, retry succeeds, selected-root containment |
|
|
16
|
+
| `install.profile-root.test.mjs` | Contract tests for the install-root resolver: precedence, containment boundary, external allowlist, and the install.mjs import smoke check |
|
|
17
|
+
| `install-constants.mjs` | The named values `install.mjs` reads: `SKIPPED_SOURCE_ENTRY_NAMES` and `SKIPPED_SOURCE_FILE_EXTENSIONS` for the build artifacts the source walk leaves behind, `RUN_BACKUP_DIRECTORY_NAME_PATTERN` for the timestamp shape a run backup directory carries, `MANAGED_SKILLS_DIRECTORY_NAME` and `MANAGED_HOOKS_DIRECTORY_NAME` for the directory name each of those trees carries in a package source and under `~/.claude` — read by the copy loops, the hooks.json reads, the git-hook shims, the mypy configuration, and the prunes alike — `SETTINGS_FILE_NAME` for the settings file the merge, the retired-hook prune, and the uninstall purge share, and `MYPY_INI_FILE_NAME` for the home-directory file `install_mypy_ini.mjs` writes (also imported by `resolve-install-root.mjs`) |
|
|
11
18
|
| `ever-shipped-skills.mjs` | Static `EVER_SHIPPED_SKILL_NAMES` set of every top-level skill directory name the package has shipped; the installer subtracts the current skill set from it to prune retired skills left under `~/.claude/skills` |
|
|
12
19
|
| `expand_home_directory_tokens.mjs` | Expands residual `$HOME` / `${HOME}` / `~/` tokens in settings.json hook and statusLine commands to absolute home paths at install time (literal-safe for homes that contain `$`) |
|
|
13
20
|
| `git_hooks_installer.mjs` | Installs or updates the `pre-commit`, `pre-push`, and `post-commit` Git hooks in the user's git config; writes hook scripts that delegate to the installed Python hooks |
|
|
14
21
|
| `install_mypy_ini.mjs` | Writes `~/.mypy.ini` with settings that make mypy find the hooks package and enforce strict type checking |
|
|
15
22
|
| `install.test.mjs` | Unit tests for `install.mjs` — covers conflict detection, interpreter detection, settings merging, the settings shapes the installer never wrote — those every hook walk hands back untouched, and the shipped-event value the merge replaces with a warning — the source-artifact skip in `collectFiles`, the case-only rename decision and the `copyTree` copy that acts on it, the retired-hook diff and settings prune, the stale-file prune: the manifest diff, path-key case folding, emptied-parent cleanup, and the warn-and-keep paths, and backup retention: the sweep a moved-content run drives, the empty root a run whose moves failed gives up, and the populated root retention keeps |
|
|
23
|
+
| `install.profiles.test.mjs` | Profile target-selection and per-target ownership manifest contract tests (main-default, multi-profile, ambiguity/duplicate rejection, help text) |
|
|
24
|
+
| `install.plan.test.mjs` | Read-only plan and preflight tests: zero-write plan construction, source-conflict and missing-Python fail-closed, settings check only when hooks install, tolerant broken-manifest, invalid managed root, mutation-kind list for E2 |
|
|
16
25
|
| `install.prune.test.mjs` | End-to-end prune tests that run the real installer against a sandbox `HOME` — retired-skill, retired-hook, and stale-file moves into one timestamped backup, the settings entry a retired hook loses, the top-level paths every root's diff leaves alone, the manifest record a failed move keeps, the full-install and resolved-dependency gates, backup retention, and the uninstall: the `~/.mypy.ini` removal, the containment guard, and nested-directory cleanup |
|
|
17
26
|
| `git_hooks_installer.test.mjs` | Tests for `git_hooks_installer.mjs` |
|
|
18
27
|
| `install_mypy_ini.test.mjs` | Tests for `install_mypy_ini.mjs` |
|
|
@@ -67,9 +76,11 @@ A run that moves nothing sweeps nothing, so every recovery point the user holds
|
|
|
67
76
|
|
|
68
77
|
## Uninstall
|
|
69
78
|
|
|
70
|
-
`--uninstall`
|
|
79
|
+
`--uninstall` builds a read-only uninstall plan, captures a recovery snapshot, then removes each file the plan lists.
|
|
71
80
|
|
|
72
|
-
Each record passes a containment guard
|
|
81
|
+
Settings JSON is validated before any removal. A malformed or non-object `settings.json` fails closed with the managed files still on disk. Each manifest record passes a containment guard: the path resolves under `~/.claude`, or it names the `~/.mypy.ini` the install writes in the home directory. Every other record is skipped with a warning and counted. Skipping keeps one malformed record — hand-edited, or written by an installer that ran against a different home — from stranding the user with a half-removed install. The purge removes every legitimate record, clears the manifest, and reports the skipped count.
|
|
82
|
+
|
|
83
|
+
The uninstall runs inside the same snapshot/restore journal as install: prior settings, manifest, managed files, and `core.hooksPath` restore when a later phase fails, so a retry starts from a complete ownership record. The journal is discarded only after a successful commit.
|
|
73
84
|
|
|
74
85
|
Removing a file leaves its directory a candidate for cleanup. Once the file loop ends, the purge walks up from each such directory to the managed top-level directory the file sits under (`MANAGED_TOP_LEVEL_DIRECTORY_NAMES`), removing each directory it finds empty. That reaches a nested tree such as `skills/<name>/scripts/`. A record under no managed root gets no walk, so `~/.claude` itself is never a stop root and a directory the installer never wrote stays. A separate pass drops each managed top-level directory the purge empties.
|
|
75
86
|
|
|
@@ -21,6 +21,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
21
21
|
'auditing-claude-config',
|
|
22
22
|
'autoconverge',
|
|
23
23
|
'bdd-protocol',
|
|
24
|
+
'beat-sheet',
|
|
24
25
|
'bg-agent',
|
|
25
26
|
'bugteam',
|
|
26
27
|
'caveman',
|
|
@@ -71,4 +72,5 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
|
|
|
71
72
|
'update',
|
|
72
73
|
'usage-pause',
|
|
73
74
|
'verified-build',
|
|
75
|
+
'split-pr',
|
|
74
76
|
]);
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only install and uninstall plans for claude-dev-env.
|
|
3
|
+
*
|
|
4
|
+
* Builds a frozen plan before any write. E2 consumes the install plan; F consumes
|
|
5
|
+
* the uninstall plan. Plan construction performs zero writes.
|
|
6
|
+
*
|
|
7
|
+
* ::
|
|
8
|
+
*
|
|
9
|
+
* plan = buildInstallPlan({ packageRoot, managedRoot, ... })
|
|
10
|
+
* // preflight failed: InstallPlanPreflightError, disk unchanged
|
|
11
|
+
* // ok: frozen plan with pythonCommand, priorManifest, mutation kinds
|
|
12
|
+
*
|
|
13
|
+
* uninstallPlan = buildUninstallPlan({ managedRoot, manifestFilePath, ... })
|
|
14
|
+
* // malformed settings: fail before any removal
|
|
15
|
+
*
|
|
16
|
+
* Install manifest parse stays tolerant. Uninstall validates settings when present.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { SETTINGS_FILE_NAME } from './install-constants.mjs';
|
|
22
|
+
|
|
23
|
+
export const PREFLIGHT_ERROR_CODES = Object.freeze({
|
|
24
|
+
SOURCE_CONFLICTS: 'source_conflicts',
|
|
25
|
+
MISSING_PYTHON: 'missing_python',
|
|
26
|
+
MALFORMED_SETTINGS: 'malformed_settings',
|
|
27
|
+
INVALID_MANAGED_ROOT: 'invalid_managed_root',
|
|
28
|
+
MISSING_MANIFEST: 'missing_manifest',
|
|
29
|
+
MALFORMED_MANIFEST: 'malformed_manifest',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const MISSING_PYTHON_ERROR_MESSAGE =
|
|
33
|
+
'ERROR: No usable Python 3 found. Install Python 3.8+ from python.org and ensure py, python3, or python is on PATH. On Windows the Microsoft Store python.exe alias is rejected because it cannot run hooks.';
|
|
34
|
+
|
|
35
|
+
export const MALFORMED_SETTINGS_ERROR_MESSAGE =
|
|
36
|
+
'ERROR: settings.json is malformed JSON. Fix it and rerun.';
|
|
37
|
+
|
|
38
|
+
export const SETTINGS_NOT_OBJECT_ERROR_MESSAGE =
|
|
39
|
+
'ERROR: settings.json holds a value other than a JSON object. Fix it and rerun.';
|
|
40
|
+
|
|
41
|
+
export const MISSING_MANIFEST_ERROR_MESSAGE =
|
|
42
|
+
'No installation manifest found. Nothing to uninstall.';
|
|
43
|
+
|
|
44
|
+
export const MALFORMED_MANIFEST_ERROR_MESSAGE =
|
|
45
|
+
'ERROR: installation manifest is malformed JSON. Fix it and rerun.';
|
|
46
|
+
|
|
47
|
+
const MANIFEST_FILES_KEY = 'files';
|
|
48
|
+
const MANIFEST_SKILLS_KEY = 'skills';
|
|
49
|
+
const MANIFEST_MANAGED_PERMISSIONS_KEY = 'managedPermissions';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Preflight failure that leaves the managed installation untouched.
|
|
53
|
+
*/
|
|
54
|
+
export class InstallPlanPreflightError extends Error {
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} message
|
|
57
|
+
* @param {{ code: string, conflicts?: Array<{ statusCode: string, path: string }> }} details
|
|
58
|
+
*/
|
|
59
|
+
constructor(message, details) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = 'InstallPlanPreflightError';
|
|
62
|
+
this.code = details.code;
|
|
63
|
+
this.conflicts = details.conflicts || [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the selected groups install hook files and merge settings.
|
|
69
|
+
*
|
|
70
|
+
* @param {string[]|null} selectedGroups
|
|
71
|
+
* @param {Record<string, object>} installGroups
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function planShouldInstallHooks(selectedGroups, installGroups) {
|
|
75
|
+
if (!selectedGroups) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return selectedGroups
|
|
79
|
+
.map((eachName) => installGroups[eachName])
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.some(
|
|
82
|
+
(eachGroup) => eachGroup.includeAllHooks
|
|
83
|
+
|| (Array.isArray(eachGroup.includeHookFiles) && eachGroup.includeHookFiles.length > 0),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read prior manifest lists without throwing on bad JSON.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} manifestFilePath
|
|
91
|
+
* @param {{ existsSync?: typeof existsSync, readFileSync?: typeof readFileSync }} [io]
|
|
92
|
+
* @returns {{ files: string[]|null, skills: string[]|null }}
|
|
93
|
+
*/
|
|
94
|
+
function arrayFieldOrNull(record, fieldName) {
|
|
95
|
+
const fieldValue = record[fieldName];
|
|
96
|
+
return Array.isArray(fieldValue) ? fieldValue : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function readPriorManifestArraysFromPath(manifestFilePath, io = {}) {
|
|
100
|
+
const exists = io.existsSync || existsSync;
|
|
101
|
+
const readFile = io.readFileSync || readFileSync;
|
|
102
|
+
const missingRecord = { files: null, skills: null };
|
|
103
|
+
if (!manifestFilePath || !exists(manifestFilePath)) {
|
|
104
|
+
return missingRecord;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const priorManifest = JSON.parse(readFile(manifestFilePath, 'utf8'));
|
|
108
|
+
return {
|
|
109
|
+
files: arrayFieldOrNull(priorManifest, MANIFEST_FILES_KEY),
|
|
110
|
+
skills: arrayFieldOrNull(priorManifest, MANIFEST_SKILLS_KEY),
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
return missingRecord;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fail when managedRoot is empty or points at a non-directory path.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} managedRoot
|
|
121
|
+
* @param {{ existsSync?: typeof existsSync, statSync?: typeof statSync }} [io]
|
|
122
|
+
* @returns {void}
|
|
123
|
+
*/
|
|
124
|
+
export function preflightManagedRoot(managedRoot, io = {}) {
|
|
125
|
+
const exists = io.existsSync || existsSync;
|
|
126
|
+
const stat = io.statSync || statSync;
|
|
127
|
+
if (typeof managedRoot !== 'string' || managedRoot.trim() === '') {
|
|
128
|
+
throw new InstallPlanPreflightError(
|
|
129
|
+
'ERROR: install managed root is empty or missing.',
|
|
130
|
+
{ code: PREFLIGHT_ERROR_CODES.INVALID_MANAGED_ROOT },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (!exists(managedRoot)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let stats;
|
|
137
|
+
try {
|
|
138
|
+
stats = stat(managedRoot);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new InstallPlanPreflightError(
|
|
141
|
+
`ERROR: install managed root is not readable: ${managedRoot}`,
|
|
142
|
+
{ code: PREFLIGHT_ERROR_CODES.INVALID_MANAGED_ROOT },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (!stats.isDirectory()) {
|
|
146
|
+
throw new InstallPlanPreflightError(
|
|
147
|
+
`ERROR: install managed root is not a directory: ${managedRoot}`,
|
|
148
|
+
{ code: PREFLIGHT_ERROR_CODES.INVALID_MANAGED_ROOT },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parse existing settings.json when the run will merge hooks.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} settingsPath
|
|
157
|
+
* @param {boolean} shouldInstallHooks
|
|
158
|
+
* @param {{ existsSync?: typeof existsSync, readFileSync?: typeof readFileSync }} [io]
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
export function preflightSettingsIfNeeded(settingsPath, shouldInstallHooks, io = {}) {
|
|
162
|
+
if (!shouldInstallHooks) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const exists = io.existsSync || existsSync;
|
|
166
|
+
const readFile = io.readFileSync || readFileSync;
|
|
167
|
+
if (!exists(settingsPath)) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const raw = readFile(settingsPath, 'utf8').trim();
|
|
171
|
+
if (!raw) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
let settings;
|
|
175
|
+
try {
|
|
176
|
+
settings = JSON.parse(raw);
|
|
177
|
+
} catch {
|
|
178
|
+
throw new InstallPlanPreflightError(MALFORMED_SETTINGS_ERROR_MESSAGE, {
|
|
179
|
+
code: PREFLIGHT_ERROR_CODES.MALFORMED_SETTINGS,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
183
|
+
throw new InstallPlanPreflightError(SETTINGS_NOT_OBJECT_ERROR_MESSAGE, {
|
|
184
|
+
code: PREFLIGHT_ERROR_CODES.MALFORMED_SETTINGS,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Build a frozen installation plan after read-only preflight.
|
|
191
|
+
*
|
|
192
|
+
* @param {{
|
|
193
|
+
* packageRoot: string,
|
|
194
|
+
* managedRoot: string,
|
|
195
|
+
* manifestFilePath: string,
|
|
196
|
+
* targetIdentity: string,
|
|
197
|
+
* selectedGroups: string[]|null,
|
|
198
|
+
* isUpdateRefresh: boolean,
|
|
199
|
+
* installGroups: Record<string, object>,
|
|
200
|
+
* collectSourceConflicts: (packageRoot: string) => Array<{ statusCode: string, path: string }>,
|
|
201
|
+
* detectPythonCommand: () => string|null,
|
|
202
|
+
* packageName?: string,
|
|
203
|
+
* io?: {
|
|
204
|
+
* existsSync?: typeof existsSync,
|
|
205
|
+
* readFileSync?: typeof readFileSync,
|
|
206
|
+
* statSync?: typeof statSync,
|
|
207
|
+
* },
|
|
208
|
+
* }} input
|
|
209
|
+
* @returns {Readonly<object>}
|
|
210
|
+
*/
|
|
211
|
+
export function buildInstallPlan(input) {
|
|
212
|
+
const io = input.io || {};
|
|
213
|
+
const exists = io.existsSync || existsSync;
|
|
214
|
+
const packageName = input.packageName || 'claude-dev-env';
|
|
215
|
+
|
|
216
|
+
preflightManagedRoot(input.managedRoot, io);
|
|
217
|
+
|
|
218
|
+
const priorManifest = readPriorManifestArraysFromPath(input.manifestFilePath, io);
|
|
219
|
+
const shouldInstallHooks = planShouldInstallHooks(input.selectedGroups, input.installGroups);
|
|
220
|
+
const settingsPath = join(input.managedRoot, SETTINGS_FILE_NAME);
|
|
221
|
+
|
|
222
|
+
const allConflicts = input.collectSourceConflicts(input.packageRoot);
|
|
223
|
+
if (allConflicts.length > 0) {
|
|
224
|
+
const conflictLines = allConflicts
|
|
225
|
+
.map((eachConflict) => ` ${eachConflict.statusCode} ${eachConflict.path}`)
|
|
226
|
+
.join('\n');
|
|
227
|
+
throw new InstallPlanPreflightError(
|
|
228
|
+
`\nERROR: ${packageName} source has unmerged conflicts under ${input.packageRoot}:\n\n`
|
|
229
|
+
+ `${conflictLines}\n\n`
|
|
230
|
+
+ 'Resolve the conflicts in the package source before running the installer.\n'
|
|
231
|
+
+ 'Installing from a conflicted source can copy stale or broken files into ~/.claude/.\n',
|
|
232
|
+
{
|
|
233
|
+
code: PREFLIGHT_ERROR_CODES.SOURCE_CONFLICTS,
|
|
234
|
+
conflicts: allConflicts,
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const pythonCommand = input.detectPythonCommand();
|
|
240
|
+
if (!pythonCommand) {
|
|
241
|
+
throw new InstallPlanPreflightError(MISSING_PYTHON_ERROR_MESSAGE, {
|
|
242
|
+
code: PREFLIGHT_ERROR_CODES.MISSING_PYTHON,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
preflightSettingsIfNeeded(settingsPath, shouldInstallHooks, io);
|
|
247
|
+
|
|
248
|
+
const isUpdateRefresh = Boolean(input.isUpdateRefresh);
|
|
249
|
+
const shouldPurgeBeforeReinstall = isUpdateRefresh
|
|
250
|
+
&& !input.selectedGroups
|
|
251
|
+
&& exists(input.manifestFilePath);
|
|
252
|
+
|
|
253
|
+
return Object.freeze({
|
|
254
|
+
packageRoot: input.packageRoot,
|
|
255
|
+
managedRoot: input.managedRoot,
|
|
256
|
+
manifestFilePath: input.manifestFilePath,
|
|
257
|
+
targetIdentity: input.targetIdentity,
|
|
258
|
+
selectedGroups: input.selectedGroups,
|
|
259
|
+
isUpdateRefresh,
|
|
260
|
+
shouldPurgeBeforeReinstall,
|
|
261
|
+
pythonCommand,
|
|
262
|
+
shouldInstallHooks,
|
|
263
|
+
priorManifest: Object.freeze({
|
|
264
|
+
files: priorManifest.files,
|
|
265
|
+
skills: priorManifest.skills,
|
|
266
|
+
}),
|
|
267
|
+
settingsPath,
|
|
268
|
+
packageName,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* High-level mutation kinds the executor applies for this plan (E2 surface).
|
|
274
|
+
*
|
|
275
|
+
* @param {{
|
|
276
|
+
* shouldPurgeBeforeReinstall: boolean,
|
|
277
|
+
* shouldInstallHooks: boolean,
|
|
278
|
+
* selectedGroups: string[]|null,
|
|
279
|
+
* }} plan
|
|
280
|
+
* @returns {ReadonlyArray<string>}
|
|
281
|
+
*/
|
|
282
|
+
export function describeInstallMutations(plan) {
|
|
283
|
+
/** @type {string[]} */
|
|
284
|
+
const allMutations = [];
|
|
285
|
+
if (plan.shouldPurgeBeforeReinstall) {
|
|
286
|
+
allMutations.push('purge_managed_installation');
|
|
287
|
+
}
|
|
288
|
+
allMutations.push('ensure_managed_root', 'copy_content_trees', 'copy_skills');
|
|
289
|
+
if (plan.shouldInstallHooks) {
|
|
290
|
+
allMutations.push(
|
|
291
|
+
'copy_hook_files',
|
|
292
|
+
'merge_hooks_settings',
|
|
293
|
+
'install_git_hooks',
|
|
294
|
+
'install_mypy_ini',
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
allMutations.push('copy_claude_hub');
|
|
298
|
+
if (!plan.selectedGroups) {
|
|
299
|
+
allMutations.push('prune_retired_and_stale');
|
|
300
|
+
}
|
|
301
|
+
allMutations.push('write_manifest');
|
|
302
|
+
return Object.freeze(allMutations);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Build a frozen uninstall plan after read-only preflight.
|
|
307
|
+
*
|
|
308
|
+
* Validates settings.json when present so a malformed file fails before any
|
|
309
|
+
* managed file is removed. Classifies each manifest path through the caller's
|
|
310
|
+
* removable-record predicate.
|
|
311
|
+
*
|
|
312
|
+
* ::
|
|
313
|
+
*
|
|
314
|
+
* plan = buildUninstallPlan({ managedRoot, manifestFilePath, isRemovableRecord })
|
|
315
|
+
* // malformed settings → InstallPlanPreflightError, disk unchanged
|
|
316
|
+
* // ok: frozen removableFiles / skippedFiles for the executor
|
|
317
|
+
*
|
|
318
|
+
* @param {{
|
|
319
|
+
* managedRoot: string,
|
|
320
|
+
* manifestFilePath: string,
|
|
321
|
+
* requireManifest: boolean,
|
|
322
|
+
* isRemovableRecord: (absolutePath: string) => boolean,
|
|
323
|
+
* io?: {
|
|
324
|
+
* existsSync?: typeof existsSync,
|
|
325
|
+
* readFileSync?: typeof readFileSync,
|
|
326
|
+
* },
|
|
327
|
+
* }} input
|
|
328
|
+
* @returns {Readonly<object>}
|
|
329
|
+
*/
|
|
330
|
+
export function buildUninstallPlan(input) {
|
|
331
|
+
const io = input.io || {};
|
|
332
|
+
const exists = io.existsSync || existsSync;
|
|
333
|
+
const readFile = io.readFileSync || readFileSync;
|
|
334
|
+
const requireManifest = Boolean(input.requireManifest);
|
|
335
|
+
const settingsPath = join(input.managedRoot, SETTINGS_FILE_NAME);
|
|
336
|
+
|
|
337
|
+
if (!input.manifestFilePath || !exists(input.manifestFilePath)) {
|
|
338
|
+
if (requireManifest) {
|
|
339
|
+
throw new InstallPlanPreflightError(MISSING_MANIFEST_ERROR_MESSAGE, {
|
|
340
|
+
code: PREFLIGHT_ERROR_CODES.MISSING_MANIFEST,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return Object.freeze({
|
|
344
|
+
managedRoot: input.managedRoot,
|
|
345
|
+
manifestFilePath: input.manifestFilePath,
|
|
346
|
+
settingsPath,
|
|
347
|
+
manifestExisted: false,
|
|
348
|
+
removableFiles: Object.freeze([]),
|
|
349
|
+
skippedFiles: Object.freeze([]),
|
|
350
|
+
allManifestFiles: Object.freeze([]),
|
|
351
|
+
managedPermissionDenyEntries: Object.freeze([]),
|
|
352
|
+
isNoOp: true,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let manifest;
|
|
357
|
+
try {
|
|
358
|
+
manifest = JSON.parse(readFile(input.manifestFilePath, 'utf8'));
|
|
359
|
+
} catch {
|
|
360
|
+
throw new InstallPlanPreflightError(MALFORMED_MANIFEST_ERROR_MESSAGE, {
|
|
361
|
+
code: PREFLIGHT_ERROR_CODES.MALFORMED_MANIFEST,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
preflightSettingsIfNeeded(settingsPath, true, io);
|
|
366
|
+
|
|
367
|
+
const isManifestObject = manifest && typeof manifest === 'object' && !Array.isArray(manifest);
|
|
368
|
+
const rawFiles = isManifestObject ? manifest[MANIFEST_FILES_KEY] : null;
|
|
369
|
+
const allManifestFiles = Array.isArray(rawFiles)
|
|
370
|
+
? rawFiles.filter((eachPath) => typeof eachPath === 'string' && eachPath.trim() !== '')
|
|
371
|
+
: [];
|
|
372
|
+
const rawManagedDeny = isManifestObject
|
|
373
|
+
? manifest[MANIFEST_MANAGED_PERMISSIONS_KEY]?.deny
|
|
374
|
+
: null;
|
|
375
|
+
const managedPermissionDenyEntries = Array.isArray(rawManagedDeny)
|
|
376
|
+
? rawManagedDeny.filter((eachEntry) => typeof eachEntry === 'string' && eachEntry.trim() !== '')
|
|
377
|
+
: [];
|
|
378
|
+
|
|
379
|
+
/** @type {string[]} */
|
|
380
|
+
const removableFiles = [];
|
|
381
|
+
/** @type {string[]} */
|
|
382
|
+
const skippedFiles = [];
|
|
383
|
+
for (const eachPath of allManifestFiles) {
|
|
384
|
+
if (input.isRemovableRecord(eachPath)) {
|
|
385
|
+
removableFiles.push(eachPath);
|
|
386
|
+
} else {
|
|
387
|
+
skippedFiles.push(eachPath);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return Object.freeze({
|
|
392
|
+
managedRoot: input.managedRoot,
|
|
393
|
+
manifestFilePath: input.manifestFilePath,
|
|
394
|
+
settingsPath,
|
|
395
|
+
manifestExisted: true,
|
|
396
|
+
removableFiles: Object.freeze(removableFiles),
|
|
397
|
+
skippedFiles: Object.freeze(skippedFiles),
|
|
398
|
+
allManifestFiles: Object.freeze(allManifestFiles),
|
|
399
|
+
managedPermissionDenyEntries: Object.freeze(managedPermissionDenyEntries),
|
|
400
|
+
isNoOp: false,
|
|
401
|
+
});
|
|
402
|
+
}
|