@uluops/setup 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +94 -10
  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 +212 -22
  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 +8 -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,18 +19,69 @@ 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 | Fully supported | — | `~/.codex/config.toml` |
23
+
24
+ > **Codex note:** the TOML writer seeds `approval_mode = "approve"` for every read-side MCP tool exported by `@uluops/ops-mcp` and `@uluops/registry-mcp` so interactive sessions don't surface an approval prompt on every `list_*`/`get_*`/`query_*` call. Write-side tools (`save_run`, `bulk_update_status`, `publish_definition`, etc.) are intentionally NOT pre-approved — Codex still asks before any state-changing operation. A re-install over a hand-tuned config (any `[mcp_servers.<server>.tools.*]` block present) preserves your customizations verbatim and skips the seed step.
23
25
 
24
26
  ```bash
25
27
  # Install for Claude Code (default)
26
28
  npx @uluops/setup
27
29
 
28
- # Install for OpenCode
30
+ # Install for one specific harness
29
31
  npx @uluops/setup --harness opencode
30
-
31
- # Install for Gemini CLI
32
32
  npx @uluops/setup --harness gemini-cli
33
+
34
+ # Install into every detected stable harness in a single run
35
+ npx @uluops/setup --all-detected
36
+
37
+ # Install into a specific subset (comma-separated)
38
+ npx @uluops/setup --harness claude-code,gemini-cli
39
+ ```
40
+
41
+ ### Harness auto-detection
42
+
43
+ If you don't pass `--harness` or `--all-detected`, setup probes your home directory for known harness install markers and picks a target:
44
+
45
+ - **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`).
46
+ - **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.
47
+ - **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`.
48
+ - **No harnesses detected** — falls back to the default (`claude-code`) so `npx @uluops/setup` always does something useful on a fresh machine.
49
+
50
+ Passing `--harness <name>` always wins — auto-detection is skipped entirely. Auto-detection only returns harnesses marked stable; when a future harness ships as experimental, an explicit `--harness <name>` will remain the only way to opt in.
51
+
52
+ ### Multi-harness install
53
+
54
+ `@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.
55
+
56
+ ```bash
57
+ # Install into every detected stable harness
58
+ npx @uluops/setup --all-detected
59
+
60
+ # Same thing, alternative form (--all-detected is the canonical name)
61
+ npx @uluops/setup --harness all
62
+
63
+ # Subset
64
+ npx @uluops/setup --harness claude-code,opencode
65
+
66
+ # Aliases work in the comma-separated list
67
+ npx @uluops/setup --harness claude,oc
68
+ ```
69
+
70
+ Each harness gets its own per-section block in the summary:
71
+
33
72
  ```
73
+ Setup complete: 3 installed of 3 harnesses
74
+
75
+ ✓ [Claude Code] installed (23 agents · 28 commands · metrics)
76
+ ✓ [OpenCode] installed (23 agents)
77
+ ✓ [Gemini CLI] installed (23 agents · 28 commands · metrics)
78
+
79
+ Restart each of Claude Code, OpenCode, Gemini CLI to load agents.
80
+ ```
81
+
82
+ **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).
83
+
84
+ **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.
34
85
 
35
86
  ## What it does
36
87
 
@@ -43,7 +94,7 @@ npx @uluops/setup --harness gemini-cli
43
94
  | Pipeline commands | 2 | `~/.claude/commands/pipelines/` |
44
95
  | Agent metrics hook | 1 | `~/.claude/tools/agent-metrics/` |
45
96
 
46
- > Paths shown are for Claude Code (default). Gemini CLI installs agents as `.md` and commands as `.toml` to `~/.gemini/`. OpenCode installs agents to `~/.config/opencode/agents/`. Agent definitions and commands are transformed to the target harness format at install time from a single source.
97
+ > Paths shown are for Claude Code (default). Gemini CLI installs agents as `.md` and commands as `.toml` to `~/.gemini/`. OpenCode installs agents to `~/.config/opencode/agents/`. Codex installs agents as `.toml` to `~/.codex/agents/` and ships a single `uluops-operator` skill under `~/.codex/skills/` instead of slash commands. Agent definitions and commands are transformed to the target harness format at install time from a single source.
47
98
 
48
99
  The installer runs these steps in sequence:
49
100
 
@@ -74,8 +125,16 @@ Setup will first ask whether you're creating a new UluOps account. Pick **Y** to
74
125
  npx @uluops/setup [options]
75
126
 
76
127
  --api-key <key> API key (skip prompt)
77
- --harness <name> Target harness: claude-code, opencode, gemini-cli, codex
128
+ --harness <value> Target harness(es). Single name, comma-separated subset,
129
+ or "all" sentinel:
130
+ --harness claude-code
131
+ --harness claude-code,codex
132
+ --harness all
133
+ Names: claude-code, opencode, gemini-cli, codex
78
134
  Aliases: claude, oc, gemini (default: claude-code)
135
+ --all-detected Install into every detected stable harness. Canonical
136
+ synonym for --harness all. Cannot be combined with
137
+ --harness <single-name> (fail-fast conflict error).
79
138
  --signup Create account from terminal (email + password)
80
139
  --scope <mode> MCP config scope: "global" or "local" (default: global)
81
140
  --local-defs Save definitions to ./uluops/ for review
@@ -85,7 +144,11 @@ npx @uluops/setup [options]
85
144
  --skip-validation Accept API key without server verification
86
145
  --list Show available agents and workflows without installing
87
146
  --verify Check installation health: manifest, files, MCP config, API connectivity (no changes)
88
- --uninstall Remove all UluOps-managed artifacts
147
+ --uninstall Remove UluOps-managed artifacts. Combine with --harness
148
+ <name>[,<name>] to uninstall from a subset only —
149
+ remaining harnesses (and global infrastructure like
150
+ @uluops/cli) are preserved. Plain --uninstall removes
151
+ everything (today's behavior).
89
152
  --dry-run Show what would happen without making changes
90
153
  -y, --yes Skip confirmations
91
154
  ```
