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.
- package/CLAUDE.md +1 -1
- 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 +58 -548
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +32 -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/references/CLAUDE.md +1 -0
- package/docs/references/advisor-tool.md +1 -1
- package/docs/references/prose-style-enforcement.md +25 -0
- 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/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 +1 -3
- package/package.json +4 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +6 -1
- 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 +11 -0
- 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 +2 -0
- 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/verify_installable_package_constants.py +116 -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 +3 -0
- 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_resolve_worker_spawn.py +2 -2
- 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 +2 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -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/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 +26 -5
- 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/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/finding_pipeline_constants.py +49 -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 +16 -7
- 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 +77 -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/system-prompts/software-engineer.xml +11 -2
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/output-styles/caveman-agent.md +0 -37
package/bin/install.mjs
CHANGED
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmSync, rmdirSync, renameSync, realpathSync, lstatSync } from 'node:fs';
|
|
4
4
|
import { join, dirname, resolve, relative, basename, isAbsolute, extname } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import { execSync, execFileSync } from 'node:child_process';
|
|
6
|
+
import { execSync, execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
9
|
import { installAllGitHooks } from './git_hooks_installer.mjs';
|
|
10
10
|
import { installMypyIniForClaudeHooks } from './install_mypy_ini.mjs';
|
|
11
11
|
import { expandHomeDirectoryTokensInSettings } from './expand_home_directory_tokens.mjs';
|
|
12
12
|
import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
|
|
13
|
+
import {
|
|
14
|
+
managedDenyEntriesFromPackageSettings,
|
|
15
|
+
mergeManagedPermissionsIntoSettings,
|
|
16
|
+
pruneManagedPermissionsFromSettings,
|
|
17
|
+
} from './merge_managed_permissions.mjs';
|
|
13
18
|
import {
|
|
14
19
|
SKIPPED_SOURCE_ENTRY_NAMES,
|
|
15
20
|
SKIPPED_SOURCE_FILE_EXTENSIONS,
|
|
@@ -17,17 +22,72 @@ import {
|
|
|
17
22
|
MANAGED_SKILLS_DIRECTORY_NAME,
|
|
18
23
|
MANAGED_HOOKS_DIRECTORY_NAME,
|
|
19
24
|
SETTINGS_FILE_NAME,
|
|
20
|
-
MYPY_INI_FILE_NAME,
|
|
21
25
|
} from './install-constants.mjs';
|
|
26
|
+
import {
|
|
27
|
+
resolveInstallRoot,
|
|
28
|
+
parseExplicitTargetFromArgv,
|
|
29
|
+
} from './resolve-install-root.mjs';
|
|
30
|
+
import {
|
|
31
|
+
parseInstallTargetSelectionFromArgv,
|
|
32
|
+
resolveInstallTargets,
|
|
33
|
+
buildTargetManifestRecord,
|
|
34
|
+
stripTargetSelectionFlagsFromArgv,
|
|
35
|
+
resolveProfilesRootDirectory,
|
|
36
|
+
MAIN_DEFAULT_TARGET_IDENTITY,
|
|
37
|
+
TARGET_IDENTITY_FLAG,
|
|
38
|
+
} from './select-install-targets.mjs';
|
|
39
|
+
import {
|
|
40
|
+
buildInstallPlan,
|
|
41
|
+
buildUninstallPlan,
|
|
42
|
+
InstallPlanPreflightError,
|
|
43
|
+
} from './install-plan.mjs';
|
|
44
|
+
import {
|
|
45
|
+
FAULT_PHASES,
|
|
46
|
+
InstallTransactionFaultError,
|
|
47
|
+
TRANSACTION_JOURNAL_DIRECTORY_NAME,
|
|
48
|
+
capturePriorInstallSnapshot,
|
|
49
|
+
resolveFaultPhaseFromEnvironment,
|
|
50
|
+
runWithInstallTransaction,
|
|
51
|
+
} from './install-transaction.mjs';
|
|
22
52
|
|
|
23
|
-
const CLAUDE_HOME = join(homedir(), '.claude');
|
|
24
53
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
-
const
|
|
26
|
-
|
|
54
|
+
const INSTALL_ROOT_RESOLUTION = resolveInstallRoot({
|
|
55
|
+
environment: process.env,
|
|
56
|
+
homeDirectory: homedir(),
|
|
57
|
+
explicitTarget: (() => {
|
|
58
|
+
try {
|
|
59
|
+
return parseExplicitTargetFromArgv(process.argv.slice(2));
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
})(),
|
|
64
|
+
});
|
|
65
|
+
const CLAUDE_HOME = INSTALL_ROOT_RESOLUTION.managedRoot;
|
|
66
|
+
const MANIFEST_FILE = INSTALL_ROOT_RESOLUTION.manifestFilePath;
|
|
67
|
+
const MYPY_INI_INSTALL_PATH = INSTALL_ROOT_RESOLUTION.mypyIniInstallPath;
|
|
27
68
|
const PACKAGE_NAME = 'claude-dev-env';
|
|
28
69
|
const PACKAGE_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
29
70
|
const packageRequire = createRequire(import.meta.url);
|
|
30
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Target identity for the current process hop. Multi-profile parent hops re-exec
|
|
74
|
+
* each child with --target-identity; a bare main install stays "main".
|
|
75
|
+
*/
|
|
76
|
+
const INSTALL_TARGET_IDENTITY = (() => {
|
|
77
|
+
try {
|
|
78
|
+
const selection = parseInstallTargetSelectionFromArgv(process.argv.slice(2));
|
|
79
|
+
if (selection.targetIdentity) {
|
|
80
|
+
return selection.targetIdentity;
|
|
81
|
+
}
|
|
82
|
+
if (selection.mode === 'profiles' && selection.allProfileIds.length === 1) {
|
|
83
|
+
return selection.allProfileIds[0];
|
|
84
|
+
}
|
|
85
|
+
return MAIN_DEFAULT_TARGET_IDENTITY;
|
|
86
|
+
} catch {
|
|
87
|
+
return MAIN_DEFAULT_TARGET_IDENTITY;
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
|
|
31
91
|
export const CONTENT_DIRECTORIES = ['rules', 'docs', 'commands', 'agents', 'system-prompts', 'scripts', '_shared', 'audit-rubrics'];
|
|
32
92
|
|
|
33
93
|
/**
|
|
@@ -48,8 +108,7 @@ const NEVER_PRUNED_SKILL_DIRECTORIES = new Set(['_shared']);
|
|
|
48
108
|
const PRUNED_SKILLS_BACKUP_DIRECTORY_NAME = '.claude-dev-env-pruned';
|
|
49
109
|
const RETIRED_SKILL_REASON_LABEL = 'retired';
|
|
50
110
|
const STALE_FILE_REASON_LABEL = 'stale';
|
|
51
|
-
const
|
|
52
|
-
const MANIFEST_SKILLS_KEY = 'skills';
|
|
111
|
+
const MANIFEST_MANAGED_PERMISSIONS_KEY = 'managedPermissions';
|
|
53
112
|
|
|
54
113
|
export const CORE_INCLUDE_DIRECTORIES = [
|
|
55
114
|
'rules', 'docs', 'commands', 'agents', 'audit-rubrics', '_shared', 'scripts',
|
|
@@ -62,6 +121,7 @@ export const CORE_SKILLS = [
|
|
|
62
121
|
'privacy-hygiene',
|
|
63
122
|
'issue-tracker',
|
|
64
123
|
'recall', 'remember', 'task-build',
|
|
124
|
+
'show',
|
|
65
125
|
];
|
|
66
126
|
|
|
67
127
|
export function collectPackageSourceConflicts(packageDirectory) {
|
|
@@ -110,24 +170,6 @@ export function collectPackageSourceConflicts(packageDirectory) {
|
|
|
110
170
|
return allConflicts;
|
|
111
171
|
}
|
|
112
172
|
|
|
113
|
-
function abortWhenPackageSourceHasConflicts(packageDirectory) {
|
|
114
|
-
const conflicts = collectPackageSourceConflicts(packageDirectory);
|
|
115
|
-
if (conflicts.length === 0) return;
|
|
116
|
-
console.error(
|
|
117
|
-
`\nERROR: ${PACKAGE_NAME} source has unmerged conflicts under ${packageDirectory}:\n`,
|
|
118
|
-
);
|
|
119
|
-
for (const conflict of conflicts) {
|
|
120
|
-
console.error(` ${conflict.statusCode} ${conflict.path}`);
|
|
121
|
-
}
|
|
122
|
-
console.error(
|
|
123
|
-
'\nResolve the conflicts in the package source before running the installer.',
|
|
124
|
-
);
|
|
125
|
-
console.error(
|
|
126
|
-
'Installing from a conflicted source can copy stale or broken files into ~/.claude/.\n',
|
|
127
|
-
);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
173
|
function resolveDependencyPackageRoot(dependencyPackageName) {
|
|
132
174
|
const dependencyPackageJsonPath = packageRequire.resolve(
|
|
133
175
|
`${dependencyPackageName}/package.json`
|
|
@@ -1357,6 +1399,37 @@ export function pruneRetiredHookEntriesFromSettings(settingsPath, retiredHookRel
|
|
|
1357
1399
|
return removedCount;
|
|
1358
1400
|
}
|
|
1359
1401
|
|
|
1402
|
+
/**
|
|
1403
|
+
* Load ~/.claude/settings.json for an in-place merge, or `{}` when absent/empty.
|
|
1404
|
+
*
|
|
1405
|
+
* Malformed JSON or a non-object root ends the process so install never writes
|
|
1406
|
+
* onto a shape the harness cannot read.
|
|
1407
|
+
*
|
|
1408
|
+
* @param {string} settingsPath Absolute path to settings.json.
|
|
1409
|
+
* @returns {object}
|
|
1410
|
+
*/
|
|
1411
|
+
function loadClaudeSettingsObjectOrExit(settingsPath) {
|
|
1412
|
+
if (!existsSync(settingsPath)) {
|
|
1413
|
+
return {};
|
|
1414
|
+
}
|
|
1415
|
+
const raw = readFileSync(settingsPath, 'utf8').trim();
|
|
1416
|
+
if (!raw) {
|
|
1417
|
+
return {};
|
|
1418
|
+
}
|
|
1419
|
+
let settings;
|
|
1420
|
+
try {
|
|
1421
|
+
settings = JSON.parse(raw);
|
|
1422
|
+
} catch {
|
|
1423
|
+
console.error(' ERROR: settings.json is malformed JSON. Fix it and rerun.');
|
|
1424
|
+
process.exit(1);
|
|
1425
|
+
}
|
|
1426
|
+
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
1427
|
+
console.error(' ERROR: settings.json holds a value other than a JSON object. Fix it and rerun.');
|
|
1428
|
+
process.exit(1);
|
|
1429
|
+
}
|
|
1430
|
+
return settings;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1360
1433
|
/**
|
|
1361
1434
|
* Merge one package source root's hook groups into ~/.claude/settings.json.
|
|
1362
1435
|
*
|
|
@@ -1373,74 +1446,68 @@ function mergeHooks(hooksSourceRoot, pythonCommand) {
|
|
|
1373
1446
|
if (!existsSync(hooksJsonPath)) return 0;
|
|
1374
1447
|
const hooksConfig = JSON.parse(readFileSync(hooksJsonPath, 'utf8'));
|
|
1375
1448
|
const settingsPath = join(CLAUDE_HOME, SETTINGS_FILE_NAME);
|
|
1376
|
-
|
|
1377
|
-
if (existsSync(settingsPath)) {
|
|
1378
|
-
const raw = readFileSync(settingsPath, 'utf8').trim();
|
|
1379
|
-
if (raw) {
|
|
1380
|
-
try { settings = JSON.parse(raw); }
|
|
1381
|
-
catch { console.error(' ERROR: settings.json is malformed JSON. Fix it and rerun.'); process.exit(1); }
|
|
1382
|
-
if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
|
|
1383
|
-
console.error(' ERROR: settings.json holds a value other than a JSON object. Fix it and rerun.');
|
|
1384
|
-
process.exit(1);
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1449
|
+
const settings = loadClaudeSettingsObjectOrExit(settingsPath);
|
|
1388
1450
|
const groupCount = mergeHooksIntoSettings(settings, hooksConfig, CLAUDE_HOME, pythonCommand);
|
|
1389
1451
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
1390
1452
|
return groupCount;
|
|
1391
1453
|
}
|
|
1392
1454
|
|
|
1393
|
-
function writeManifest(installedFiles, skillNames) {
|
|
1394
|
-
const manifest = {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1455
|
+
function writeManifest(installedFiles, skillNames, managedPermissions = null) {
|
|
1456
|
+
const manifest = buildTargetManifestRecord({
|
|
1457
|
+
packageName: PACKAGE_NAME,
|
|
1458
|
+
packageVersion: PACKAGE_VERSION,
|
|
1459
|
+
targetIdentity: INSTALL_TARGET_IDENTITY,
|
|
1460
|
+
managedRoot: CLAUDE_HOME,
|
|
1461
|
+
files: installedFiles,
|
|
1462
|
+
skills: skillNames,
|
|
1463
|
+
});
|
|
1464
|
+
if (managedPermissions && Array.isArray(managedPermissions.deny) && managedPermissions.deny.length > 0) {
|
|
1465
|
+
manifest[MANIFEST_MANAGED_PERMISSIONS_KEY] = {
|
|
1466
|
+
deny: [...managedPermissions.deny],
|
|
1467
|
+
};
|
|
1402
1468
|
}
|
|
1403
1469
|
writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2) + '\n');
|
|
1404
1470
|
}
|
|
1405
1471
|
|
|
1406
1472
|
/**
|
|
1407
|
-
*
|
|
1408
|
-
* parse of the manifest.
|
|
1473
|
+
* Load package-owned deny entries from the package settings.json source.
|
|
1409
1474
|
*
|
|
1410
|
-
*
|
|
1411
|
-
* read and parsed once and both lists are handed back together.
|
|
1412
|
-
*
|
|
1413
|
-
* Either list is null when the manifest is missing, unreadable, or holds no array
|
|
1414
|
-
* at that key, so a caller treats every such case as "no prior record": the skills
|
|
1415
|
-
* list leans on the ever-shipped set to find retired skills, and the files list
|
|
1416
|
-
* holds the stale-file prune for that run rather than guessing at what a prior
|
|
1417
|
-
* install wrote. A `--update` run purges the manifest before reinstalling, so this
|
|
1418
|
-
* read happens at the top of `install()` while the record is still on disk.
|
|
1419
|
-
*
|
|
1420
|
-
* @returns {{files: string[]|null, skills: string[]|null}} The recorded lists, each null when absent.
|
|
1475
|
+
* @returns {string[]}
|
|
1421
1476
|
*/
|
|
1422
|
-
function
|
|
1423
|
-
const
|
|
1424
|
-
if (!existsSync(
|
|
1477
|
+
function loadPackageManagedDenyEntries() {
|
|
1478
|
+
const packageSettingsPath = join(PACKAGE_ROOT, SETTINGS_FILE_NAME);
|
|
1479
|
+
if (!existsSync(packageSettingsPath)) {
|
|
1480
|
+
return [];
|
|
1481
|
+
}
|
|
1425
1482
|
try {
|
|
1426
|
-
const
|
|
1427
|
-
return
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
return
|
|
1483
|
+
const packageSettings = JSON.parse(readFileSync(packageSettingsPath, 'utf8'));
|
|
1484
|
+
return managedDenyEntriesFromPackageSettings(packageSettings);
|
|
1485
|
+
} catch (parseError) {
|
|
1486
|
+
console.warn(
|
|
1487
|
+
` Warning: could not read package ${SETTINGS_FILE_NAME} for managed permissions (${parseError.message})`,
|
|
1488
|
+
);
|
|
1489
|
+
return [];
|
|
1433
1490
|
}
|
|
1434
1491
|
}
|
|
1435
1492
|
|
|
1436
1493
|
/**
|
|
1437
|
-
*
|
|
1494
|
+
* Merge package-owned permission defaults into ~/.claude/settings.json.
|
|
1438
1495
|
*
|
|
1439
|
-
* @
|
|
1440
|
-
* @returns {string[]|null} The array, or null when the key holds anything else.
|
|
1496
|
+
* @returns {{addedCount: number, alreadyPresentCount: number, managedDenyEntries: string[]}}
|
|
1441
1497
|
*/
|
|
1442
|
-
function
|
|
1443
|
-
|
|
1498
|
+
function mergeManagedPermissions() {
|
|
1499
|
+
const managedDenyEntries = loadPackageManagedDenyEntries();
|
|
1500
|
+
if (managedDenyEntries.length === 0) {
|
|
1501
|
+
return { addedCount: 0, alreadyPresentCount: 0, managedDenyEntries: [] };
|
|
1502
|
+
}
|
|
1503
|
+
const settingsPath = join(CLAUDE_HOME, SETTINGS_FILE_NAME);
|
|
1504
|
+
const settingsExisted = existsSync(settingsPath);
|
|
1505
|
+
const settings = loadClaudeSettingsObjectOrExit(settingsPath);
|
|
1506
|
+
const mergeOutcome = mergeManagedPermissionsIntoSettings(settings, managedDenyEntries);
|
|
1507
|
+
if (mergeOutcome.addedCount > 0 || !settingsExisted) {
|
|
1508
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
1509
|
+
}
|
|
1510
|
+
return mergeOutcome;
|
|
1444
1511
|
}
|
|
1445
1512
|
|
|
1446
1513
|
/**
|
|
@@ -1660,25 +1727,104 @@ function runFullInstallPrunes(
|
|
|
1660
1727
|
* @returns {void}
|
|
1661
1728
|
*/
|
|
1662
1729
|
function install(selectedGroups, options = {}) {
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1730
|
+
let plan;
|
|
1731
|
+
try {
|
|
1732
|
+
plan = buildInstallPlan({
|
|
1733
|
+
packageRoot: PACKAGE_ROOT,
|
|
1734
|
+
managedRoot: CLAUDE_HOME,
|
|
1735
|
+
manifestFilePath: MANIFEST_FILE,
|
|
1736
|
+
targetIdentity: INSTALL_TARGET_IDENTITY,
|
|
1737
|
+
selectedGroups,
|
|
1738
|
+
isUpdateRefresh: Boolean(options.isUpdateRefresh),
|
|
1739
|
+
installGroups: INSTALL_GROUPS,
|
|
1740
|
+
collectSourceConflicts: collectPackageSourceConflicts,
|
|
1741
|
+
detectPythonCommand: detectPython,
|
|
1742
|
+
packageName: PACKAGE_NAME,
|
|
1743
|
+
});
|
|
1744
|
+
} catch (preflightError) {
|
|
1745
|
+
if (preflightError instanceof InstallPlanPreflightError) {
|
|
1746
|
+
console.error(preflightError.message);
|
|
1747
|
+
process.exit(1);
|
|
1748
|
+
}
|
|
1749
|
+
throw preflightError;
|
|
1750
|
+
}
|
|
1751
|
+
executeInstallPlan(plan);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Apply one preflighted installation plan inside a snapshot/restore transaction.
|
|
1756
|
+
*
|
|
1757
|
+
* Captures the prior managed installation before any purge or write, then
|
|
1758
|
+
* restores settings, manifest, managed files, and core.hooksPath on failure.
|
|
1759
|
+
*
|
|
1760
|
+
* @param {ReturnType<typeof buildInstallPlan>} plan
|
|
1761
|
+
* @returns {void}
|
|
1762
|
+
*/
|
|
1763
|
+
function executeInstallPlan(plan) {
|
|
1764
|
+
const faultPhase = resolveFaultPhaseFromEnvironment(process.env);
|
|
1765
|
+
const snapshot = capturePriorInstallSnapshot({
|
|
1766
|
+
managedRoot: CLAUDE_HOME,
|
|
1767
|
+
manifestFilePath: MANIFEST_FILE,
|
|
1768
|
+
settingsPath: plan.settingsPath,
|
|
1769
|
+
priorManifestFiles: plan.priorManifest.files,
|
|
1770
|
+
journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
try {
|
|
1774
|
+
runWithInstallTransaction({
|
|
1775
|
+
snapshot,
|
|
1776
|
+
faultPhase,
|
|
1777
|
+
runMutations: ({ throwIfFault, syncWrittenPaths }) => {
|
|
1778
|
+
executeInstallPlanMutations(plan, { throwIfFault, syncWrittenPaths });
|
|
1779
|
+
},
|
|
1780
|
+
});
|
|
1781
|
+
} catch (mutationError) {
|
|
1782
|
+
if (mutationError instanceof InstallTransactionFaultError) {
|
|
1783
|
+
console.error(
|
|
1784
|
+
`ERROR: install aborted at ${mutationError.phase}; prior installation restored.`,
|
|
1785
|
+
);
|
|
1786
|
+
process.exit(1);
|
|
1787
|
+
}
|
|
1788
|
+
const message = mutationError instanceof Error
|
|
1789
|
+
? mutationError.message
|
|
1790
|
+
: String(mutationError);
|
|
1791
|
+
console.error(`ERROR: install failed (${message}); prior installation restored.`);
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/**
|
|
1797
|
+
* Mutation body for one preflighted plan. Invoked inside the transaction wrapper.
|
|
1798
|
+
*
|
|
1799
|
+
* @param {ReturnType<typeof buildInstallPlan>} plan
|
|
1800
|
+
* @param {{
|
|
1801
|
+
* throwIfFault: (phase: string) => void,
|
|
1802
|
+
* syncWrittenPaths: (allPaths: string[]) => void,
|
|
1803
|
+
* }} transactionHelpers
|
|
1804
|
+
* @returns {void}
|
|
1805
|
+
*/
|
|
1806
|
+
function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
1807
|
+
const { throwIfFault, syncWrittenPaths } = transactionHelpers;
|
|
1808
|
+
const selectedGroups = plan.selectedGroups;
|
|
1809
|
+
const priorManifestFiles = plan.priorManifest.files;
|
|
1810
|
+
const priorManifestSkills = plan.priorManifest.skills;
|
|
1811
|
+
const isUpdateRefresh = plan.isUpdateRefresh;
|
|
1812
|
+
const pythonCommand = plan.pythonCommand;
|
|
1813
|
+
|
|
1814
|
+
if (plan.shouldPurgeBeforeReinstall) {
|
|
1666
1815
|
console.log(
|
|
1667
1816
|
`${PACKAGE_NAME}: --update — removing prior managed files under ${CLAUDE_HOME}, then reinstalling from the package.\n`,
|
|
1668
1817
|
);
|
|
1669
|
-
purgeManagedInstallation({
|
|
1818
|
+
purgeManagedInstallation({
|
|
1819
|
+
requireManifest: false,
|
|
1820
|
+
throwIfFault,
|
|
1821
|
+
});
|
|
1670
1822
|
} else if (isUpdateRefresh) {
|
|
1671
1823
|
const installScope = selectedGroups ? `groups: ${selectedGroups.join(', ')}` : 'full';
|
|
1672
1824
|
console.log(`${PACKAGE_NAME}: --update — re-running ${installScope} install into ${CLAUDE_HOME}\n`);
|
|
1673
1825
|
}
|
|
1674
1826
|
const groupLabel = selectedGroups ? `groups: ${selectedGroups.join(', ')}` : 'all';
|
|
1675
1827
|
console.log(`\nInstalling ${PACKAGE_NAME} (${groupLabel})...\n`);
|
|
1676
|
-
abortWhenPackageSourceHasConflicts(PACKAGE_ROOT);
|
|
1677
|
-
const pythonCommand = detectPython();
|
|
1678
|
-
if (!pythonCommand) {
|
|
1679
|
-
console.error('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.');
|
|
1680
|
-
process.exit(1);
|
|
1681
|
-
}
|
|
1682
1828
|
console.log(` Python: ${pythonCommand}`);
|
|
1683
1829
|
mkdirSync(CLAUDE_HOME, { recursive: true });
|
|
1684
1830
|
|
|
@@ -1779,6 +1925,9 @@ function install(selectedGroups, options = {}) {
|
|
|
1779
1925
|
}
|
|
1780
1926
|
summary.skills = { created: skillsCreated, updated: skillsUpdated, pruned: 0, paths: skillPaths };
|
|
1781
1927
|
allInstalledFiles.push(...skillPaths);
|
|
1928
|
+
syncWrittenPaths(allInstalledFiles);
|
|
1929
|
+
throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
|
|
1930
|
+
throwIfFault(FAULT_PHASES.BEFORE_DURABLE_PROMOTION);
|
|
1782
1931
|
const shouldInstallAnyHooks = shouldInstallAllHooks || (allowedHookFiles && allowedHookFiles.size > 0);
|
|
1783
1932
|
if (shouldInstallAnyHooks) {
|
|
1784
1933
|
let totalHooksCreated = 0;
|
|
@@ -1811,6 +1960,8 @@ function install(selectedGroups, options = {}) {
|
|
|
1811
1960
|
console.log(` Hook files: ${totalHooksCreated} new, ${totalHooksUpdated} updated`);
|
|
1812
1961
|
summary.hookGroups = totalHookGroups;
|
|
1813
1962
|
console.log(` Hook groups: ${totalHookGroups} merged into settings.json`);
|
|
1963
|
+
syncWrittenPaths(allInstalledFiles);
|
|
1964
|
+
throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
|
|
1814
1965
|
|
|
1815
1966
|
console.warn(
|
|
1816
1967
|
' Warning: git hook installation sets core.hooksPath globally — '
|
|
@@ -1831,6 +1982,9 @@ function install(selectedGroups, options = {}) {
|
|
|
1831
1982
|
console.warn(` Git hooks: ${gitHookInstallationResult.hooksPathConfigurationResult.reason}`);
|
|
1832
1983
|
}
|
|
1833
1984
|
console.log(` Git hook shims: ${gitHookInstallationResult.createdShimPaths.length} files (pre-commit, pre-push, post-commit)`);
|
|
1985
|
+
syncWrittenPaths(allInstalledFiles);
|
|
1986
|
+
throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
|
|
1987
|
+
throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
|
|
1834
1988
|
|
|
1835
1989
|
const mypyIniInstallResult = installMypyIniForClaudeHooks({
|
|
1836
1990
|
homeDirectory: homedir(),
|
|
@@ -1847,7 +2001,23 @@ function install(selectedGroups, options = {}) {
|
|
|
1847
2001
|
console.warn(` To enable mypy for Claude hooks, add this line under [mypy]:`);
|
|
1848
2002
|
console.warn(` ${mypyIniInstallResult.expectedLine}`);
|
|
1849
2003
|
}
|
|
2004
|
+
} else {
|
|
2005
|
+
syncWrittenPaths(allInstalledFiles);
|
|
2006
|
+
throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
|
|
2007
|
+
throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
|
|
2008
|
+
throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
const permissionMerge = mergeManagedPermissions();
|
|
2012
|
+
summary.managedPermissions = permissionMerge;
|
|
2013
|
+
if (permissionMerge.managedDenyEntries.length > 0) {
|
|
2014
|
+
console.log(
|
|
2015
|
+
` Permissions: ${permissionMerge.addedCount} managed deny(s) added, `
|
|
2016
|
+
+ `${permissionMerge.alreadyPresentCount} already present `
|
|
2017
|
+
+ `(${permissionMerge.managedDenyEntries.length} package-owned)`,
|
|
2018
|
+
);
|
|
1850
2019
|
}
|
|
2020
|
+
|
|
1851
2021
|
const claudeHubSource = join(PACKAGE_ROOT, 'CLAUDE.md');
|
|
1852
2022
|
if (existsSync(claudeHubSource)) {
|
|
1853
2023
|
const claudeHubDest = join(CLAUDE_HOME, 'CLAUDE.md');
|
|
@@ -1893,7 +2063,16 @@ function install(selectedGroups, options = {}) {
|
|
|
1893
2063
|
const manifestFiles = didPruneFinish
|
|
1894
2064
|
? manifestFilesWithFailedPrunes(allInstalledFiles, failedPrunePaths)
|
|
1895
2065
|
: unionOnComparisonKey(priorManifestFiles || [], allInstalledFiles);
|
|
1896
|
-
|
|
2066
|
+
const managedPermissionDenyEntries = summary.managedPermissions.managedDenyEntries;
|
|
2067
|
+
writeManifest(
|
|
2068
|
+
manifestFiles,
|
|
2069
|
+
manifestSkillNames,
|
|
2070
|
+
managedPermissionDenyEntries.length > 0
|
|
2071
|
+
? { deny: managedPermissionDenyEntries }
|
|
2072
|
+
: null,
|
|
2073
|
+
);
|
|
2074
|
+
syncWrittenPaths([...allInstalledFiles, MANIFEST_FILE, plan.settingsPath]);
|
|
2075
|
+
throwIfFault(FAULT_PHASES.AFTER_MANIFEST_WRITE);
|
|
1897
2076
|
console.log(`\nInstalled ${PACKAGE_NAME}:`);
|
|
1898
2077
|
for (const directory of CONTENT_DIRECTORIES) {
|
|
1899
2078
|
if (summary[directory]) {
|
|
@@ -1985,66 +2164,92 @@ function removeRecordedFile(filePath) {
|
|
|
1985
2164
|
}
|
|
1986
2165
|
|
|
1987
2166
|
/**
|
|
1988
|
-
*
|
|
1989
|
-
* emptied.
|
|
1990
|
-
*
|
|
1991
|
-
* A record is removed when it names a path the installer writes: anything under
|
|
1992
|
-
* ~/.claude, plus the `~/.mypy.ini` the install writes in the home directory.
|
|
1993
|
-
* Every other record is skipped with a warning and counted, so one malformed
|
|
1994
|
-
* entry costs that entry alone: the purge removes every legitimate record, clears
|
|
1995
|
-
* the manifest, and leaves the user with a whole uninstall rather than a
|
|
1996
|
-
* half-removed install.
|
|
2167
|
+
* Build the uninstall plan for this managed root.
|
|
1997
2168
|
*
|
|
1998
|
-
*
|
|
1999
|
-
*
|
|
2000
|
-
|
|
2001
|
-
|
|
2169
|
+
* @param {boolean} requireManifest
|
|
2170
|
+
* @returns {ReturnType<typeof buildUninstallPlan>}
|
|
2171
|
+
*/
|
|
2172
|
+
function resolveUninstallPlan(requireManifest) {
|
|
2173
|
+
return buildUninstallPlan({
|
|
2174
|
+
managedRoot: CLAUDE_HOME,
|
|
2175
|
+
manifestFilePath: MANIFEST_FILE,
|
|
2176
|
+
requireManifest,
|
|
2177
|
+
isRemovableRecord: isRemovableManifestRecord,
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* Apply one preflighted uninstall plan: remove removable files, prune managed
|
|
2183
|
+
* hooks from settings, unset core.hooksPath when it points here, then drop the
|
|
2184
|
+
* ownership manifest. Fault phases mirror the install transaction names.
|
|
2002
2185
|
*
|
|
2003
|
-
* @param {
|
|
2004
|
-
* @
|
|
2186
|
+
* @param {ReturnType<typeof buildUninstallPlan>} plan
|
|
2187
|
+
* @param {{ throwIfFault?: (phase: string) => void }} [helpers]
|
|
2188
|
+
* @returns {number} Count of files this call removed.
|
|
2005
2189
|
*/
|
|
2006
|
-
function
|
|
2007
|
-
|
|
2008
|
-
if (requireManifest) {
|
|
2009
|
-
console.error('No installation manifest found. Nothing to uninstall.');
|
|
2010
|
-
process.exit(1);
|
|
2011
|
-
}
|
|
2012
|
-
return 0;
|
|
2013
|
-
}
|
|
2014
|
-
const manifest = JSON.parse(readFileSync(MANIFEST_FILE, 'utf8'));
|
|
2190
|
+
function executeUninstallPlan(plan, helpers = {}) {
|
|
2191
|
+
const throwIfFault = helpers.throwIfFault || (() => {});
|
|
2015
2192
|
let removed = 0;
|
|
2016
|
-
let skippedUnmanagedCount = 0;
|
|
2017
2193
|
const managedRootByEmptiedDirectory = new Map();
|
|
2018
|
-
for (const filePath of
|
|
2019
|
-
if (
|
|
2020
|
-
|
|
2021
|
-
skippedUnmanagedCount++;
|
|
2022
|
-
continue;
|
|
2194
|
+
for (const filePath of plan.removableFiles) {
|
|
2195
|
+
if (removeRecordedFile(filePath)) {
|
|
2196
|
+
removed += 1;
|
|
2023
2197
|
}
|
|
2024
|
-
if (removeRecordedFile(filePath)) removed++;
|
|
2025
2198
|
const managedRoot = owningManagedRoot(filePath);
|
|
2026
|
-
if (managedRoot)
|
|
2199
|
+
if (managedRoot) {
|
|
2200
|
+
managedRootByEmptiedDirectory.set(dirname(resolve(filePath)), managedRoot);
|
|
2201
|
+
}
|
|
2027
2202
|
}
|
|
2028
2203
|
for (const [emptiedDirectory, managedRoot] of managedRootByEmptiedDirectory) {
|
|
2029
2204
|
removeEmptiedParentDirectories(emptiedDirectory, managedRoot);
|
|
2030
2205
|
}
|
|
2031
|
-
|
|
2032
|
-
console.warn(
|
|
2206
|
+
for (const eachSkippedPath of plan.skippedFiles) {
|
|
2207
|
+
console.warn(
|
|
2208
|
+
` Warning: skipping ${eachSkippedPath} — the manifest record names no path this installer writes`,
|
|
2209
|
+
);
|
|
2033
2210
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2211
|
+
if (plan.skippedFiles.length > 0) {
|
|
2212
|
+
console.warn(
|
|
2213
|
+
` ${plan.skippedFiles.length} manifest record(s) skipped — each names a path outside ${CLAUDE_HOME} and outside ${MYPY_INI_INSTALL_PATH}`,
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
|
|
2217
|
+
|
|
2218
|
+
if (existsSync(plan.settingsPath)) {
|
|
2219
|
+
const settings = JSON.parse(readFileSync(plan.settingsPath, 'utf8'));
|
|
2220
|
+
let settingsChanged = false;
|
|
2037
2221
|
if (settings.hooks) {
|
|
2038
2222
|
const managedHookRelativePaths = managedHookScriptRelativePathsFromSourceRoots(
|
|
2039
|
-
managedPackageSourceRoots()
|
|
2223
|
+
managedPackageSourceRoots(),
|
|
2040
2224
|
);
|
|
2041
2225
|
pruneManagedHooksFromSettings(settings, managedHookRelativePaths);
|
|
2042
|
-
|
|
2226
|
+
settingsChanged = true;
|
|
2043
2227
|
console.log(' Hook entries removed from settings.json');
|
|
2044
2228
|
}
|
|
2229
|
+
const managedDenyFromPlan = plan.managedPermissionDenyEntries.length > 0
|
|
2230
|
+
? plan.managedPermissionDenyEntries
|
|
2231
|
+
: loadPackageManagedDenyEntries();
|
|
2232
|
+
if (managedDenyFromPlan.length > 0) {
|
|
2233
|
+
const pruneOutcome = pruneManagedPermissionsFromSettings(settings, managedDenyFromPlan);
|
|
2234
|
+
if (pruneOutcome.removedCount > 0) {
|
|
2235
|
+
settingsChanged = true;
|
|
2236
|
+
console.log(
|
|
2237
|
+
` Permission entries removed from settings.json: ${pruneOutcome.removedCount} managed deny(s)`,
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
if (settingsChanged) {
|
|
2242
|
+
writeFileSync(plan.settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
2243
|
+
}
|
|
2045
2244
|
}
|
|
2245
|
+
throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
|
|
2246
|
+
|
|
2046
2247
|
unsetGlobalGitHooksPathIfOurs();
|
|
2047
|
-
|
|
2248
|
+
throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
|
|
2249
|
+
|
|
2250
|
+
unlinkSync(plan.manifestFilePath);
|
|
2251
|
+
throwIfFault(FAULT_PHASES.AFTER_MANIFEST_WRITE);
|
|
2252
|
+
|
|
2048
2253
|
for (const directory of MANAGED_TOP_LEVEL_DIRECTORY_NAMES) {
|
|
2049
2254
|
const dirPath = join(CLAUDE_HOME, directory);
|
|
2050
2255
|
try {
|
|
@@ -2054,11 +2259,85 @@ function purgeManagedInstallation({ requireManifest }) {
|
|
|
2054
2259
|
} catch { /* leave non-empty dirs */ }
|
|
2055
2260
|
}
|
|
2056
2261
|
console.log(`\nRemoved ${removed} files.\n`);
|
|
2262
|
+
return removed;
|
|
2057
2263
|
}
|
|
2058
2264
|
|
|
2265
|
+
/**
|
|
2266
|
+
* Remove every file the ownership record lists for this managed root.
|
|
2267
|
+
*
|
|
2268
|
+
* Preflights the uninstall plan (settings JSON must be an object when present)
|
|
2269
|
+
* before any removal. When called from `--update` inside an install transaction,
|
|
2270
|
+
* pass that transaction's `throwIfFault` so a fault restores the outer snapshot
|
|
2271
|
+
* without nesting a second journal.
|
|
2272
|
+
*
|
|
2273
|
+
* @param {{
|
|
2274
|
+
* requireManifest: boolean,
|
|
2275
|
+
* throwIfFault?: (phase: string) => void,
|
|
2276
|
+
* }} options
|
|
2277
|
+
* @returns {number|void} 0 when no manifest exists and none is required.
|
|
2278
|
+
*/
|
|
2279
|
+
function purgeManagedInstallation({ requireManifest, throwIfFault }) {
|
|
2280
|
+
const plan = resolveUninstallPlan(requireManifest);
|
|
2281
|
+
if (plan.isNoOp) {
|
|
2282
|
+
return 0;
|
|
2283
|
+
}
|
|
2284
|
+
return executeUninstallPlan(plan, { throwIfFault });
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/**
|
|
2288
|
+
* Uninstall the selected managed root inside a snapshot/restore transaction.
|
|
2289
|
+
*
|
|
2290
|
+
* Captures files, settings, manifest, and core.hooksPath before removal so any
|
|
2291
|
+
* later fault restores a retryable installation from the ownership record.
|
|
2292
|
+
*
|
|
2293
|
+
* @returns {void}
|
|
2294
|
+
*/
|
|
2059
2295
|
function uninstall() {
|
|
2060
2296
|
console.log(`\nUninstalling ${PACKAGE_NAME}...\n`);
|
|
2061
|
-
|
|
2297
|
+
let plan;
|
|
2298
|
+
try {
|
|
2299
|
+
plan = resolveUninstallPlan(true);
|
|
2300
|
+
} catch (preflightError) {
|
|
2301
|
+
if (preflightError instanceof InstallPlanPreflightError) {
|
|
2302
|
+
console.error(preflightError.message);
|
|
2303
|
+
process.exit(1);
|
|
2304
|
+
}
|
|
2305
|
+
throw preflightError;
|
|
2306
|
+
}
|
|
2307
|
+
if (plan.isNoOp) {
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
const faultPhase = resolveFaultPhaseFromEnvironment(process.env);
|
|
2312
|
+
const snapshot = capturePriorInstallSnapshot({
|
|
2313
|
+
managedRoot: CLAUDE_HOME,
|
|
2314
|
+
manifestFilePath: MANIFEST_FILE,
|
|
2315
|
+
settingsPath: plan.settingsPath,
|
|
2316
|
+
priorManifestFiles: plan.removableFiles,
|
|
2317
|
+
journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
try {
|
|
2321
|
+
runWithInstallTransaction({
|
|
2322
|
+
snapshot,
|
|
2323
|
+
faultPhase,
|
|
2324
|
+
runMutations: ({ throwIfFault }) => {
|
|
2325
|
+
executeUninstallPlan(plan, { throwIfFault });
|
|
2326
|
+
},
|
|
2327
|
+
});
|
|
2328
|
+
} catch (mutationError) {
|
|
2329
|
+
if (mutationError instanceof InstallTransactionFaultError) {
|
|
2330
|
+
console.error(
|
|
2331
|
+
`ERROR: uninstall aborted at ${mutationError.phase}; prior installation restored.`,
|
|
2332
|
+
);
|
|
2333
|
+
process.exit(1);
|
|
2334
|
+
}
|
|
2335
|
+
const message = mutationError instanceof Error
|
|
2336
|
+
? mutationError.message
|
|
2337
|
+
: String(mutationError);
|
|
2338
|
+
console.error(`ERROR: uninstall failed (${message}); prior installation restored.`);
|
|
2339
|
+
process.exit(1);
|
|
2340
|
+
}
|
|
2062
2341
|
}
|
|
2063
2342
|
|
|
2064
2343
|
/**
|
|
@@ -2078,10 +2357,13 @@ function printHelp() {
|
|
|
2078
2357
|
${PACKAGE_NAME} - Claude Code development standards installer
|
|
2079
2358
|
|
|
2080
2359
|
Usage:
|
|
2081
|
-
npx ${PACKAGE_NAME} Install everything
|
|
2360
|
+
npx ${PACKAGE_NAME} Install everything into the main default root
|
|
2082
2361
|
npx ${PACKAGE_NAME} --update Full install: remove prior manifest-tracked files first, then reinstall
|
|
2083
2362
|
npx ${PACKAGE_NAME} --only X Install specific groups
|
|
2084
|
-
npx ${PACKAGE_NAME} --
|
|
2363
|
+
npx ${PACKAGE_NAME} --target DIR Install into DIR instead of ~/.claude (overrides CLAUDE_CONFIG_DIR)
|
|
2364
|
+
npx ${PACKAGE_NAME} --profile ID Install into one named profile root (under the profiles root)
|
|
2365
|
+
npx ${PACKAGE_NAME} --profiles A,B Install into each selected profile (one ownership manifest per target)
|
|
2366
|
+
npx ${PACKAGE_NAME} --uninstall Remove installed files from the selected root
|
|
2085
2367
|
npx ${PACKAGE_NAME} --help Show this help
|
|
2086
2368
|
|
|
2087
2369
|
Groups:
|
|
@@ -2090,8 +2372,16 @@ ${groupLines}
|
|
|
2090
2372
|
Examples:
|
|
2091
2373
|
npx ${PACKAGE_NAME} --only core
|
|
2092
2374
|
npx ${PACKAGE_NAME} --only core,journal
|
|
2375
|
+
npx ${PACKAGE_NAME} --profile editor --only core
|
|
2376
|
+
npx ${PACKAGE_NAME} --profiles editor,mel --only core
|
|
2377
|
+
|
|
2378
|
+
Install location: ~/.claude/ by default; CLAUDE_CONFIG_DIR or --target selects another managed root.
|
|
2379
|
+
Named profiles resolve under LLM_SETTINGS_PROFILES_ROOT or ~/.claude-profiles/<directoryName>.
|
|
2093
2380
|
|
|
2094
|
-
|
|
2381
|
+
Root precedence: --target > CLAUDE_CONFIG_DIR > ~/.claude
|
|
2382
|
+
Profile selection (--profile/--profiles) is mutually exclusive with --target.
|
|
2383
|
+
Target selection finishes before any mutation. Each target writes its own manifest
|
|
2384
|
+
with package, version, targetIdentity, managedRoot, files, and skills.
|
|
2095
2385
|
|
|
2096
2386
|
If ~/.claude/CLAUDE.md already exists and differs from the package copy, the installer
|
|
2097
2387
|
writes the previous contents to ~/.claude/backups/CLAUDE.md.<timestamp>.bak first.
|
|
@@ -2130,30 +2420,186 @@ function realPathOrSelf(filesystemPath) {
|
|
|
2130
2420
|
}
|
|
2131
2421
|
}
|
|
2132
2422
|
|
|
2423
|
+
/**
|
|
2424
|
+
* Load profile id → directoryName from the A1 launcher contract when present.
|
|
2425
|
+
* Falls back to identity mapping when the file is missing, unreadable, or empty.
|
|
2426
|
+
*
|
|
2427
|
+
* @returns {Record<string, string>}
|
|
2428
|
+
*/
|
|
2429
|
+
function loadDirectoryNameByProfileId() {
|
|
2430
|
+
const fallbackDirectoryNameByProfileId = {
|
|
2431
|
+
main: 'main',
|
|
2432
|
+
editor: 'editor',
|
|
2433
|
+
mel: 'mel',
|
|
2434
|
+
ev: 'ev',
|
|
2435
|
+
master: 'master',
|
|
2436
|
+
kimi: 'kimi',
|
|
2437
|
+
};
|
|
2438
|
+
const manifestPath = join(
|
|
2439
|
+
PACKAGE_ROOT,
|
|
2440
|
+
'scripts',
|
|
2441
|
+
'profile-isolation-launchers',
|
|
2442
|
+
'config',
|
|
2443
|
+
'profiles.manifest.json',
|
|
2444
|
+
);
|
|
2445
|
+
if (!existsSync(manifestPath)) {
|
|
2446
|
+
return fallbackDirectoryNameByProfileId;
|
|
2447
|
+
}
|
|
2448
|
+
try {
|
|
2449
|
+
const document = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
2450
|
+
/** @type {Record<string, string>} */
|
|
2451
|
+
const directoryNameByProfileId = {};
|
|
2452
|
+
const profiles = document && typeof document === 'object' ? document.profiles : null;
|
|
2453
|
+
if (profiles && typeof profiles === 'object') {
|
|
2454
|
+
for (const [eachProfileId, eachProfile] of Object.entries(profiles)) {
|
|
2455
|
+
if (eachProfile && typeof eachProfile === 'object' && typeof eachProfile.directoryName === 'string') {
|
|
2456
|
+
directoryNameByProfileId[eachProfileId] = eachProfile.directoryName;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
if (Object.keys(directoryNameByProfileId).length === 0) {
|
|
2461
|
+
return fallbackDirectoryNameByProfileId;
|
|
2462
|
+
}
|
|
2463
|
+
return directoryNameByProfileId;
|
|
2464
|
+
} catch {
|
|
2465
|
+
return fallbackDirectoryNameByProfileId;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/**
|
|
2470
|
+
* Run one install hop for a resolved target. Multi-target parents re-exec this
|
|
2471
|
+
* module with --target and --target-identity so CLAUDE_HOME is correct at load.
|
|
2472
|
+
*
|
|
2473
|
+
* Abort-first: the first non-zero child status ends the loop.
|
|
2474
|
+
*
|
|
2475
|
+
* @param {import('./select-install-targets.mjs').ResolvedInstallTarget[]} allTargets
|
|
2476
|
+
* @param {string[]} childArgv
|
|
2477
|
+
* @returns {void}
|
|
2478
|
+
*/
|
|
2479
|
+
function runInstallForAllTargets(allTargets, childArgv) {
|
|
2480
|
+
if (allTargets.length === 1) {
|
|
2481
|
+
const onlyTarget = allTargets[0];
|
|
2482
|
+
if (onlyTarget.managedRoot !== CLAUDE_HOME || onlyTarget.targetIdentity !== INSTALL_TARGET_IDENTITY) {
|
|
2483
|
+
const childStatus = spawnInstallChild(onlyTarget, childArgv);
|
|
2484
|
+
process.exit(childStatus);
|
|
2485
|
+
}
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
for (const eachTarget of allTargets) {
|
|
2489
|
+
console.log(
|
|
2490
|
+
`\n${PACKAGE_NAME}: target ${eachTarget.targetIdentity} → ${eachTarget.managedRoot}`,
|
|
2491
|
+
);
|
|
2492
|
+
const childStatus = spawnInstallChild(eachTarget, childArgv);
|
|
2493
|
+
if (childStatus !== 0) {
|
|
2494
|
+
console.error(
|
|
2495
|
+
`ERROR: install failed for target ${eachTarget.targetIdentity} (exit ${childStatus})`,
|
|
2496
|
+
);
|
|
2497
|
+
process.exit(childStatus === null ? 1 : childStatus);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
process.exit(0);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* @param {import('./select-install-targets.mjs').ResolvedInstallTarget} target
|
|
2505
|
+
* @param {string[]} childArgv
|
|
2506
|
+
* @returns {number | null}
|
|
2507
|
+
*/
|
|
2508
|
+
function spawnInstallChild(target, childArgv) {
|
|
2509
|
+
const result = spawnSync(
|
|
2510
|
+
process.execPath,
|
|
2511
|
+
[
|
|
2512
|
+
fileURLToPath(import.meta.url),
|
|
2513
|
+
'--target',
|
|
2514
|
+
target.managedRoot,
|
|
2515
|
+
TARGET_IDENTITY_FLAG,
|
|
2516
|
+
target.targetIdentity,
|
|
2517
|
+
...childArgv,
|
|
2518
|
+
],
|
|
2519
|
+
{
|
|
2520
|
+
stdio: 'inherit',
|
|
2521
|
+
env: process.env,
|
|
2522
|
+
},
|
|
2523
|
+
);
|
|
2524
|
+
if (result.error) {
|
|
2525
|
+
console.error(`ERROR: failed to spawn install child: ${result.error.message}`);
|
|
2526
|
+
return 1;
|
|
2527
|
+
}
|
|
2528
|
+
return result.status === null ? 1 : result.status;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2133
2531
|
if (invokedAsEntryPoint(import.meta.url, process.argv[1])) {
|
|
2134
2532
|
const rawArgs = process.argv.slice(2);
|
|
2135
|
-
|
|
2136
|
-
const isUpdateRefresh = rawArgs.includes('--update');
|
|
2137
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
2533
|
+
if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
2138
2534
|
printHelp();
|
|
2139
|
-
} else if (args.includes('--uninstall')) {
|
|
2140
|
-
uninstall();
|
|
2141
2535
|
} else {
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2536
|
+
let targetSelection;
|
|
2537
|
+
try {
|
|
2538
|
+
targetSelection = parseInstallTargetSelectionFromArgv(rawArgs);
|
|
2539
|
+
} catch (selectionError) {
|
|
2540
|
+
const message = selectionError instanceof Error
|
|
2541
|
+
? selectionError.message
|
|
2542
|
+
: String(selectionError);
|
|
2543
|
+
console.error(`ERROR: ${message}`);
|
|
2544
|
+
process.exit(1);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
const homeDirectory = homedir();
|
|
2548
|
+
let allTargets;
|
|
2549
|
+
try {
|
|
2550
|
+
allTargets = resolveInstallTargets(targetSelection, {
|
|
2551
|
+
homeDirectory,
|
|
2552
|
+
environment: process.env,
|
|
2553
|
+
profilesRoot: resolveProfilesRootDirectory({
|
|
2554
|
+
homeDirectory,
|
|
2555
|
+
environment: process.env,
|
|
2556
|
+
}),
|
|
2557
|
+
directoryNameByProfileId: loadDirectoryNameByProfileId(),
|
|
2558
|
+
});
|
|
2559
|
+
} catch (resolveError) {
|
|
2560
|
+
const message = resolveError instanceof Error
|
|
2561
|
+
? resolveError.message
|
|
2562
|
+
: String(resolveError);
|
|
2563
|
+
console.error(`ERROR: ${message}`);
|
|
2564
|
+
process.exit(1);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
const childArgv = stripTargetSelectionFlagsFromArgv(rawArgs);
|
|
2568
|
+
const args = childArgv.filter((flag) => flag !== '--update');
|
|
2569
|
+
const isUpdateRefresh = childArgv.includes('--update');
|
|
2570
|
+
|
|
2571
|
+
const needsChildHop = allTargets.length > 1
|
|
2572
|
+
|| allTargets[0].managedRoot !== CLAUDE_HOME
|
|
2573
|
+
|| (
|
|
2574
|
+
targetSelection.mode === 'profiles'
|
|
2575
|
+
&& allTargets[0].targetIdentity !== INSTALL_TARGET_IDENTITY
|
|
2576
|
+
);
|
|
2577
|
+
|
|
2578
|
+
if (needsChildHop && !args.includes('--uninstall')) {
|
|
2579
|
+
runInstallForAllTargets(allTargets, childArgv);
|
|
2580
|
+
} else if (args.includes('--uninstall')) {
|
|
2581
|
+
if (needsChildHop) {
|
|
2582
|
+
runInstallForAllTargets(allTargets, childArgv);
|
|
2583
|
+
} else {
|
|
2584
|
+
uninstall();
|
|
2149
2585
|
}
|
|
2150
|
-
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2586
|
+
} else {
|
|
2587
|
+
const onlyIndex = args.indexOf('--only');
|
|
2588
|
+
let selectedGroups = null;
|
|
2589
|
+
if (onlyIndex !== -1) {
|
|
2590
|
+
const onlyValue = args[onlyIndex + 1];
|
|
2591
|
+
if (!onlyValue || onlyValue.startsWith('--')) {
|
|
2592
|
+
console.error(`ERROR: --only requires a comma-separated list of groups.\nAvailable groups: ${Object.keys(INSTALL_GROUPS).join(', ')}`);
|
|
2593
|
+
process.exit(1);
|
|
2594
|
+
}
|
|
2595
|
+
selectedGroups = onlyValue.split(',').map(name => name.trim());
|
|
2596
|
+
const invalidGroups = selectedGroups.filter(name => !INSTALL_GROUPS[name]);
|
|
2597
|
+
if (invalidGroups.length > 0) {
|
|
2598
|
+
console.error(`ERROR: Unknown group(s): ${invalidGroups.join(', ')}\nAvailable groups: ${Object.keys(INSTALL_GROUPS).join(', ')}`);
|
|
2599
|
+
process.exit(1);
|
|
2600
|
+
}
|
|
2155
2601
|
}
|
|
2602
|
+
install(selectedGroups, { isUpdateRefresh });
|
|
2156
2603
|
}
|
|
2157
|
-
install(selectedGroups, { isUpdateRefresh });
|
|
2158
2604
|
}
|
|
2159
2605
|
}
|