claude-dev-env 2.9.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.
Files changed (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Install, update, and uninstall transaction journal for claude-dev-env.
3
+ *
4
+ * Captures the prior managed installation, runs mutations, and restores
5
+ * settings, manifest, managed files, and core.hooksPath when any phase fails.
6
+ *
7
+ * ::
8
+ *
9
+ * const snapshot = capturePriorInstallSnapshot({ managedRoot, ... })
10
+ * try {
11
+ * runWithInstallTransaction({ snapshot, faultPhase, runMutations })
12
+ * } catch {
13
+ * // prior installation restored
14
+ * }
15
+ */
16
+
17
+ import {
18
+ copyFileSync,
19
+ existsSync,
20
+ lstatSync,
21
+ mkdirSync,
22
+ mkdtempSync,
23
+ readdirSync,
24
+ readlinkSync,
25
+ rmdirSync,
26
+ rmSync,
27
+ symlinkSync,
28
+ unlinkSync,
29
+ writeFileSync,
30
+ } from 'node:fs';
31
+ import { dirname, join } from 'node:path';
32
+ import { execFileSync } from 'node:child_process';
33
+
34
+ /** Env var that forces a throw after a named mutation phase (tests only). */
35
+ export const INSTALL_FAULT_ENV = 'CLAUDE_DEV_ENV_INSTALL_FAULT';
36
+
37
+ /** Journal directory name under the managed root. */
38
+ export const TRANSACTION_JOURNAL_DIRECTORY_NAME = '.claude-dev-env-txn';
39
+
40
+ export const FAULT_PHASES = Object.freeze({
41
+ AFTER_FILE_STAGING: 'after_file_staging',
42
+ AFTER_SETTINGS_WRITE: 'after_settings_write',
43
+ AFTER_GIT_CONFIG: 'after_git_config',
44
+ AFTER_MANIFEST_WRITE: 'after_manifest_write',
45
+ BEFORE_DURABLE_PROMOTION: 'before_durable_promotion',
46
+ AFTER_LINK_PUBLICATION: 'after_link_publication',
47
+ });
48
+
49
+ const ALL_FAULT_PHASE_VALUES = Object.freeze(Object.values(FAULT_PHASES));
50
+
51
+ const META_FILE_NAME = 'meta.json';
52
+ const SETTINGS_BLOB_NAME = 'settings.json';
53
+ const MANIFEST_BLOB_NAME = 'manifest.json';
54
+ const FILES_DIRECTORY_NAME = 'files';
55
+ const ENTRY_KIND_FILE = 'file';
56
+ const ENTRY_KIND_SYMLINK = 'symlink';
57
+ const ENTRY_KIND_MISSING = 'missing';
58
+
59
+ /**
60
+ * Fault injection that aborts the install after a named phase.
61
+ */
62
+ export class InstallTransactionFaultError extends Error {
63
+ /**
64
+ * @param {string} phase
65
+ */
66
+ constructor(phase) {
67
+ super(`Install fault injection at phase: ${phase}`);
68
+ this.name = 'InstallTransactionFaultError';
69
+ this.phase = phase;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Read the configured fault phase from the environment, or null.
75
+ *
76
+ * @param {NodeJS.ProcessEnv} [environment]
77
+ * @returns {string|null}
78
+ */
79
+ export function resolveFaultPhaseFromEnvironment(environment = process.env) {
80
+ const rawPhase = environment[INSTALL_FAULT_ENV];
81
+ if (!rawPhase) {
82
+ return null;
83
+ }
84
+ if (!ALL_FAULT_PHASE_VALUES.includes(rawPhase)) {
85
+ throw new Error(
86
+ `Unknown ${INSTALL_FAULT_ENV} value "${rawPhase}". `
87
+ + `Expected one of: ${ALL_FAULT_PHASE_VALUES.join(', ')}`,
88
+ );
89
+ }
90
+ return rawPhase;
91
+ }
92
+
93
+ /**
94
+ * Throw when the current phase matches the active fault phase.
95
+ *
96
+ * @param {string} currentPhase
97
+ * @param {string|null|undefined} faultPhase
98
+ * @returns {void}
99
+ */
100
+ export function throwIfFaultPhase(currentPhase, faultPhase) {
101
+ if (faultPhase && faultPhase === currentPhase) {
102
+ throw new InstallTransactionFaultError(currentPhase);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Read global core.hooksPath, or null when unset.
108
+ *
109
+ * @param {{ execFileSync?: typeof execFileSync, env?: NodeJS.ProcessEnv }} [io]
110
+ * @returns {string|null}
111
+ */
112
+ export function readGlobalCoreHooksPath(io = {}) {
113
+ const execFile = io.execFileSync || execFileSync;
114
+ const environment = io.env || process.env;
115
+ try {
116
+ const value = execFile('git', ['config', '--global', '--get', 'core.hooksPath'], {
117
+ encoding: 'utf8',
118
+ stdio: ['ignore', 'pipe', 'pipe'],
119
+ env: environment,
120
+ });
121
+ const trimmed = String(value).trim();
122
+ return trimmed === '' ? null : trimmed;
123
+ } catch (readError) {
124
+ if (readError && readError.status === 1) {
125
+ return null;
126
+ }
127
+ return null;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Write or clear global core.hooksPath.
133
+ *
134
+ * @param {string|null} hooksPath
135
+ * @param {{ execFileSync?: typeof execFileSync, env?: NodeJS.ProcessEnv }} [io]
136
+ * @returns {void}
137
+ */
138
+ export function writeGlobalCoreHooksPath(hooksPath, io = {}) {
139
+ const execFile = io.execFileSync || execFileSync;
140
+ const environment = io.env || process.env;
141
+ if (hooksPath === null || hooksPath === undefined || hooksPath === '') {
142
+ try {
143
+ execFile('git', ['config', '--global', '--unset', 'core.hooksPath'], {
144
+ stdio: 'ignore',
145
+ env: environment,
146
+ });
147
+ } catch (unsetError) {
148
+ if (unsetError && (unsetError.status === 1 || unsetError.status === 5)) {
149
+ return;
150
+ }
151
+ throw unsetError;
152
+ }
153
+ return;
154
+ }
155
+ execFile('git', ['config', '--global', 'core.hooksPath', hooksPath], {
156
+ stdio: 'ignore',
157
+ env: environment,
158
+ });
159
+ }
160
+
161
+ /**
162
+ * @param {string} absolutePath
163
+ * @param {{ existsSync?: typeof existsSync, lstatSync?: typeof lstatSync, readlinkSync?: typeof readlinkSync }} io
164
+ * @returns {{ kind: string, linkTarget?: string }}
165
+ */
166
+ function classifyPath(absolutePath, io) {
167
+ const exists = io.existsSync || existsSync;
168
+ const lstat = io.lstatSync || lstatSync;
169
+ const readlink = io.readlinkSync || readlinkSync;
170
+ if (!exists(absolutePath)) {
171
+ return { kind: ENTRY_KIND_MISSING };
172
+ }
173
+ const stats = lstat(absolutePath);
174
+ if (stats.isSymbolicLink()) {
175
+ return { kind: ENTRY_KIND_SYMLINK, linkTarget: readlink(absolutePath) };
176
+ }
177
+ if (stats.isFile()) {
178
+ return { kind: ENTRY_KIND_FILE };
179
+ }
180
+ return { kind: ENTRY_KIND_MISSING };
181
+ }
182
+
183
+ /**
184
+ * Capture prior settings, manifest, hooksPath, and package-owned files.
185
+ *
186
+ * @param {{
187
+ * managedRoot: string,
188
+ * manifestFilePath: string,
189
+ * settingsPath: string,
190
+ * priorManifestFiles?: string[]|null,
191
+ * journalParentDirectory?: string,
192
+ * io?: object,
193
+ * }} input
194
+ * @returns {object}
195
+ */
196
+ export function capturePriorInstallSnapshot(input) {
197
+ const io = input.io || {};
198
+ const exists = io.existsSync || existsSync;
199
+ const mkdir = io.mkdirSync || mkdirSync;
200
+ const mkdtemp = io.mkdtempSync || mkdtempSync;
201
+ const copyFile = io.copyFileSync || copyFileSync;
202
+ const writeFile = io.writeFileSync || writeFileSync;
203
+
204
+ const journalParent = input.journalParentDirectory
205
+ || join(input.managedRoot, TRANSACTION_JOURNAL_DIRECTORY_NAME);
206
+ mkdir(journalParent, { recursive: true });
207
+ const journalRoot = mkdtemp(join(journalParent, 'run-'));
208
+ const filesDirectory = join(journalRoot, FILES_DIRECTORY_NAME);
209
+ mkdir(filesDirectory, { recursive: true });
210
+
211
+ const settingsExisted = exists(input.settingsPath);
212
+ if (settingsExisted) {
213
+ copyFile(input.settingsPath, join(journalRoot, SETTINGS_BLOB_NAME));
214
+ }
215
+
216
+ const manifestExisted = exists(input.manifestFilePath);
217
+ if (manifestExisted) {
218
+ copyFile(input.manifestFilePath, join(journalRoot, MANIFEST_BLOB_NAME));
219
+ }
220
+
221
+ const priorHooksPath = readGlobalCoreHooksPath(io);
222
+ /** @type {Array<{ absolutePath: string, kind: string, blobName?: string, linkTarget?: string }>} */
223
+ const allFileEntries = [];
224
+ const priorFiles = Array.isArray(input.priorManifestFiles) ? input.priorManifestFiles : [];
225
+ let entryIndex = 0;
226
+ for (const eachPath of priorFiles) {
227
+ if (typeof eachPath !== 'string' || eachPath.trim() === '') {
228
+ continue;
229
+ }
230
+ const classification = classifyPath(eachPath, io);
231
+ if (classification.kind === ENTRY_KIND_FILE) {
232
+ const blobName = String(entryIndex);
233
+ copyFile(eachPath, join(filesDirectory, blobName));
234
+ allFileEntries.push({
235
+ absolutePath: eachPath,
236
+ kind: ENTRY_KIND_FILE,
237
+ blobName,
238
+ });
239
+ entryIndex += 1;
240
+ } else if (classification.kind === ENTRY_KIND_SYMLINK) {
241
+ allFileEntries.push({
242
+ absolutePath: eachPath,
243
+ kind: ENTRY_KIND_SYMLINK,
244
+ linkTarget: classification.linkTarget,
245
+ });
246
+ } else {
247
+ allFileEntries.push({
248
+ absolutePath: eachPath,
249
+ kind: ENTRY_KIND_MISSING,
250
+ });
251
+ }
252
+ }
253
+
254
+ const meta = {
255
+ managedRoot: input.managedRoot,
256
+ settingsPath: input.settingsPath,
257
+ manifestFilePath: input.manifestFilePath,
258
+ settingsExisted,
259
+ manifestExisted,
260
+ priorHooksPath,
261
+ allFileEntries,
262
+ };
263
+ writeFile(join(journalRoot, META_FILE_NAME), `${JSON.stringify(meta, null, 2)}\n`);
264
+
265
+ return Object.freeze({
266
+ journalRoot,
267
+ managedRoot: input.managedRoot,
268
+ settingsPath: input.settingsPath,
269
+ manifestFilePath: input.manifestFilePath,
270
+ settingsExisted,
271
+ manifestExisted,
272
+ priorHooksPath,
273
+ allFileEntries: Object.freeze(allFileEntries.map((eachEntry) => Object.freeze({ ...eachEntry }))),
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Restore the captured prior installation and drop extra write-set paths.
279
+ *
280
+ * @param {ReturnType<typeof capturePriorInstallSnapshot>} snapshot
281
+ * @param {{
282
+ * allWrittenPaths?: string[],
283
+ * io?: object,
284
+ * }} [options]
285
+ * @returns {void}
286
+ */
287
+ export function restorePriorInstallSnapshot(snapshot, options = {}) {
288
+ const io = options.io || {};
289
+ const exists = io.existsSync || existsSync;
290
+ const copyFile = io.copyFileSync || copyFileSync;
291
+ const mkdir = io.mkdirSync || mkdirSync;
292
+ const unlink = io.unlinkSync || unlinkSync;
293
+ const symlink = io.symlinkSync || symlinkSync;
294
+ const lstat = io.lstatSync || lstatSync;
295
+
296
+ const filesDirectory = join(snapshot.journalRoot, FILES_DIRECTORY_NAME);
297
+ for (const eachEntry of snapshot.allFileEntries) {
298
+ try {
299
+ if (exists(eachEntry.absolutePath)) {
300
+ const stats = lstat(eachEntry.absolutePath);
301
+ if (stats.isFile() || stats.isSymbolicLink()) {
302
+ unlink(eachEntry.absolutePath);
303
+ }
304
+ }
305
+ } catch {
306
+ // keep restoring remaining entries
307
+ }
308
+ if (eachEntry.kind === ENTRY_KIND_FILE && eachEntry.blobName) {
309
+ mkdir(dirname(eachEntry.absolutePath), { recursive: true });
310
+ copyFile(join(filesDirectory, eachEntry.blobName), eachEntry.absolutePath);
311
+ } else if (eachEntry.kind === ENTRY_KIND_SYMLINK && eachEntry.linkTarget) {
312
+ mkdir(dirname(eachEntry.absolutePath), { recursive: true });
313
+ try {
314
+ symlink(eachEntry.linkTarget, eachEntry.absolutePath);
315
+ } catch {
316
+ // platform may reject the link shape; leave missing
317
+ }
318
+ }
319
+ }
320
+
321
+ const priorPathSet = new Set(snapshot.allFileEntries.map((eachEntry) => eachEntry.absolutePath));
322
+ const allWrittenPaths = Array.isArray(options.allWrittenPaths) ? options.allWrittenPaths : [];
323
+ for (const eachWrittenPath of allWrittenPaths) {
324
+ if (priorPathSet.has(eachWrittenPath)) {
325
+ continue;
326
+ }
327
+ if (eachWrittenPath === snapshot.settingsPath || eachWrittenPath === snapshot.manifestFilePath) {
328
+ continue;
329
+ }
330
+ try {
331
+ if (exists(eachWrittenPath)) {
332
+ const stats = lstat(eachWrittenPath);
333
+ if (stats.isFile() || stats.isSymbolicLink()) {
334
+ unlink(eachWrittenPath);
335
+ }
336
+ }
337
+ } catch {
338
+ // keep restoring remaining entries
339
+ }
340
+ }
341
+
342
+ if (snapshot.settingsExisted) {
343
+ mkdir(dirname(snapshot.settingsPath), { recursive: true });
344
+ copyFile(join(snapshot.journalRoot, SETTINGS_BLOB_NAME), snapshot.settingsPath);
345
+ } else if (exists(snapshot.settingsPath)) {
346
+ try {
347
+ unlink(snapshot.settingsPath);
348
+ } catch {
349
+ // leave in place when unlink fails
350
+ }
351
+ }
352
+
353
+ if (snapshot.manifestExisted) {
354
+ mkdir(dirname(snapshot.manifestFilePath), { recursive: true });
355
+ copyFile(join(snapshot.journalRoot, MANIFEST_BLOB_NAME), snapshot.manifestFilePath);
356
+ } else if (exists(snapshot.manifestFilePath)) {
357
+ try {
358
+ unlink(snapshot.manifestFilePath);
359
+ } catch {
360
+ // leave in place when unlink fails
361
+ }
362
+ }
363
+
364
+ writeGlobalCoreHooksPath(snapshot.priorHooksPath, io);
365
+ }
366
+
367
+ /**
368
+ * Remove the journal directory after a successful commit.
369
+ *
370
+ * @param {ReturnType<typeof capturePriorInstallSnapshot>} snapshot
371
+ * @param {{ rmSync?: typeof rmSync, readdirSync?: typeof readdirSync, rmdirSync?: typeof rmdirSync }} [io]
372
+ * @returns {void}
373
+ */
374
+ export function discardInstallTransactionJournal(snapshot, io = {}) {
375
+ const removeTree = io.rmSync || rmSync;
376
+ const readDirectory = io.readdirSync || readdirSync;
377
+ const removeDirectory = io.rmdirSync || rmdirSync;
378
+ try {
379
+ removeTree(snapshot.journalRoot, { recursive: true, force: true });
380
+ } catch {
381
+ // journal cleanup is best-effort after a successful install
382
+ }
383
+ const journalParent = dirname(snapshot.journalRoot);
384
+ try {
385
+ if (readDirectory(journalParent).length === 0) {
386
+ removeDirectory(journalParent);
387
+ }
388
+ } catch {
389
+ // parent cleanup is best-effort
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Run install mutations inside a snapshot/restore transaction.
395
+ *
396
+ * @param {{
397
+ * snapshot: ReturnType<typeof capturePriorInstallSnapshot>,
398
+ * faultPhase?: string|null,
399
+ * runMutations: (helpers: {
400
+ * throwIfFault: (phase: string) => void,
401
+ * recordWrittenPath: (absolutePath: string) => void,
402
+ * syncWrittenPaths: (allPaths: string[]) => void,
403
+ * }) => void,
404
+ * io?: object,
405
+ * }} input
406
+ * @returns {void}
407
+ */
408
+ export function runWithInstallTransaction(input) {
409
+ const io = input.io || {};
410
+ /** @type {string[]} */
411
+ const allWrittenPaths = [];
412
+ const faultPhase = input.faultPhase === undefined
413
+ ? resolveFaultPhaseFromEnvironment(process.env)
414
+ : input.faultPhase;
415
+
416
+ try {
417
+ input.runMutations({
418
+ throwIfFault: (phase) => throwIfFaultPhase(phase, faultPhase),
419
+ recordWrittenPath: (absolutePath) => {
420
+ if (typeof absolutePath === 'string' && absolutePath !== '') {
421
+ allWrittenPaths.push(absolutePath);
422
+ }
423
+ },
424
+ syncWrittenPaths: (allPaths) => {
425
+ allWrittenPaths.length = 0;
426
+ for (const eachPath of allPaths) {
427
+ if (typeof eachPath === 'string' && eachPath !== '') {
428
+ allWrittenPaths.push(eachPath);
429
+ }
430
+ }
431
+ },
432
+ });
433
+ discardInstallTransactionJournal(input.snapshot, io);
434
+ } catch (mutationError) {
435
+ try {
436
+ restorePriorInstallSnapshot(input.snapshot, { allWrittenPaths, io });
437
+ } catch (restoreError) {
438
+ const mutationMessage = mutationError instanceof Error
439
+ ? mutationError.message
440
+ : String(mutationError);
441
+ const restoreMessage = restoreError instanceof Error
442
+ ? restoreError.message
443
+ : String(restoreError);
444
+ throw new Error(
445
+ `Install failed (${mutationMessage}) and restore also failed (${restoreMessage})`,
446
+ );
447
+ }
448
+ try {
449
+ discardInstallTransactionJournal(input.snapshot, io);
450
+ } catch {
451
+ // journal cleanup is best-effort after restore
452
+ }
453
+ throw mutationError;
454
+ }
455
+ }