@@ -96,7 +159,7 @@ npx @uluops/setup [options]
96
159
  Displays all agents and workflows included in the current version of the setup tool.
97
160
 
98
161
  ```text
99
- ⟨u⟩ ulu·ops v0.6.0 — available agents and workflows
162
+ ⟨u⟩ ulu·ops v0.8.1 — available agents and workflows
100
163
 
101
164
  WORKFLOWS
102
165
  /workflows:post-implementation Iterative validation after coding
@@ -115,7 +178,7 @@ Displays all agents and workflows included in the current version of the setup t
115
178
  Validates your current installation against the local manifest and checks API connectivity.
116
179
 
117
180
  ```text
118
- ⟨u⟩ ulu·ops Installation Check v0.6.0
181
+ ⟨u⟩ ulu·ops Installation Check v0.8.1
119
182
 
120
183
  ✓ Manifest found (~/.uluops/manifest.json)
121
184
  ✓ All 23 agents present in ~/.claude/agents/
@@ -139,9 +202,18 @@ npx @uluops/setup --signup
139
202
  # Install for OpenCode
140
203
  npx @uluops/setup --harness opencode
141
204
 
205
+ # Install into every detected stable harness in one run
206
+ npx @uluops/setup --all-detected
207
+
208
+ # Install into a specific subset
209
+ npx @uluops/setup --harness claude-code,gemini-cli
210
+
142
211
  # Non-interactive (CI/automation)
143
212
  npx @uluops/setup --api-key ulr_abc123 -y
144
213
 
214
+ # CI multi-harness: explicit opt-in to all-detected
215
+ npx @uluops/setup --api-key ulr_abc123 --all-detected -y
216
+
145
217
  # Local MCP config (project-scoped)
146
218
  npx @uluops/setup --scope local
147
219
 
@@ -180,10 +252,22 @@ Setup manages four surfaces: agent files, command files, MCP config entries, and
180
252
  ## Uninstall
181
253
 
182
254
  ```
255
+ # Uninstall everything (every harness in the manifest + globals + shell export)
183
256
  npx @uluops/setup --uninstall
257
+
258
+ # Uninstall from a specific harness only — other harnesses + globals preserved
259
+ npx @uluops/setup --uninstall --harness opencode
260
+
261
+ # Uninstall from a comma-separated subset
262
+ npx @uluops/setup --uninstall --harness opencode,gemini-cli
263
+
264
+ # Same as no filter — uninstall everything
265
+ npx @uluops/setup --uninstall --all-detected
184
266
  ```
185
267
 
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.
268
+ 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.
269
+
270
+ **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
271
 
188
272
  ## Requirements
189
273
 
@@ -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
+ }