claude-dev-env 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +8 -2
- package/_shared/pr-loop/CLAUDE.md +1 -1
- package/_shared/pr-loop/audit-contract.md +17 -6
- package/_shared/pr-loop/audit-reply-template.md +4 -4
- package/_shared/pr-loop/code-rules-gate.md +3 -5
- package/_shared/pr-loop/fix-protocol.md +2 -3
- package/_shared/pr-loop/gh-payloads.md +1 -1
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/README.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
- package/_shared/pr-loop/scripts/preflight.py +9 -4
- package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
- package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/state-schema.md +5 -14
- package/agents/CLAUDE.md +2 -2
- package/agents/clean-coder.md +62 -562
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +58 -40
- package/audit-rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/audit-categories.json +704 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
- package/bin/CLAUDE.md +16 -5
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/install-plan.mjs +402 -0
- package/bin/install-transaction.mjs +455 -0
- package/bin/install.mjs +593 -147
- package/bin/install.plan.test.mjs +194 -0
- package/bin/install.profile-root.test.mjs +154 -0
- package/bin/install.profiles.test.mjs +253 -0
- package/bin/install.settings-defaults.test.mjs +200 -0
- package/bin/install.transaction.test.mjs +400 -0
- package/bin/install.uninstall-transaction.test.mjs +418 -0
- package/bin/merge_managed_permissions.mjs +130 -0
- package/bin/resolve-install-root.mjs +181 -0
- package/bin/select-install-targets.mjs +401 -0
- package/commands/CLAUDE.md +0 -2
- package/docs/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +3 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/prose-style-enforcement.md +25 -0
- package/docs/references/team-advisor-skill.md +14 -8
- package/hooks/blocking/CLAUDE.md +6 -6
- package/hooks/blocking/_path_setup.py +9 -5
- package/hooks/blocking/code_rules_docstrings.py +124 -30
- package/hooks/blocking/code_rules_enforcer.py +161 -16
- package/hooks/blocking/code_rules_shared.py +40 -23
- package/hooks/blocking/config/CLAUDE.md +3 -5
- package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
- package/hooks/blocking/eli11_reply_enforcer.py +70 -113
- package/hooks/blocking/hedging_language_blocker.py +103 -20
- package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
- package/hooks/blocking/intent_only_ending_blocker.py +6 -0
- package/hooks/blocking/plain_language_blocker.py +139 -20
- package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
- package/hooks/blocking/state_description_blocker.py +7 -1
- package/hooks/blocking/tdd_enforcer.py +8 -0
- package/hooks/blocking/test__path_setup.py +28 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
- package/hooks/blocking/test_hedging_language_blocker.py +120 -1
- package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
- package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
- package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
- package/hooks/blocking/test_plain_language_blocker.py +129 -19
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
- package/hooks/blocking/test_state_description_blocker.py +45 -2
- package/hooks/blocking/test_stop_dispatcher.py +11 -7
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
- package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/CLAUDE.md +14 -3
- package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
- package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/project_paths_reader.py +31 -4
- package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
- package/hooks/hooks_constants/pytest_invocation.py +354 -0
- package/hooks/hooks_constants/session_start_injector.py +163 -0
- package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
- package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
- package/hooks/hooks_constants/shell_command_segments.py +5 -0
- package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
- package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
- package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
- package/hooks/hooks_constants/test_session_start_injector.py +168 -0
- package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
- package/hooks/observability/CLAUDE.md +2 -0
- package/hooks/observability/prose_matcher_advisory.py +237 -0
- package/hooks/observability/test_prose_matcher_advisory.py +143 -0
- package/hooks/session/CLAUDE.md +9 -1
- package/hooks/session/_path_setup.py +13 -0
- package/hooks/session/issue_tracker_session_starter.py +135 -0
- package/hooks/session/orchestrator_auto_starter.py +100 -0
- package/hooks/session/test__path_setup.py +28 -0
- package/hooks/session/test_issue_tracker_session_starter.py +104 -0
- package/hooks/session/test_orchestrator_auto_starter.py +99 -0
- package/hooks/session/test_working_style_prompt.py +47 -0
- package/hooks/session/untracked_repo_detector.py +1 -24
- package/hooks/session/working_style_prompt.py +36 -0
- package/hooks/validators/_path_setup.py +19 -0
- package/hooks/validators/run_all_validators.py +8 -13
- package/installable-surfaces.manifest.json +21 -0
- package/output-styles/CLAUDE.md +15 -0
- package/package.json +5 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/code-standards.md +33 -7
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +7 -2
- package/rules/hedging-claims.md +4 -2
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/opus5-communication-contract.md +45 -0
- package/rules/plain-language.md +2 -2
- package/rules/research-mode.md +1 -1
- package/scripts/CLAUDE.md +13 -2
- package/scripts/Sync-RepoMain.ps1 +215 -0
- package/scripts/active_capability_references.py +218 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
- package/scripts/claude_chain_runner.py +394 -6
- package/scripts/claude_chain_usage.py +1 -1
- package/scripts/codex_compat_materializer.py +105 -85
- package/scripts/dev_env_scripts_constants/CLAUDE.md +3 -1
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
- package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
- package/scripts/spawn_grok_batch.py +556 -9
- package/scripts/test_active_capability_references.py +108 -0
- package/scripts/test_claude_chain_runner.py +414 -82
- package/scripts/test_claude_chain_usage.py +12 -12
- package/scripts/test_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_spawn_grok_batch.py +295 -0
- package/scripts/test_verify_installable_package.py +208 -0
- package/scripts/tests/test_codex_compat_materializer.py +33 -0
- package/scripts/verify_installable_package.py +612 -0
- package/settings.json +10 -0
- package/skills/CLAUDE.md +6 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
- package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
- package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
- package/skills/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -0
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/stop-conditions.md +5 -3
- package/skills/beat-sheet/SKILL.md +54 -0
- package/skills/beat-sheet/reference/visual-beats.md +29 -0
- package/skills/bugteam/CONSTRAINTS.md +4 -4
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/e-code-review/SKILL.md +29 -3
- package/skills/e-code-review/reference/effort-evaluation.md +35 -0
- package/skills/e-code-review/reference/medium.md +15 -4
- package/skills/e-code-review/reference/runner-selection.md +40 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
- package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
- package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
- package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
- package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
- package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
- package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
- package/skills/e-code-review/scripts/grok_code_review.py +230 -0
- package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
- package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +289 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +5 -0
- package/skills/plan-to-pr/scripts/create_packet.py +4 -4
- package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
- package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
- package/skills/plan-to-pr/scripts/validate_run.py +4 -1
- package/skills/pr-converge/scripts/check_convergence.py +21 -19
- package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
- package/skills/pr-converge/scripts/conftest.py +35 -0
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/rebase/SKILL.md +15 -3
- package/skills/reviewer-gates/SKILL.md +2 -2
- package/skills/show/SKILL.md +51 -0
- package/skills/show/references/accessibility.md +7 -0
- package/skills/show/references/art.md +3 -0
- package/skills/show/references/charts.md +3 -0
- package/skills/show/references/core-design.md +14 -0
- package/skills/show/references/erds.md +3 -0
- package/skills/show/references/flowcharts.md +3 -0
- package/skills/show/references/host-and-html.md +3 -0
- package/skills/show/references/illustrative-diagrams.md +10 -0
- package/skills/show/references/interaction.md +3 -0
- package/skills/show/references/mockups.md +3 -0
- package/skills/show/references/quality-gates.md +7 -0
- package/skills/show/references/structural-diagrams.md +3 -0
- package/skills/show/references/subject-inventory.md +21 -0
- package/skills/show/references/svg-contract.md +22 -0
- package/skills/show/routing.yaml +30 -0
- package/skills/show/samples/pr1262-v2.svg +222 -0
- package/skills/show/scripts/README.md +6 -0
- package/skills/show/scripts/validate-artifact.py +91 -0
- package/skills/show/scripts/validate-package.py +18 -0
- package/skills/show/templates/html-widget.html +4 -0
- package/skills/show/templates/svg-base.svg +19 -0
- package/skills/show/tests/fixtures/css-var.svg +6 -0
- package/skills/show/tests/fixtures/dead-ref.svg +7 -0
- package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
- package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
- package/skills/show/tests/fixtures/invalid.svg +1 -0
- package/skills/show/tests/fixtures/large-canvas.svg +21 -0
- package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
- package/skills/show/tests/fixtures/valid.html +1 -0
- package/skills/show/tests/test_validate-artifact.py +74 -0
- package/skills/show/tests/test_validators.py +59 -0
- package/skills/show/workflows/create-visual.md +13 -0
- package/skills/show/workflows/review-visual.md +20 -0
- package/skills/split-pr/SKILL.md +85 -0
- package/skills/split-pr/reference/path-layers.md +16 -0
- package/skills/split-pr/reference/proposal-format.md +15 -0
- package/skills/split-pr/reference/split-further-loop.md +10 -0
- package/skills/split-pr/reference/splitting-principles.md +26 -0
- package/skills/split-pr/scripts/analyze_pr.py +279 -0
- package/skills/split-pr/scripts/categorize_files.py +106 -0
- package/skills/split-pr/scripts/config/__init__.py +1 -0
- package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
- package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
- package/skills/split-pr/scripts/config/packing_constants.py +61 -0
- package/skills/split-pr/scripts/config/plan_constants.py +49 -0
- package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
- package/skills/split-pr/scripts/execute_split_slices.py +82 -0
- package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
- package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
- package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
- package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
- package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
- package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
- package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
- package/skills/split-pr/scripts/split_pr_title.py +41 -0
- package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
- package/skills/split-pr/scripts/test_categorize_files.py +55 -0
- package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
- package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
- package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
- package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
- package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
- package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
- package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
- package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
- package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
- package/skills/split-pr/scripts/test_verify_plan.py +56 -0
- package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
- package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
- package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
- package/skills/split-pr/scripts/verify_plan.py +139 -0
- package/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -44
- package/system-prompts/software-engineer.xml +17 -5
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/skills/test_markdown_link_integrity.py +0 -107
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launcher runtime helpers for MCP activation through a supported interface.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE,
|
|
8
|
+
loadProfilesManifestDocument,
|
|
9
|
+
MCP_BUNDLE_FULL,
|
|
10
|
+
MCP_BUNDLE_LEAN,
|
|
11
|
+
} from './config/profile-isolation-constants.mjs';
|
|
12
|
+
import {
|
|
13
|
+
resolveProfileDefinition,
|
|
14
|
+
validateProfilesManifest,
|
|
15
|
+
} from './lib/profile-manifest.mjs';
|
|
16
|
+
import {
|
|
17
|
+
listServerNamesForBundle,
|
|
18
|
+
loadMcpBundlesDocument,
|
|
19
|
+
materializeProfileMcpConfig,
|
|
20
|
+
resolveProfileMcpBundleId,
|
|
21
|
+
SUPPORTED_ACTIVATION_INTERFACE,
|
|
22
|
+
} from './mcp-bundles.mjs';
|
|
23
|
+
import {
|
|
24
|
+
classifyVersionCompatibility,
|
|
25
|
+
shouldBlockLaunch,
|
|
26
|
+
} from './version-compatibility.mjs';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a launcher name to a profile id from the profiles manifest.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} launcherName
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function resolveProfileIdForLauncherName(launcherName) {
|
|
35
|
+
const manifest = validateProfilesManifest(loadProfilesManifestDocument());
|
|
36
|
+
try {
|
|
37
|
+
return resolveProfileDefinition(manifest, launcherName).id;
|
|
38
|
+
} catch (errorValue) {
|
|
39
|
+
if (
|
|
40
|
+
errorValue instanceof Error
|
|
41
|
+
&& errorValue.message.startsWith('Unknown profile id or alias:')
|
|
42
|
+
) {
|
|
43
|
+
throw new Error(`no profile owns launcher name: ${launcherName}`);
|
|
44
|
+
}
|
|
45
|
+
throw errorValue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Assemble activation for one profile into a disposable CLAUDE_CONFIG_DIR.
|
|
51
|
+
*
|
|
52
|
+
* @param {{
|
|
53
|
+
* profileId: string,
|
|
54
|
+
* claudeConfigDir: string,
|
|
55
|
+
* }} parameters
|
|
56
|
+
* @returns {{
|
|
57
|
+
* profileId: string,
|
|
58
|
+
* environment: Record<string, string>,
|
|
59
|
+
* activationInterface: string,
|
|
60
|
+
* mcpConfigPath: string,
|
|
61
|
+
* bundleId: string,
|
|
62
|
+
* allServerNames: string[],
|
|
63
|
+
* expectedInventory: string[],
|
|
64
|
+
* }}
|
|
65
|
+
*/
|
|
66
|
+
export function activateProfileMcpBundle(parameters) {
|
|
67
|
+
const bundlesDocument = loadMcpBundlesDocument();
|
|
68
|
+
const materialization = materializeProfileMcpConfig({
|
|
69
|
+
profileId: parameters.profileId,
|
|
70
|
+
claudeConfigDir: parameters.claudeConfigDir,
|
|
71
|
+
bundlesDocument,
|
|
72
|
+
});
|
|
73
|
+
const expectedInventory = [...materialization.allServerNames].sort();
|
|
74
|
+
return {
|
|
75
|
+
profileId: parameters.profileId,
|
|
76
|
+
environment: {
|
|
77
|
+
[CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE]: parameters.claudeConfigDir,
|
|
78
|
+
},
|
|
79
|
+
activationInterface: materialization.activationInterface,
|
|
80
|
+
mcpConfigPath: materialization.mcpConfigPath,
|
|
81
|
+
bundleId: materialization.bundleId,
|
|
82
|
+
allServerNames: materialization.allServerNames,
|
|
83
|
+
expectedInventory,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Activate MCP for a launcher name (claude-profile-a / claude-profile-b).
|
|
89
|
+
*
|
|
90
|
+
* @param {{
|
|
91
|
+
* launcherName: string,
|
|
92
|
+
* claudeConfigDir: string,
|
|
93
|
+
* }} parameters
|
|
94
|
+
* @returns {ReturnType<typeof activateProfileMcpBundle>}
|
|
95
|
+
*/
|
|
96
|
+
export function activateLauncherMcpBundle(parameters) {
|
|
97
|
+
const profileId = resolveProfileIdForLauncherName(parameters.launcherName);
|
|
98
|
+
return activateProfileMcpBundle({
|
|
99
|
+
profileId,
|
|
100
|
+
claudeConfigDir: parameters.claudeConfigDir,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Lean-boundary check: lean inventory is a subset of full and excludes full-only servers.
|
|
106
|
+
*
|
|
107
|
+
* @returns {{leanServers: string[], fullServers: string[], fullOnlyServers: string[]}}
|
|
108
|
+
*/
|
|
109
|
+
export function describeLeanServerBoundary() {
|
|
110
|
+
const bundlesDocument = loadMcpBundlesDocument();
|
|
111
|
+
const leanServers = listServerNamesForBundle(MCP_BUNDLE_LEAN, bundlesDocument).sort();
|
|
112
|
+
const fullServers = listServerNamesForBundle(MCP_BUNDLE_FULL, bundlesDocument).sort();
|
|
113
|
+
const leanSet = new Set(leanServers);
|
|
114
|
+
const fullOnlyServers = fullServers.filter((eachName) => !leanSet.has(eachName));
|
|
115
|
+
return { leanServers, fullServers, fullOnlyServers };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build a diagnostic when a required bundle or profile is invalid.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} profileId
|
|
122
|
+
* @param {unknown} errorValue
|
|
123
|
+
* @returns {string}
|
|
124
|
+
*/
|
|
125
|
+
export function formatProfileMcpActivationFailure(profileId, errorValue) {
|
|
126
|
+
const message = errorValue instanceof Error ? errorValue.message : String(errorValue);
|
|
127
|
+
return `profile ${profileId} mcp activation failed: ${message}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Evaluate CLI vs Desktop version compatibility from already-collected probes.
|
|
132
|
+
*
|
|
133
|
+
* Call this before any profile-state mutation. Spawn and binary discovery stay
|
|
134
|
+
* in the caller; this adapter only runs the shared classifier.
|
|
135
|
+
*
|
|
136
|
+
* @param {{
|
|
137
|
+
* cli: import('./version-compatibility.mjs').VersionProbeResult,
|
|
138
|
+
* desktop: import('./version-compatibility.mjs').VersionProbeResult,
|
|
139
|
+
* }} parameters
|
|
140
|
+
* @returns {import('./version-compatibility.mjs').CompatibilityResult}
|
|
141
|
+
*/
|
|
142
|
+
export function evaluateLauncherVersionCompatibility(parameters) {
|
|
143
|
+
return classifyVersionCompatibility(parameters);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build a preflight failure message when compatibility blocks launch.
|
|
148
|
+
*
|
|
149
|
+
* @param {import('./version-compatibility.mjs').CompatibilityResult} result
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
export function formatCompatibilityPreflightFailure(result) {
|
|
153
|
+
return `version compatibility preflight blocked launch (${result.class}): ${result.message}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Path helpers for tests and pack verification.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} packageRoot
|
|
160
|
+
* @returns {string[]}
|
|
161
|
+
*/
|
|
162
|
+
export function listMcpActivationPackageRelativePaths(packageRoot) {
|
|
163
|
+
return [
|
|
164
|
+
join(packageRoot, 'scripts/profile-isolation-launchers/mcp-bundles.mjs'),
|
|
165
|
+
join(packageRoot, 'scripts/profile-isolation-launchers/launcher-runtime.mjs'),
|
|
166
|
+
join(packageRoot, 'scripts/profile-isolation-launchers/version-compatibility.mjs'),
|
|
167
|
+
join(packageRoot, 'scripts/profile-isolation-launchers/config/mcp-bundles.json'),
|
|
168
|
+
join(packageRoot, 'scripts/profile-isolation-launchers/config/profiles.manifest.json'),
|
|
169
|
+
join(packageRoot, 'scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs'),
|
|
170
|
+
join(packageRoot, 'scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs'),
|
|
171
|
+
join(packageRoot, 'scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs'),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
SUPPORTED_ACTIVATION_INTERFACE,
|
|
177
|
+
resolveProfileMcpBundleId,
|
|
178
|
+
classifyVersionCompatibility,
|
|
179
|
+
shouldBlockLaunch,
|
|
180
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LAUNCHER_SCHEMA_VERSION,
|
|
5
|
+
loadProfilesManifestDocument,
|
|
6
|
+
loadSharedAllowlistDocument,
|
|
7
|
+
MCP_BUNDLE_FULL,
|
|
8
|
+
MCP_BUNDLE_LEAN,
|
|
9
|
+
MIGRATION_MODE_CLEAN_LOCAL_RUNTIME,
|
|
10
|
+
MIGRATION_MODE_MATERIALIZE_FROM_LEGACY,
|
|
11
|
+
} from '../config/profile-isolation-constants.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{
|
|
15
|
+
* id: string,
|
|
16
|
+
* aliases: string[],
|
|
17
|
+
* directoryName: string,
|
|
18
|
+
* launcherNames: string[],
|
|
19
|
+
* fullLauncherNames: string[],
|
|
20
|
+
* migrationMode: string,
|
|
21
|
+
* mcpBundle: string,
|
|
22
|
+
* }} ProfileDefinition
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* schemaVersion: number,
|
|
28
|
+
* profilesRootPlaceholder: string,
|
|
29
|
+
* sharedSourcePlaceholder: string,
|
|
30
|
+
* pluginSeedPlaceholder: string,
|
|
31
|
+
* migrationOrder: string[],
|
|
32
|
+
* profileById: Record<string, ProfileDefinition>,
|
|
33
|
+
* }} ValidatedProfilesManifest
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {{
|
|
38
|
+
* schemaVersion: number,
|
|
39
|
+
* allSharedRelativePaths: string[],
|
|
40
|
+
* allAlwaysLocalRelativePaths: string[],
|
|
41
|
+
* allDesktopExcludedPathFragments: string[],
|
|
42
|
+
* }} ValidatedSharedAllowlist
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const ALL_ALLOWED_MIGRATION_MODES = new Set([
|
|
46
|
+
MIGRATION_MODE_CLEAN_LOCAL_RUNTIME,
|
|
47
|
+
MIGRATION_MODE_MATERIALIZE_FROM_LEGACY,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const ALL_ALLOWED_MCP_BUNDLES = new Set([MCP_BUNDLE_LEAN, MCP_BUNDLE_FULL]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {unknown} maybeManifest
|
|
54
|
+
* @returns {ValidatedProfilesManifest}
|
|
55
|
+
*/
|
|
56
|
+
export function validateProfilesManifest(maybeManifest) {
|
|
57
|
+
if (!isPlainObject(maybeManifest)) {
|
|
58
|
+
throw new Error('profiles manifest must be a JSON object');
|
|
59
|
+
}
|
|
60
|
+
const schemaVersion = maybeManifest.schemaVersion;
|
|
61
|
+
if (schemaVersion !== LAUNCHER_SCHEMA_VERSION) {
|
|
62
|
+
throw new Error(`profiles manifest schemaVersion must be ${LAUNCHER_SCHEMA_VERSION}`);
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(maybeManifest.migrationOrder) || maybeManifest.migrationOrder.length === 0) {
|
|
65
|
+
throw new Error('profiles manifest migrationOrder must be a non-empty array');
|
|
66
|
+
}
|
|
67
|
+
if (!isPlainObject(maybeManifest.profiles)) {
|
|
68
|
+
throw new Error('profiles manifest profiles must be an object');
|
|
69
|
+
}
|
|
70
|
+
/** @type {Record<string, ProfileDefinition>} */
|
|
71
|
+
const profileById = {};
|
|
72
|
+
for (const [eachProfileKey, eachProfileValue] of Object.entries(maybeManifest.profiles)) {
|
|
73
|
+
profileById[eachProfileKey] = validateProfileDefinition(eachProfileKey, eachProfileValue);
|
|
74
|
+
}
|
|
75
|
+
assertUniqueProfileIdentities(profileById);
|
|
76
|
+
/** @type {string[]} */
|
|
77
|
+
const allMigrationOrderIds = [];
|
|
78
|
+
for (const eachOrderedProfileId of maybeManifest.migrationOrder) {
|
|
79
|
+
if (typeof eachOrderedProfileId !== 'string' || !(eachOrderedProfileId in profileById)) {
|
|
80
|
+
throw new Error(`migrationOrder references unknown profile id: ${String(eachOrderedProfileId)}`);
|
|
81
|
+
}
|
|
82
|
+
allMigrationOrderIds.push(eachOrderedProfileId);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
schemaVersion,
|
|
86
|
+
profilesRootPlaceholder: requireNonEmptyString(
|
|
87
|
+
maybeManifest.profilesRootPlaceholder,
|
|
88
|
+
'profilesRootPlaceholder',
|
|
89
|
+
),
|
|
90
|
+
sharedSourcePlaceholder: requireNonEmptyString(
|
|
91
|
+
maybeManifest.sharedSourcePlaceholder,
|
|
92
|
+
'sharedSourcePlaceholder',
|
|
93
|
+
),
|
|
94
|
+
pluginSeedPlaceholder: requireNonEmptyString(
|
|
95
|
+
maybeManifest.pluginSeedPlaceholder,
|
|
96
|
+
'pluginSeedPlaceholder',
|
|
97
|
+
),
|
|
98
|
+
migrationOrder: allMigrationOrderIds,
|
|
99
|
+
profileById,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {unknown} maybeAllowlist
|
|
105
|
+
* @returns {ValidatedSharedAllowlist}
|
|
106
|
+
*/
|
|
107
|
+
export function validateSharedAllowlist(maybeAllowlist) {
|
|
108
|
+
if (!isPlainObject(maybeAllowlist)) {
|
|
109
|
+
throw new Error('shared allowlist must be a JSON object');
|
|
110
|
+
}
|
|
111
|
+
if (maybeAllowlist.schemaVersion !== LAUNCHER_SCHEMA_VERSION) {
|
|
112
|
+
throw new Error(`shared allowlist schemaVersion must be ${LAUNCHER_SCHEMA_VERSION}`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
schemaVersion: LAUNCHER_SCHEMA_VERSION,
|
|
116
|
+
allSharedRelativePaths: requireStringArray(
|
|
117
|
+
maybeAllowlist.allSharedRelativePaths,
|
|
118
|
+
'allSharedRelativePaths',
|
|
119
|
+
),
|
|
120
|
+
allAlwaysLocalRelativePaths: requireStringArray(
|
|
121
|
+
maybeAllowlist.allAlwaysLocalRelativePaths,
|
|
122
|
+
'allAlwaysLocalRelativePaths',
|
|
123
|
+
),
|
|
124
|
+
allDesktopExcludedPathFragments: requireStringArray(
|
|
125
|
+
maybeAllowlist.allDesktopExcludedPathFragments,
|
|
126
|
+
'allDesktopExcludedPathFragments',
|
|
127
|
+
),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @returns {ValidatedProfilesManifest}
|
|
133
|
+
*/
|
|
134
|
+
export function loadAndValidateProfilesManifest() {
|
|
135
|
+
return validateProfilesManifest(loadProfilesManifestDocument());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @returns {ValidatedSharedAllowlist}
|
|
140
|
+
*/
|
|
141
|
+
export function loadAndValidateSharedAllowlist() {
|
|
142
|
+
return validateSharedAllowlist(loadSharedAllowlistDocument());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {ValidatedProfilesManifest} validatedManifest
|
|
147
|
+
* @param {string} profileIdOrAlias
|
|
148
|
+
* @returns {ProfileDefinition}
|
|
149
|
+
*/
|
|
150
|
+
export function resolveProfileDefinition(validatedManifest, profileIdOrAlias) {
|
|
151
|
+
if (typeof profileIdOrAlias !== 'string') {
|
|
152
|
+
throw new Error(`Unknown profile id or alias: ${String(profileIdOrAlias)}`);
|
|
153
|
+
}
|
|
154
|
+
const normalizedIdentity = profileIdOrAlias.trim().toLowerCase();
|
|
155
|
+
for (const eachProfile of Object.values(validatedManifest.profileById)) {
|
|
156
|
+
const allProfileIdentities = [
|
|
157
|
+
eachProfile.id,
|
|
158
|
+
...eachProfile.aliases,
|
|
159
|
+
...eachProfile.launcherNames,
|
|
160
|
+
...eachProfile.fullLauncherNames,
|
|
161
|
+
];
|
|
162
|
+
if (
|
|
163
|
+
allProfileIdentities.some(
|
|
164
|
+
(eachIdentity) => eachIdentity.trim().toLowerCase() === normalizedIdentity,
|
|
165
|
+
)
|
|
166
|
+
) {
|
|
167
|
+
return eachProfile;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`Unknown profile id or alias: ${profileIdOrAlias}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} profilesRootDirectoryPath
|
|
175
|
+
* @param {ProfileDefinition} profileDefinition
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
export function resolveProfileRootDirectoryPath(profilesRootDirectoryPath, profileDefinition) {
|
|
179
|
+
return join(profilesRootDirectoryPath, profileDefinition.directoryName);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reject a launcher name, alias, or id claimed by more than one profile.
|
|
184
|
+
*
|
|
185
|
+
* ::
|
|
186
|
+
*
|
|
187
|
+
* profiles['profile-c'].launcherNames = ["claude"]
|
|
188
|
+
* profiles.master.launcherNames = ["claude"]
|
|
189
|
+
* flag: same identity claimed twice → resolve order would be nondeterministic
|
|
190
|
+
* ok: each identity maps to exactly one profile id
|
|
191
|
+
*
|
|
192
|
+
* @param {Record<string, ProfileDefinition>} profileById
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
function assertUniqueProfileIdentities(profileById) {
|
|
196
|
+
/** @type {Map<string, string>} */
|
|
197
|
+
const profileIdByNormalizedIdentity = new Map();
|
|
198
|
+
for (const eachProfile of Object.values(profileById)) {
|
|
199
|
+
const allProfileIdentities = [
|
|
200
|
+
eachProfile.id,
|
|
201
|
+
...eachProfile.aliases,
|
|
202
|
+
...eachProfile.launcherNames,
|
|
203
|
+
...eachProfile.fullLauncherNames,
|
|
204
|
+
];
|
|
205
|
+
for (const eachIdentity of allProfileIdentities) {
|
|
206
|
+
const normalizedIdentity = eachIdentity.trim().toLowerCase();
|
|
207
|
+
const previousProfileId = profileIdByNormalizedIdentity.get(normalizedIdentity);
|
|
208
|
+
if (previousProfileId !== undefined) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`duplicate profile identity '${eachIdentity}' claimed by ${previousProfileId} and ${eachProfile.id}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
profileIdByNormalizedIdentity.set(normalizedIdentity, eachProfile.id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @param {string} profileKey
|
|
220
|
+
* @param {unknown} maybeProfile
|
|
221
|
+
* @returns {ProfileDefinition}
|
|
222
|
+
*/
|
|
223
|
+
function validateProfileDefinition(profileKey, maybeProfile) {
|
|
224
|
+
if (!isPlainObject(maybeProfile)) {
|
|
225
|
+
throw new Error(`profile ${profileKey} must be an object`);
|
|
226
|
+
}
|
|
227
|
+
const profileId = requireNonEmptyString(maybeProfile.id, `${profileKey}.id`);
|
|
228
|
+
if (profileId !== profileKey) {
|
|
229
|
+
throw new Error(`profile key ${profileKey} must match id ${profileId}`);
|
|
230
|
+
}
|
|
231
|
+
const migrationMode = requireNonEmptyString(maybeProfile.migrationMode, `${profileKey}.migrationMode`);
|
|
232
|
+
if (!ALL_ALLOWED_MIGRATION_MODES.has(migrationMode)) {
|
|
233
|
+
throw new Error(`profile ${profileKey} has unsupported migrationMode: ${migrationMode}`);
|
|
234
|
+
}
|
|
235
|
+
const mcpBundle = requireNonEmptyString(maybeProfile.mcpBundle, `${profileKey}.mcpBundle`);
|
|
236
|
+
if (!ALL_ALLOWED_MCP_BUNDLES.has(mcpBundle)) {
|
|
237
|
+
throw new Error(`profile ${profileKey} has unsupported mcpBundle: ${mcpBundle}`);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
id: profileId,
|
|
241
|
+
aliases: requireStringArray(maybeProfile.aliases, `${profileKey}.aliases`),
|
|
242
|
+
directoryName: requireNonEmptyString(maybeProfile.directoryName, `${profileKey}.directoryName`),
|
|
243
|
+
launcherNames: requireStringArray(maybeProfile.launcherNames, `${profileKey}.launcherNames`),
|
|
244
|
+
fullLauncherNames: requireStringArray(
|
|
245
|
+
maybeProfile.fullLauncherNames,
|
|
246
|
+
`${profileKey}.fullLauncherNames`,
|
|
247
|
+
),
|
|
248
|
+
migrationMode,
|
|
249
|
+
mcpBundle,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {unknown} candidate
|
|
255
|
+
* @returns {candidate is Record<string, unknown>}
|
|
256
|
+
*/
|
|
257
|
+
function isPlainObject(candidate) {
|
|
258
|
+
return typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {unknown} candidate
|
|
263
|
+
* @param {string} fieldName
|
|
264
|
+
* @returns {string}
|
|
265
|
+
*/
|
|
266
|
+
function requireNonEmptyString(candidate, fieldName) {
|
|
267
|
+
if (typeof candidate !== 'string' || candidate.trim() === '') {
|
|
268
|
+
throw new Error(`${fieldName} must be a non-empty string`);
|
|
269
|
+
}
|
|
270
|
+
return candidate;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @param {unknown} candidate
|
|
275
|
+
* @param {string} fieldName
|
|
276
|
+
* @returns {string[]}
|
|
277
|
+
*/
|
|
278
|
+
function requireStringArray(candidate, fieldName) {
|
|
279
|
+
if (
|
|
280
|
+
!Array.isArray(candidate) ||
|
|
281
|
+
candidate.some(
|
|
282
|
+
(eachEntry) => typeof eachEntry !== 'string' || eachEntry.trim() === '',
|
|
283
|
+
)
|
|
284
|
+
) {
|
|
285
|
+
throw new Error(`${fieldName} must be an array of non-empty strings`);
|
|
286
|
+
}
|
|
287
|
+
return /** @type {string[]} */ (candidate);
|
|
288
|
+
}
|