@uluops/setup 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +91 -9
  2. package/assets/codex/skills/uluops-operator/SKILL.md +159 -0
  3. package/dist/cli/select-harnesses.d.ts +91 -0
  4. package/dist/cli/select-harnesses.js +108 -0
  5. package/dist/cli.js +78 -37
  6. package/dist/commands/errors.d.ts +24 -0
  7. package/dist/commands/errors.js +28 -0
  8. package/dist/commands/helpers.d.ts +7 -0
  9. package/dist/commands/helpers.js +80 -3
  10. package/dist/commands/per-harness.d.ts +64 -0
  11. package/dist/commands/per-harness.js +37 -0
  12. package/dist/commands/setup.d.ts +5 -3
  13. package/dist/commands/setup.js +174 -48
  14. package/dist/commands/uninstall-filter.d.ts +36 -0
  15. package/dist/commands/uninstall-filter.js +69 -0
  16. package/dist/commands/uninstall.d.ts +12 -2
  17. package/dist/commands/uninstall.js +121 -45
  18. package/dist/harnesses/codex.d.ts +5 -10
  19. package/dist/harnesses/codex.js +89 -21
  20. package/dist/harnesses/index.js +6 -1
  21. package/dist/harnesses/opencode.d.ts +8 -0
  22. package/dist/harnesses/opencode.js +24 -1
  23. package/dist/harnesses/types.d.ts +2 -0
  24. package/dist/harnesses/types.js +5 -1
  25. package/dist/lib/atomic-write.js +10 -2
  26. package/dist/lib/config-merger.d.ts +6 -3
  27. package/dist/lib/config-merger.js +50 -7
  28. package/dist/lib/display.d.ts +21 -5
  29. package/dist/lib/display.js +118 -13
  30. package/dist/lib/file-ops.d.ts +13 -5
  31. package/dist/lib/file-ops.js +34 -34
  32. package/dist/lib/install-lock.js +11 -1
  33. package/dist/lib/json-guards.d.ts +22 -0
  34. package/dist/lib/json-guards.js +33 -0
  35. package/dist/lib/manifest.d.ts +20 -0
  36. package/dist/lib/manifest.js +61 -12
  37. package/dist/lib/paths.d.ts +0 -17
  38. package/dist/lib/paths.js +0 -19
  39. package/dist/lib/settings-merger.js +3 -1
  40. package/dist/steps/agent-metrics-cli.d.ts +9 -1
  41. package/dist/steps/agent-metrics-cli.js +66 -20
  42. package/dist/steps/agents.d.ts +11 -0
  43. package/dist/steps/agents.js +30 -25
  44. package/dist/steps/auth.d.ts +13 -0
  45. package/dist/steps/auth.js +61 -5
  46. package/dist/steps/cli.d.ts +6 -0
  47. package/dist/steps/cli.js +29 -10
  48. package/dist/steps/commands.d.ts +10 -0
  49. package/dist/steps/commands.js +31 -30
  50. package/dist/steps/detect.js +15 -1
  51. package/dist/steps/mcp.js +1 -8
  52. package/dist/steps/metrics.js +10 -3
  53. package/dist/steps/shell.js +3 -13
  54. package/dist/steps/signup.js +14 -1
  55. package/dist/steps/skills.d.ts +14 -0
  56. package/dist/steps/skills.js +95 -0
  57. package/dist/steps/verify.js +195 -91
  58. package/package.json +3 -2
package/README.md CHANGED
@@ -19,19 +19,68 @@ npx @uluops/setup
19
19
  | Claude Code | Fully supported (default) | `claude` | `~/.claude.json` |
20
20
  | OpenCode | Fully supported | `oc` | `~/.config/opencode/opencode.json` |
21
21
  | Gemini CLI | Fully supported | `gemini` | `~/.gemini/settings.json` |
22
- | Codex | Coming soon | — | `~/.codex/config.toml` |
22
+ | Codex | Experimental (opt-in via `--harness codex`) | — | `~/.codex/config.toml` |
23
23
 
24
24
  ```bash
25
25
  # Install for Claude Code (default)
26
26
  npx @uluops/setup
27
27
 
28
- # Install for OpenCode
28
+ # Install for one specific harness
29
29
  npx @uluops/setup --harness opencode
30
-
31
- # Install for Gemini CLI
32
30
  npx @uluops/setup --harness gemini-cli
31
+
32
+ # Install into every detected stable harness in a single run
33
+ npx @uluops/setup --all-detected
34
+
35
+ # Install into a specific subset (comma-separated)
36
+ npx @uluops/setup --harness claude-code,gemini-cli
37
+ ```
38
+
39
+ ### Harness auto-detection
40
+
41
+ If you don't pass `--harness` or `--all-detected`, setup probes your home directory for known harness install markers and picks a target:
42
+
43
+ - **One harness detected** — that harness is used as the target. A dimmed `Detected <Name>` line confirms the choice (suppressed when the detected harness is the default `claude-code`).
44
+ - **Multiple harnesses detected (interactive)** — you get a multi-select checkbox listing every detected harness, with **every option checked by default** so the "install everywhere" case is a single Enter press. Use space to toggle entries off.
45
+ - **Multiple harnesses detected (non-interactive — `--yes`, `--api-key`, piped stdin)** — to keep CI scripts predictable, this preserves earlier behavior: the first detected harness installs and a dimmed notice lists the others. CI users who want multi-install opt in explicitly with `--all-detected`.
46
+ - **No harnesses detected** — falls back to the default (`claude-code`) so `npx @uluops/setup` always does something useful on a fresh machine.
47
+
48
+ Passing `--harness <name>` always wins — auto-detection is skipped entirely. Experimental harnesses are excluded from auto-detection (an explicit `--harness` is the only way to opt in).
49
+
50
+ ### Multi-harness install
51
+
52
+ `@uluops/setup` is a zero-friction installer for any agentic stack you have. When you've installed multiple harnesses on the same machine, one invocation can wire UluOps into all of them — once-per-run prompts (API key, signup, CLI install) run a single time across the whole batch.
53
+
54
+ ```bash
55
+ # Install into every detected stable harness
56
+ npx @uluops/setup --all-detected
57
+
58
+ # Same thing, alternative form (--all-detected is the canonical name)
59
+ npx @uluops/setup --harness all
60
+
61
+ # Subset
62
+ npx @uluops/setup --harness claude-code,opencode
63
+
64
+ # Aliases work in the comma-separated list
65
+ npx @uluops/setup --harness claude,oc
33
66
  ```
34
67
 
68
+ Each harness gets its own per-section block in the summary:
69
+
70
+ ```
71
+ Setup complete: 3 installed of 3 harnesses
72
+
73
+ ✓ [Claude Code] installed (23 agents · 28 commands · metrics)
74
+ ✓ [OpenCode] installed (23 agents)
75
+ ✓ [Gemini CLI] installed (23 agents · 28 commands · metrics)
76
+
77
+ Restart each of Claude Code, OpenCode, Gemini CLI to load agents.
78
+ ```
79
+
80
+ **Failure isolation:** one harness failing does not abort the others. The failing harness lands as `✗ [<Name>] failed — <reason>` in the summary with a `Re-run: npx @uluops/setup --harness <name>` hint; siblings install cleanly. The process exits 1 when any harness failed operationally, 0 when every harness either succeeded or was declined at the conflict prompt (user choice is not failure).
81
+
82
+ **Partial state:** if a per-harness step throws after MCP succeeded (e.g., a `mkdir` permission error during the agents step), the manifest records what landed plus `partial: "<step>"` naming the failed step. `--verify` surfaces this with a `partial install — failed at "<step>"` warning so you know a re-run is needed.
83
+
35
84
  ## What it does
36
85
 
37
86
  | Artifact | Count | Destination (Claude Code) |
@@ -74,8 +123,16 @@ Setup will first ask whether you're creating a new UluOps account. Pick **Y** to
74
123
  npx @uluops/setup [options]
75
124
 
76
125
  --api-key <key> API key (skip prompt)
77
- --harness <name> Target harness: claude-code, opencode, gemini-cli, codex
126
+ --harness <value> Target harness(es). Single name, comma-separated subset,
127
+ or "all" sentinel:
128
+ --harness claude-code
129
+ --harness claude-code,codex
130
+ --harness all
131
+ Names: claude-code, opencode, gemini-cli, codex
78
132
  Aliases: claude, oc, gemini (default: claude-code)
133
+ --all-detected Install into every detected stable harness. Canonical
134
+ synonym for --harness all. Cannot be combined with
135
+ --harness <single-name> (fail-fast conflict error).
79
136
  --signup Create account from terminal (email + password)
80
137
  --scope <mode> MCP config scope: "global" or "local" (default: global)
81
138
  --local-defs Save definitions to ./uluops/ for review
@@ -85,7 +142,11 @@ npx @uluops/setup [options]
85
142
  --skip-validation Accept API key without server verification
86
143
  --list Show available agents and workflows without installing
87
144
  --verify Check installation health: manifest, files, MCP config, API connectivity (no changes)
88
- --uninstall Remove all UluOps-managed artifacts
145
+ --uninstall Remove UluOps-managed artifacts. Combine with --harness
146
+ <name>[,<name>] to uninstall from a subset only —
147
+ remaining harnesses (and global infrastructure like
148
+ @uluops/cli) are preserved. Plain --uninstall removes
149
+ everything (today's behavior).
89
150
  --dry-run Show what would happen without making changes
90
151
  -y, --yes Skip confirmations
91
152
  ```
@@ -96,7 +157,7 @@ npx @uluops/setup [options]
96
157
  Displays all agents and workflows included in the current version of the setup tool.
97
158
 
98
159
  ```text
99
- ⟨u⟩ ulu·ops v0.6.0 — available agents and workflows
160
+ ⟨u⟩ ulu·ops v0.8.1 — available agents and workflows
100
161
 
101
162
  WORKFLOWS
102
163
  /workflows:post-implementation Iterative validation after coding
@@ -115,7 +176,7 @@ Displays all agents and workflows included in the current version of the setup t
115
176
  Validates your current installation against the local manifest and checks API connectivity.
116
177
 
117
178
  ```text
118
- ⟨u⟩ ulu·ops Installation Check v0.6.0
179
+ ⟨u⟩ ulu·ops Installation Check v0.8.1
119
180
 
120
181
  ✓ Manifest found (~/.uluops/manifest.json)
121
182
  ✓ All 23 agents present in ~/.claude/agents/
@@ -139,9 +200,18 @@ npx @uluops/setup --signup
139
200
  # Install for OpenCode
140
201
  npx @uluops/setup --harness opencode
141
202
 
203
+ # Install into every detected stable harness in one run
204
+ npx @uluops/setup --all-detected
205
+
206
+ # Install into a specific subset
207
+ npx @uluops/setup --harness claude-code,gemini-cli
208
+
142
209
  # Non-interactive (CI/automation)
143
210
  npx @uluops/setup --api-key ulr_abc123 -y
144
211
 
212
+ # CI multi-harness: explicit opt-in to all-detected
213
+ npx @uluops/setup --api-key ulr_abc123 --all-detected -y
214
+
145
215
  # Local MCP config (project-scoped)
146
216
  npx @uluops/setup --scope local
147
217
 
@@ -180,10 +250,22 @@ Setup manages four surfaces: agent files, command files, MCP config entries, and
180
250
  ## Uninstall
181
251
 
182
252
  ```
253
+ # Uninstall everything (every harness in the manifest + globals + shell export)
183
254
  npx @uluops/setup --uninstall
255
+
256
+ # Uninstall from a specific harness only — other harnesses + globals preserved
257
+ npx @uluops/setup --uninstall --harness opencode
258
+
259
+ # Uninstall from a comma-separated subset
260
+ npx @uluops/setup --uninstall --harness opencode,gemini-cli
261
+
262
+ # Same as no filter — uninstall everything
263
+ npx @uluops/setup --uninstall --all-detected
184
264
  ```
185
265
 
186
- Removes only UluOps-managed files: agents, commands, MCP config entries, shell profile export (if `--shell` was used), and the global `@uluops/cli` package (only if this setup installed it — a CLI you installed yourself is left alone). Your custom agents and other MCP servers are preserved. Uninstall iterates all harnesses recorded in the manifest.
266
+ Removes only UluOps-managed files: agents, commands, MCP config entries, shell profile export (if `--shell` was used), and the global `@uluops/cli` package (only if this setup installed it — a CLI you installed yourself is left alone). Your custom agents and other MCP servers are preserved.
267
+
268
+ **Subset uninstall** (`--uninstall --harness <name>`) removes only the named harness(es) from the manifest and disk. Shared infrastructure (the global `@uluops/cli`, `@uluops/agent-metrics`, and the shell-profile export) is left in place because remaining harnesses still need it. The manifest is updated rather than deleted. A subset uninstall that names a harness not in the manifest fails fast with an error listing what IS in the manifest — no silent no-op.
187
269
 
188
270
  ## Requirements
189
271
 
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: uluops-operator
3
+ description: Use when operating inside UluOps from Codex: using UluOps MCP tools or the ulu CLI; rendering or running registry agents, commands, workflows, or pipelines; saving Codex subagent runs to tracker projects; querying, triaging, or closing tracker issues; inspecting analysis records; or deciding how UluOps definitions should map onto Codex skills, agents, and local assets.
4
+ ---
5
+
6
+ # UluOps Operator
7
+
8
+ Use this skill as the operating map for UluOps in Codex. UluOps has three main surfaces:
9
+
10
+ - **Registry**: source of truth for agents, commands, workflows, pipelines, versions, dependencies, and harness-specific rendering.
11
+ - **Tracker**: source of truth for projects, runs, issues, analysis summaries, analysis records, reliability, and lifecycle.
12
+ - **RAH**: statistical and higher-order analysis over registry/tracker state.
13
+
14
+ Prefer MCP tools for tracker/registry state changes already exposed in the session. Prefer the `ulu` CLI for rendering definitions, execution surfaces not exposed by MCP, and machine-readable scripting.
15
+
16
+ ## Tool Choice
17
+
18
+ | Task | Prefer | Notes |
19
+ |---|---|---|
20
+ | Query tracker projects, runs, issues, summaries | `mcp__uluops_tracker` | Use `query_issues`, `search_issues`, `get_project_summary`, run/analysis tools when available. |
21
+ | Update tracker issue status or add notes | `mcp__uluops_tracker` | Use structured status updates with concise resolution reasons. |
22
+ | Save Codex agent/pipeline output to tracker | `mcp__uluops_tracker` | Shape recommendations/issues/analysis records explicitly; preserve custom record types when supported. |
23
+ | Query registry definitions, versions, dependencies | `mcp__uluops_registry` or `ulu` | Use MCP for lightweight lookups; CLI when rendered output or JSON scripting is needed. |
24
+ | Render definitions for Codex | `ulu def get ... --rendered --target codex` | Registry remains source of truth; rendered files are working copies. |
25
+ | Execute UluOps definitions directly | `ulu exec ...` | Use when the user explicitly wants CLI execution rather than Codex subagents. |
26
+ | Analyze cross-run/project patterns | `mcp__rah` | Use for statistical or trend questions when exposed. |
27
+
28
+ If a localhost registry/tracker CLI command fails inside Codex with a connection or sandbox-like error, retry the same command with sandbox escalation before changing environment variables or config.
29
+
30
+ ## Codex Surface Rules
31
+
32
+ Codex does not have Claude-style slash commands as the primary reusable surface.
33
+
34
+ - Use **skills** for stable UluOps workflows that should trigger naturally in Codex.
35
+ - Use rendered **agent TOML** as source material for spawning Codex subagents.
36
+ - Treat rendered commands, workflows, and pipelines as **orchestration instructions**. For Codex, they may need to become skills or be executed stepwise by Codex rather than copied as slash commands.
37
+ - Do not bulk-install every registry definition as a skill. Keep registry as source of truth and render on demand unless a workflow is stable and high-value.
38
+
39
+ Default Codex global paths:
40
+
41
+ ```text
42
+ ~/.codex/config.toml
43
+ ~/.codex/agents/
44
+ ~/.codex/skills/
45
+ ```
46
+
47
+ ## Common Commands
48
+
49
+ Render definitions for Codex:
50
+
51
+ ```bash
52
+ ulu def get agent code-validator --rendered --target codex
53
+ ulu def get workflow ship --rendered --target codex
54
+ ulu def get pipeline foundations --rendered --target codex
55
+ ```
56
+
57
+ Render with a model envelope:
58
+
59
+ ```bash
60
+ ulu def get agent assumption-excavator --rendered --target codex -m gpt
61
+ ```
62
+
63
+ List and inspect definitions:
64
+
65
+ ```bash
66
+ ulu def list --type agent --json
67
+ ulu def get agent code-validator --json
68
+ ulu deps pipeline foundations --json
69
+ ulu versions list agent code-validator --json
70
+ ```
71
+
72
+ Tracker reads via CLI when MCP does not expose the needed surface:
73
+
74
+ ```bash
75
+ ulu projects list --json
76
+ ulu issues list uluops-plans --status open --json
77
+ ulu runs list uluops-plans --json
78
+ ulu analytics burndown --project uluops-plans --days 30 --json
79
+ ```
80
+
81
+ ## Canonical Loop
82
+
83
+ When asked to close the loop on an artifact:
84
+
85
+ 1. Identify the tracker project and artifact path.
86
+ 2. Pull current open issues or run the requested agent/pipeline.
87
+ 3. Inspect the artifact and nearby code/docs before editing.
88
+ 4. Patch the artifact or code with narrow, repo-consistent changes.
89
+ 5. Verify with tests, typecheck, or text audits appropriate to the change.
90
+ 6. Save the run or analysis to tracker when new agent output was produced.
91
+ 7. Mark resolved issues completed with a concrete reason naming the artifact or code path changed.
92
+ 8. Report what changed, what was verified, and any remaining risk.
93
+
94
+ Do not mark an issue completed merely because it was inspected. Complete it only when the artifact/code now addresses the finding. Use `deferred`, `wontfix`, `false-positive`, or `observation` when that status is more accurate.
95
+
96
+ ## Running Registry Agents As Codex Subagents
97
+
98
+ When the user asks to use a UluOps agent through Codex:
99
+
100
+ 1. Render or locate the agent definition for Codex.
101
+ 2. Read only enough of the TOML to understand the role, inputs, expected output, and scoring vocabulary.
102
+ 3. Spawn a Codex subagent using that definition's instructions and the target artifact.
103
+ 4. Ask the subagent for structured output: decision, score, findings/recommendations, analysis records, and evidence.
104
+ 5. Normalize the output before saving to tracker.
105
+
106
+ For large artifacts, tell the subagent whether it read the full artifact or performed a targeted pass. Preserve that note in the tracker run summary.
107
+
108
+ ## Saving Runs To Tracker
109
+
110
+ When saving a Codex subagent result:
111
+
112
+ - Project: use the user-provided tracker project, or infer from local context only when obvious.
113
+ - Workflow type: use the registry definition name or agent name.
114
+ - Agent name: use the UluOps agent identifier, not the subagent nickname.
115
+ - Recommendations/issues: preserve title, severity, category/failure mode, evidence, and actionable remediation.
116
+ - Analysis records: preserve custom `record_type` values; do not force them into a closed enum unless the API requires it.
117
+ - Summary: include artifact path, decision, score, method, caveats, and counts.
118
+
119
+ If the save tool rejects a custom analysis shape, simplify only the rejected field and preserve the semantic content in `content`, `metadata`, or the run summary.
120
+
121
+ ## Issue Triage
122
+
123
+ For top-issue workflows:
124
+
125
+ 1. Query open issues for the project with `priority: "all"` or the requested severity.
126
+ 2. Sort by tracker order unless the user requests a different ordering.
127
+ 3. Investigate each issue against the artifact or code.
128
+ 4. Patch the source of truth, not only the generated tracker text.
129
+ 5. Bulk update statuses only after the fixes are in place.
130
+
131
+ Good resolution reasons are short and concrete:
132
+
133
+ ```text
134
+ Resolved in plans/example.md by adding skipMissing runtime semantics and validation coverage.
135
+ ```
136
+
137
+ ## Registry And Definition Hygiene
138
+
139
+ - Registry definitions are source of truth; rendered Codex assets can drift.
140
+ - Use exact versions when reproducibility matters.
141
+ - Use dependencies to understand pipelines before running them.
142
+ - For Codex, a pipeline may be better represented as a skill recipe than a slash command or a static TOML file.
143
+ - Keep stable, high-value Codex workflows as skills. Keep broad, changing catalogs in the registry.
144
+
145
+ ## Naming Discipline
146
+
147
+ Use canonical project/repo/service names from tracker or registry when available. Do not invent aliases in specs or issue closures. If names disagree across artifacts, fix the source artifact or call out the mismatch explicitly.
148
+
149
+ Current known canonical examples:
150
+
151
+ - Tracker project: `uluops-plans`
152
+ - Tracker API service/repo in specs: `ops-uluops-api`
153
+ - Registry frontend: `ops-uluops-registry`
154
+
155
+ ## Safety
156
+
157
+ - Be conservative with mutating registry operations such as publish, archive, deprecate, fork, or delete.
158
+ - Be conservative with tracker bulk updates; use them when fixes are already made and the status is clear.
159
+ - Never overwrite user edits while installing/rendering local Codex assets. Prefer re-rendering on demand and scoped patches.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Multi-target harness selection (spec §5 behavior matrix).
3
+ *
4
+ * Pure-ish: every external dependency (prompt, info-emitter) is injected
5
+ * so this module is testable without spawning the CLI or mocking inquirer.
6
+ * The cli.ts entry point wires it to real @inquirer/prompts + chalk-styled
7
+ * info() calls; tests pass plain callbacks and assert on the returned
8
+ * harnessNames + the prompt-input received by the callback.
9
+ *
10
+ * Flag conflict detection happens here, not in cli.ts, so the error
11
+ * messaging is uniform and a single test surface covers every CLI
12
+ * combinatoric.
13
+ */
14
+ import type { HarnessProfile } from "../harnesses/index.js";
15
+ export interface HarnessSelectionInput {
16
+ /**
17
+ * Value of the --harness flag as received from commander. May be a single
18
+ * canonical name ('claude-code'), an alias ('claude'), a comma-separated
19
+ * list ('claude-code,codex'), the 'all' sentinel, or the default
20
+ * (e.g. 'claude-code' when no --harness was passed).
21
+ */
22
+ harnessArg: string;
23
+ /**
24
+ * True when commander.getOptionValueSource('harness') === 'cli', i.e.
25
+ * the user actually passed --harness. False when only the default fired.
26
+ */
27
+ harnessFromCli: boolean;
28
+ /** Whether --all-detected was passed. */
29
+ allDetected: boolean;
30
+ /** Result of detectHarnesses() — stable profiles whose home dirs exist. */
31
+ detected: HarnessProfile[];
32
+ /**
33
+ * Fallback harness when zero are detected and the user did not pass an
34
+ * explicit single name. Today: 'claude-code' (the landing-page promise).
35
+ */
36
+ defaultHarness: string;
37
+ /**
38
+ * Whether the run is interactive (TTY + no --yes / --api-key /
39
+ * ULUOPS_API_KEY). The selection branch only prompts when this is true
40
+ * AND multiple harnesses are detected.
41
+ */
42
+ isInteractive: boolean;
43
+ /**
44
+ * Async callback that presents the multi-select checkbox to the user.
45
+ * Receives the detected profiles; returns the chosen subset of harness
46
+ * names. Only called when isInteractive && detected.length > 1 && no
47
+ * explicit --harness / --all-detected was provided. May return an empty
48
+ * array (user unchecked everything) — runSetup handles that case.
49
+ */
50
+ promptCheckbox: (profiles: HarnessProfile[]) => Promise<string[]>;
51
+ /**
52
+ * Optional info-line emitter for the "dimmed notice" branches. No-op when
53
+ * undefined (tests can pass a recorder; cli.ts passes the styled info()).
54
+ */
55
+ emitInfo?: (msg: string) => void;
56
+ }
57
+ export declare class HarnessSelectionError extends Error {
58
+ constructor(message: string);
59
+ }
60
+ /**
61
+ * Parse a --harness value into a list of names (or the 'all' sentinel).
62
+ * Comma-separated parsing splits on `,` and trims whitespace; empty
63
+ * tokens are dropped. 'all' is reserved — a literal harness named 'all'
64
+ * would conflict (no such harness exists today).
65
+ */
66
+ export declare function parseHarnessArg(arg: string): string[] | "all";
67
+ /**
68
+ * Resolve the set of harness names to install into.
69
+ *
70
+ * Behavior matrix mirrors spec §5:
71
+ *
72
+ * | --harness | --all-detected | TTY | detected | result |
73
+ * |---------------|----------------|-----|----------|---------------------------------|
74
+ * | omitted | no | * | 0 | [defaultHarness] |
75
+ * | omitted | no | * | 1 | [detected[0]] |
76
+ * | omitted | no | yes | >1 | promptCheckbox(detected) |
77
+ * | omitted | no | no | >1 | [detected[0]] + dimmed notice |
78
+ * | <name> | no | * | * | [<name>] |
79
+ * | <a>,<b> | no | * | * | [<a>, <b>] |
80
+ * | all | no | * | >0 | detected.map(p => p.name) |
81
+ * | all | no | * | 0 | [defaultHarness] |
82
+ * | omitted | yes | * | >0 | detected.map(p => p.name) |
83
+ * | omitted | yes | * | 0 | [defaultHarness] |
84
+ * | all | yes | * | * | (same as either alone) |
85
+ * | <name> | yes | * | * | ERROR: conflicting flags |
86
+ *
87
+ * Caller is responsible for validating each returned name via getProfile()
88
+ * (typo detection) and for handling the empty-array case (user unchecked
89
+ * everything — runSetup exits cleanly).
90
+ */
91
+ export declare function selectHarnesses(input: HarnessSelectionInput): Promise<string[]>;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Multi-target harness selection (spec §5 behavior matrix).
3
+ *
4
+ * Pure-ish: every external dependency (prompt, info-emitter) is injected
5
+ * so this module is testable without spawning the CLI or mocking inquirer.
6
+ * The cli.ts entry point wires it to real @inquirer/prompts + chalk-styled
7
+ * info() calls; tests pass plain callbacks and assert on the returned
8
+ * harnessNames + the prompt-input received by the callback.
9
+ *
10
+ * Flag conflict detection happens here, not in cli.ts, so the error
11
+ * messaging is uniform and a single test surface covers every CLI
12
+ * combinatoric.
13
+ */
14
+ export class HarnessSelectionError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "HarnessSelectionError";
18
+ }
19
+ }
20
+ /**
21
+ * Parse a --harness value into a list of names (or the 'all' sentinel).
22
+ * Comma-separated parsing splits on `,` and trims whitespace; empty
23
+ * tokens are dropped. 'all' is reserved — a literal harness named 'all'
24
+ * would conflict (no such harness exists today).
25
+ */
26
+ export function parseHarnessArg(arg) {
27
+ const trimmed = arg.trim();
28
+ if (trimmed === "all")
29
+ return "all";
30
+ return trimmed
31
+ .split(",")
32
+ .map((s) => s.trim())
33
+ .filter((s) => s.length > 0);
34
+ }
35
+ /**
36
+ * Resolve the set of harness names to install into.
37
+ *
38
+ * Behavior matrix mirrors spec §5:
39
+ *
40
+ * | --harness | --all-detected | TTY | detected | result |
41
+ * |---------------|----------------|-----|----------|---------------------------------|
42
+ * | omitted | no | * | 0 | [defaultHarness] |
43
+ * | omitted | no | * | 1 | [detected[0]] |
44
+ * | omitted | no | yes | >1 | promptCheckbox(detected) |
45
+ * | omitted | no | no | >1 | [detected[0]] + dimmed notice |
46
+ * | <name> | no | * | * | [<name>] |
47
+ * | <a>,<b> | no | * | * | [<a>, <b>] |
48
+ * | all | no | * | >0 | detected.map(p => p.name) |
49
+ * | all | no | * | 0 | [defaultHarness] |
50
+ * | omitted | yes | * | >0 | detected.map(p => p.name) |
51
+ * | omitted | yes | * | 0 | [defaultHarness] |
52
+ * | all | yes | * | * | (same as either alone) |
53
+ * | <name> | yes | * | * | ERROR: conflicting flags |
54
+ *
55
+ * Caller is responsible for validating each returned name via getProfile()
56
+ * (typo detection) and for handling the empty-array case (user unchecked
57
+ * everything — runSetup exits cleanly).
58
+ */
59
+ export async function selectHarnesses(input) {
60
+ // Flag conflict — explicit --harness <name> + --all-detected is
61
+ // ambiguous and should fail fast before any state is touched. The 'all'
62
+ // sentinel is the one case where the two flags ARE compatible (they mean
63
+ // the same thing) so we accept that combination silently.
64
+ if (input.allDetected &&
65
+ input.harnessFromCli &&
66
+ input.harnessArg.trim() !== "all") {
67
+ throw new HarnessSelectionError(`--harness ${input.harnessArg} conflicts with --all-detected; pick one`);
68
+ }
69
+ // Explicit --harness wins over --all-detected, with one harmonization:
70
+ // --harness all behaves identically to --all-detected.
71
+ if (input.harnessFromCli) {
72
+ const parsed = parseHarnessArg(input.harnessArg);
73
+ if (parsed === "all") {
74
+ return input.detected.length > 0
75
+ ? input.detected.map((p) => p.name)
76
+ : [input.defaultHarness];
77
+ }
78
+ return parsed;
79
+ }
80
+ // --all-detected without --harness
81
+ if (input.allDetected) {
82
+ return input.detected.length > 0
83
+ ? input.detected.map((p) => p.name)
84
+ : [input.defaultHarness];
85
+ }
86
+ // Auto-detection (no explicit flags)
87
+ if (input.detected.length === 0) {
88
+ return [input.defaultHarness];
89
+ }
90
+ if (input.detected.length === 1) {
91
+ const only = input.detected[0];
92
+ if (only.name !== input.defaultHarness && input.emitInfo) {
93
+ input.emitInfo(`Detected ${only.displayName} — using as target (pass --harness to override)`);
94
+ }
95
+ return [only.name];
96
+ }
97
+ // detected.length > 1
98
+ if (input.isInteractive) {
99
+ return await input.promptCheckbox(input.detected);
100
+ }
101
+ // Non-interactive with multiple detected: today's behavior preserved
102
+ // (first detected + dimmed notice). Multi-harness CI users opt in with
103
+ // --all-detected. Spec §10.1.
104
+ if (input.emitInfo) {
105
+ input.emitInfo(`Multiple harnesses detected (${input.detected.map((p) => p.displayName).join(", ")}); defaulting to ${input.detected[0].displayName} — pass --all-detected to install into all`);
106
+ }
107
+ return [input.detected[0].name];
108
+ }