@uluops/setup 0.11.0 → 0.13.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/CHANGELOG.md +805 -0
- package/README.md +81 -32
- package/dist/cli.js +7 -1
- package/dist/commands/helpers.js +70 -7
- package/dist/commands/per-harness.d.ts +5 -0
- package/dist/commands/per-harness.js +5 -0
- package/dist/commands/setup.d.ts +7 -0
- package/dist/commands/setup.js +100 -35
- package/dist/commands/uninstall.d.ts +7 -0
- package/dist/commands/uninstall.js +36 -9
- package/dist/commands/verify.d.ts +5 -0
- package/dist/commands/verify.js +5 -0
- package/dist/harnesses/claude-code.js +15 -7
- package/dist/harnesses/codex.js +35 -8
- package/dist/harnesses/gemini-cli.js +13 -6
- package/dist/harnesses/index.d.ts +8 -0
- package/dist/harnesses/index.js +10 -0
- package/dist/harnesses/opencode.js +25 -7
- package/dist/lib/asset-catalog.js +15 -2
- package/dist/lib/atomic-write.d.ts +6 -0
- package/dist/lib/atomic-write.js +10 -0
- package/dist/lib/config-merger.js +27 -8
- package/dist/lib/display.d.ts +8 -0
- package/dist/lib/display.js +27 -1
- package/dist/lib/file-ops.d.ts +13 -4
- package/dist/lib/file-ops.js +69 -18
- package/dist/lib/install-lock.js +45 -13
- package/dist/lib/json-guards.d.ts +15 -0
- package/dist/lib/json-guards.js +30 -0
- package/dist/lib/manifest.d.ts +9 -2
- package/dist/lib/manifest.js +66 -8
- package/dist/lib/mcp-packages.d.ts +17 -15
- package/dist/lib/mcp-packages.js +15 -13
- package/dist/lib/settings-merger.js +53 -9
- package/dist/lib/version.js +19 -2
- package/dist/lib/write-coordinator.d.ts +50 -0
- package/dist/lib/write-coordinator.js +89 -0
- package/dist/steps/agent-metrics-cli.d.ts +6 -0
- package/dist/steps/agent-metrics-cli.js +19 -1
- package/dist/steps/agents.js +22 -4
- package/dist/steps/auth.js +53 -13
- package/dist/steps/cli.js +14 -1
- package/dist/steps/commands.js +28 -10
- package/dist/steps/mcp.js +18 -9
- package/dist/steps/metrics.js +77 -7
- package/dist/steps/shell.d.ts +4 -1
- package/dist/steps/shell.js +44 -6
- package/dist/steps/signup.d.ts +4 -0
- package/dist/steps/signup.js +18 -2
- package/dist/steps/skills.d.ts +11 -0
- package/dist/steps/skills.js +35 -6
- package/dist/steps/username.js +10 -2
- package/dist/steps/verify.js +55 -6
- package/package.json +7 -4
- package/dist/lib/agent-transform.d.ts +0 -12
- package/dist/lib/agent-transform.js +0 -129
package/README.md
CHANGED
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
Zero-friction installer for [UluOps](https://uluops.ai) agentic harnesses. One command sets up MCP servers, agents, and slash commands for Claude Code, OpenCode, and more.
|
|
8
8
|
|
|
9
|
-
```
|
|
9
|
+
```bash
|
|
10
10
|
npx @uluops/setup
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
> Requires **Node.js >= 20** ([full requirements](#requirements)).
|
|
14
|
+
|
|
13
15
|
> **⚠️ Windows Users:** Native Windows is not yet supported. Please use **WSL2 (Ubuntu)** and run the setup inside your WSL environment.
|
|
14
16
|
|
|
17
|
+
**Contents:** [Supported harnesses](#supported-harnesses) · [What it does](#what-it-does) · [Usage](#usage) · [Options](#options) · [Advanced commands](#advanced-commands) · [Examples](#examples) · [How updates work](#how-updates-work) · [Troubleshooting](#troubleshooting) · [Uninstall](#uninstall) · [Requirements](#requirements)
|
|
18
|
+
|
|
15
19
|
## Supported harnesses
|
|
16
20
|
|
|
17
21
|
| Harness | Status | Alias | Config |
|
|
@@ -43,7 +47,14 @@ npx @uluops/setup --harness claude-code,gemini-cli
|
|
|
43
47
|
If you don't pass `--harness` or `--all-detected`, setup probes your home directory for known harness install markers and picks a target:
|
|
44
48
|
|
|
45
49
|
- **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
|
|
50
|
+
- **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:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
? Multiple harnesses detected. Which would you like to install into?
|
|
54
|
+
❯ ◉ Claude Code
|
|
55
|
+
◉ OpenCode
|
|
56
|
+
◉ Gemini CLI
|
|
57
|
+
```
|
|
47
58
|
- **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
59
|
- **No harnesses detected** — falls back to the default (`claude-code`) so `npx @uluops/setup` always does something useful on a fresh machine.
|
|
49
60
|
|
|
@@ -69,7 +80,7 @@ npx @uluops/setup --harness claude,oc
|
|
|
69
80
|
|
|
70
81
|
Each harness gets its own per-section block in the summary:
|
|
71
82
|
|
|
72
|
-
```
|
|
83
|
+
```text
|
|
73
84
|
Setup complete: 3 installed of 3 harnesses
|
|
74
85
|
|
|
75
86
|
✓ [Claude Code] installed (23 agents · 28 commands · metrics)
|
|
@@ -99,17 +110,18 @@ Each harness gets its own per-section block in the summary:
|
|
|
99
110
|
The installer runs these steps in sequence:
|
|
100
111
|
|
|
101
112
|
1. **Authenticate** — Asks whether you're creating a new account. New users sign up with email + password; returning users paste an API key. Skip the question with `--api-key`, `--signup`, `--yes`, or `ULUOPS_API_KEY`.
|
|
102
|
-
2. **
|
|
103
|
-
3. **
|
|
104
|
-
4. **
|
|
105
|
-
5.
|
|
106
|
-
6.
|
|
113
|
+
2. **Registry username** *(optional)* — Offers to set your registry username, the one-time prerequisite for creating or publishing definitions. Never forced: consumers who only run definitions don't need one, and non-interactive runs skip it silently unless `--username <name>` is supplied.
|
|
114
|
+
3. **MCP config** — Writes tracker and registry server entries to the harness config
|
|
115
|
+
4. **Definitions** — Copies pre-rendered agent definition files
|
|
116
|
+
5. **Metrics hook** — Configures a post-agent hook for automatic run capture (Claude Code and Gemini CLI)
|
|
117
|
+
6. **`ulu` CLI** *(optional)* — Offers to install `@uluops/cli` globally. Interactive runs are prompted (default Y); non-interactive runs skip unless `--with-cli` is passed. `--no-cli` always skips. The install is best-effort: if `npm install -g` fails (permissions, nvm prefix, etc.) the rest of setup still completes and a manual install command is printed.
|
|
118
|
+
7. **Health check** — Verifies both API endpoints are reachable
|
|
107
119
|
|
|
108
120
|
> When this setup installs the CLI, the install is recorded in the manifest so `--uninstall` removes it symmetrically. If the CLI was already on your PATH before running setup, it is left alone on uninstall.
|
|
109
121
|
|
|
110
122
|
## Usage
|
|
111
123
|
|
|
112
|
-
```
|
|
124
|
+
```bash
|
|
113
125
|
npx @uluops/setup
|
|
114
126
|
```
|
|
115
127
|
|
|
@@ -121,7 +133,7 @@ Setup will first ask whether you're creating a new UluOps account. Pick **Y** to
|
|
|
121
133
|
|
|
122
134
|
### Options
|
|
123
135
|
|
|
124
|
-
```
|
|
136
|
+
```text
|
|
125
137
|
npx @uluops/setup [options]
|
|
126
138
|
|
|
127
139
|
--api-key <key> API key (skip prompt)
|
|
@@ -136,6 +148,11 @@ npx @uluops/setup [options]
|
|
|
136
148
|
synonym for --harness all. Cannot be combined with
|
|
137
149
|
--harness <single-name> (fail-fast conflict error).
|
|
138
150
|
--signup Create account from terminal (email + password)
|
|
151
|
+
--username <name> Set your registry username without prompting (lowercase
|
|
152
|
+
slug, e.g. ulu-labs). One-time prerequisite for
|
|
153
|
+
creating/publishing definitions — optional if you only
|
|
154
|
+
run them. Non-interactive runs skip the username step
|
|
155
|
+
entirely unless this flag is passed.
|
|
139
156
|
--scope <mode> MCP config scope: "global" or "local" (default: global)
|
|
140
157
|
--local-defs Install definitions into ./uluops/ (project-scoped)
|
|
141
158
|
instead of the harness's home directory
|
|
@@ -167,36 +184,43 @@ npx @uluops/setup [options]
|
|
|
167
184
|
Displays all agents and workflows included in the current version of the setup tool.
|
|
168
185
|
|
|
169
186
|
```text
|
|
170
|
-
⟨u⟩ ulu·ops v0.
|
|
187
|
+
⟨u⟩ ulu·ops v0.12.0 — available agents and workflows
|
|
171
188
|
|
|
172
189
|
WORKFLOWS
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
AGENTS (run individually)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
/workflows:post-implementation Iterative validation workflow. Run af...
|
|
191
|
+
/workflows:pre-implementation Validates proposed design and archite...
|
|
192
|
+
/workflows:prompt-audit Comprehensive prompt audit with ecosy...
|
|
193
|
+
|
|
194
|
+
AGENTS (run individually) MODEL
|
|
195
|
+
/agents:anxiety-reader Reads from the pos... sonnet
|
|
196
|
+
/agents:architect Run Pre-Implementa... sonnet
|
|
197
|
+
/agents:assumption-excavator Surfaces implicit ... sonnet
|
|
198
|
+
/agents:audit Deep runtime corre... sonnet
|
|
199
|
+
/agents:docs-validate Validates comprehe... sonnet
|
|
200
|
+
/agents:security Comprehensive secu... sonnet
|
|
201
|
+
...
|
|
183
202
|
```
|
|
184
203
|
|
|
185
204
|
#### Check installation health (`--verify`)
|
|
186
205
|
Validates your current installation against the local manifest and checks API connectivity.
|
|
187
206
|
|
|
188
207
|
```text
|
|
189
|
-
⟨u⟩ ulu·ops Installation Check v0.
|
|
208
|
+
⟨u⟩ ulu·ops Installation Check v0.12.0
|
|
190
209
|
|
|
191
|
-
✓ Manifest found (
|
|
192
|
-
✓
|
|
193
|
-
✓ MCP
|
|
194
|
-
✓
|
|
195
|
-
✓
|
|
210
|
+
✓ Manifest found (v0.12.0, installed 2026-08-21)
|
|
211
|
+
✓ [Claude Code] Readiness
|
|
212
|
+
✓ [Claude Code] MCP config present in ~/.claude.json (2 servers)
|
|
213
|
+
✓ [Claude Code] 23/23 agents in ~/.claude/agents
|
|
214
|
+
✓ [Claude Code] 28/28 commands
|
|
215
|
+
✓ [Claude Code] Agent metrics hook configured
|
|
216
|
+
✓ API key valid
|
|
217
|
+
✓ MCP packages resolvable on npm
|
|
196
218
|
|
|
197
219
|
All checks passed.
|
|
198
220
|
```
|
|
199
221
|
|
|
222
|
+
> With multiple harnesses installed, `--verify` prints one `[<Harness>]` block per manifest entry.
|
|
223
|
+
|
|
200
224
|
### Examples
|
|
201
225
|
|
|
202
226
|
```bash
|
|
@@ -207,6 +231,9 @@ npx @uluops/setup
|
|
|
207
231
|
# Skip the account question and go straight to signup
|
|
208
232
|
npx @uluops/setup --signup
|
|
209
233
|
|
|
234
|
+
# Set your registry username during setup (required to publish definitions)
|
|
235
|
+
npx @uluops/setup --username ulu-labs
|
|
236
|
+
|
|
210
237
|
# Install for OpenCode
|
|
211
238
|
npx @uluops/setup --harness opencode
|
|
212
239
|
|
|
@@ -246,12 +273,17 @@ npx @uluops/setup --no-agent-metrics-cli
|
|
|
246
273
|
|
|
247
274
|
## How updates work
|
|
248
275
|
|
|
249
|
-
Re-running `npx @uluops/setup` is safe
|
|
276
|
+
Re-running `npx @uluops/setup` is designed to be safe to repeat:
|
|
250
277
|
|
|
251
278
|
- Unchanged files are skipped (content hash comparison)
|
|
252
279
|
- Updated files are overwritten
|
|
253
|
-
-
|
|
254
|
-
-
|
|
280
|
+
- Definitions no longer shipped are cleaned up
|
|
281
|
+
- Custom agents and non-UluOps MCP servers are left alone, with two known
|
|
282
|
+
edges: a hook whose command *contains* the UluOps ownership marker (e.g. a
|
|
283
|
+
hand-forked copy of our agent-metrics hook) is treated as ours and replaced
|
|
284
|
+
on re-run, and switching `--local-defs` between runs starts a fresh tree —
|
|
285
|
+
the previous scope's files stay on disk untracked (setup warns when this
|
|
286
|
+
happens)
|
|
255
287
|
|
|
256
288
|
Setup manages four surfaces: agent files, command files, MCP config entries, and the metrics hook. A manifest at `~/.uluops/manifest.json` tracks what was installed so `--uninstall` can cleanly reverse all changes. The manifest supports multiple harnesses — each gets its own installation state.
|
|
257
289
|
|
|
@@ -259,13 +291,13 @@ Setup manages four surfaces: agent files, command files, MCP config entries, and
|
|
|
259
291
|
|
|
260
292
|
- **Agents not appearing:** Ensure you have restarted your harness (Claude Code, etc.) after running setup. For Claude Code, simply exit and restart the CLI.
|
|
261
293
|
- **MCP errors:** If the harness fails to start the MCP servers, ensure `npx` is available in your PATH. You can check your config at `~/.claude.json` or `~/.config/opencode/opencode.json`.
|
|
262
|
-
- **API key rejected:** Verify your key at [app.uluops.ai](https://app.uluops.ai).
|
|
294
|
+
- **API key rejected:** Verify your key at [app.uluops.ai](https://app.uluops.ai). Behind a corporate proxy, note that setup's own API calls do **not** honor `HTTPS_PROXY` (Node's fetch ignores proxy env vars) — use `--skip-validation` to complete setup offline and verify the key later from a network that can reach `api.uluops.ai`.
|
|
263
295
|
- **`@uluops/cli` install warning:** If setup warns it could not install the CLI globally (EACCES, nvm prefix mismatch, network), the rest of setup still completes. Run `npm install -g @uluops/cli` yourself when convenient — once it's on your PATH, every subsequent `npx @uluops/setup` will see it and skip the install step.
|
|
264
296
|
- **Windows issues:** Remember that native Windows is not supported; you must run the installer and your harness within **WSL2**.
|
|
265
297
|
|
|
266
298
|
## Uninstall
|
|
267
299
|
|
|
268
|
-
```
|
|
300
|
+
```bash
|
|
269
301
|
# Uninstall everything (every harness in the manifest + globals + shell export)
|
|
270
302
|
npx @uluops/setup --uninstall
|
|
271
303
|
|
|
@@ -283,6 +315,23 @@ Removes only UluOps-managed files: agents, commands, MCP config entries, shell p
|
|
|
283
315
|
|
|
284
316
|
**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.
|
|
285
317
|
|
|
318
|
+
## Data & privacy
|
|
319
|
+
|
|
320
|
+
The metrics hook (step 5) captures **agent execution metadata only** — token
|
|
321
|
+
counts, durations, model and agent names — into a **local buffer** on your
|
|
322
|
+
machine. The hook itself sends nothing anywhere: data reaches the UluOps
|
|
323
|
+
tracker only when a run is explicitly saved (by you, or by tooling you run).
|
|
324
|
+
Artifact content being validated is never stored — only validation results.
|
|
325
|
+
|
|
326
|
+
Validation run and issue data saved to the tracker is **retained
|
|
327
|
+
indefinitely by design** (the immutable forensics model); account and data
|
|
328
|
+
deletion is available on request. Full details:
|
|
329
|
+
[uluops.ai/privacy](https://uluops.ai/privacy).
|
|
330
|
+
|
|
331
|
+
Opt-outs: `--no-metrics` skips the hook install entirely; `--uninstall`
|
|
332
|
+
removes it later. If you're installing inside an organization, check your
|
|
333
|
+
org's telemetry policy before enabling the hook on shared projects.
|
|
334
|
+
|
|
286
335
|
## Requirements
|
|
287
336
|
|
|
288
337
|
- **Node.js:** >= 20.0.0
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from "commander";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { info, printAgentList } from "./lib/display.js";
|
|
5
5
|
import { getVersion } from "./lib/version.js";
|
|
6
|
-
import { listHarnesses, detectHarnesses, getProfile, HarnessNotTestedError, } from "./harnesses/index.js";
|
|
6
|
+
import { listHarnesses, detectHarnesses, detectExcludedExperimental, getProfile, HarnessNotTestedError, } from "./harnesses/index.js";
|
|
7
7
|
import { InstallLockHeldError } from "./lib/install-lock.js";
|
|
8
8
|
import { runSetup } from "./commands/setup.js";
|
|
9
9
|
import { runUninstall } from "./commands/uninstall.js";
|
|
@@ -73,6 +73,12 @@ async function main() {
|
|
|
73
73
|
// matrix of (--harness, --all-detected, detection count, TTY) lives in
|
|
74
74
|
// one tested place. cli.ts only wires the prompt and emit-info callbacks.
|
|
75
75
|
const detected = detectHarnesses();
|
|
76
|
+
// Name the exclusion: auto-detection only returns stable profiles, and a
|
|
77
|
+
// user who can see an experimental harness installed reads silence as a
|
|
78
|
+
// detection bug rather than a policy.
|
|
79
|
+
for (const p of detectExcludedExperimental()) {
|
|
80
|
+
console.log(chalk.dim(` Detected ${p.displayName} (experimental) — excluded from auto-detection; opt in with --harness ${p.name}`));
|
|
81
|
+
}
|
|
76
82
|
const isInteractive = !opts.yes &&
|
|
77
83
|
!opts.apiKey &&
|
|
78
84
|
!process.env["ULUOPS_API_KEY"] &&
|
package/dist/commands/helpers.js
CHANGED
|
@@ -14,6 +14,7 @@ import { installAgentMetricsCli, AGENT_METRICS_PACKAGE, AGENT_METRICS_BIN, } fro
|
|
|
14
14
|
import { writeShellExport } from "../steps/shell.js";
|
|
15
15
|
import { probeHookSupport } from "../lib/settings-merger.js";
|
|
16
16
|
import { findProjectRoot, ASSETS_DIR } from "../lib/paths.js";
|
|
17
|
+
import { isEnoent } from "../lib/file-ops.js";
|
|
17
18
|
import { getHealthTimeout } from "../lib/health.js";
|
|
18
19
|
import { ok, warn, fail, info } from "../lib/display.js";
|
|
19
20
|
import { ConflictRejectedError } from "./errors.js";
|
|
@@ -74,7 +75,14 @@ export async function initContext(opts) {
|
|
|
74
75
|
ok(`API key generated`);
|
|
75
76
|
}
|
|
76
77
|
else {
|
|
77
|
-
|
|
78
|
+
// Non-TTY must fall through to resolveApiKey's no-key error (which
|
|
79
|
+
// names --api-key / ULUOPS_API_KEY) — prompting against a closed stdin
|
|
80
|
+
// dies on inquirer's raw cancellation instead. Mirrors the isTTY guard
|
|
81
|
+
// in shouldPromptForAccount.
|
|
82
|
+
const interactive = !opts.yes &&
|
|
83
|
+
!opts.apiKey &&
|
|
84
|
+
!process.env["ULUOPS_API_KEY"] &&
|
|
85
|
+
Boolean(process.stdin.isTTY);
|
|
78
86
|
const auth = await resolveApiKey({
|
|
79
87
|
apiKeyFlag: opts.apiKey,
|
|
80
88
|
skipValidation: opts.skipValidation,
|
|
@@ -216,7 +224,14 @@ export async function configureMetricsStep(profile, opts) {
|
|
|
216
224
|
}
|
|
217
225
|
if (!profile.hooks) {
|
|
218
226
|
info(chalk.dim(`Metrics hooks not supported for ${profile.displayName}`));
|
|
219
|
-
|
|
227
|
+
// skippedReason for shape parity with installMetrics' own hookless
|
|
228
|
+
// branch — metricsObserved must read this run as non-observing.
|
|
229
|
+
return {
|
|
230
|
+
toolFilesCopied: 0,
|
|
231
|
+
hookConfigured: false,
|
|
232
|
+
hooksInstalledVersion: null,
|
|
233
|
+
skippedReason: "no-hook-support",
|
|
234
|
+
};
|
|
220
235
|
}
|
|
221
236
|
const probe = probeHookSupport();
|
|
222
237
|
if (probe.warning)
|
|
@@ -229,6 +244,15 @@ export async function configureMetricsStep(profile, opts) {
|
|
|
229
244
|
parts.push("hook configured");
|
|
230
245
|
const toolPath = profile.paths.toolsDir?.replace(process.env["HOME"] ?? "", "~");
|
|
231
246
|
ok(`Agent metrics → ${toolPath}/ (${parts.join(", ")})`);
|
|
247
|
+
// Disclosure, not decoration: the hook captures execution metadata to a
|
|
248
|
+
// LOCAL buffer and sends nothing itself — say so where it's installed.
|
|
249
|
+
info(chalk.dim(" Captures agent token/duration metadata to a local buffer (nothing is sent).\n" +
|
|
250
|
+
" Skip with --no-metrics · uluops.ai/privacy"));
|
|
251
|
+
}
|
|
252
|
+
else if (res.skippedReason === "hook-state-unknown") {
|
|
253
|
+
// installMetrics already warned with the settings path and the
|
|
254
|
+
// keeping-prior-record note — do not follow it with a message that
|
|
255
|
+
// misnames the cause (tool files may well have copied).
|
|
232
256
|
}
|
|
233
257
|
else {
|
|
234
258
|
warn("Agent metrics hook not configured (tool files not found)");
|
|
@@ -369,10 +393,18 @@ export async function runHealthCheck(opts) {
|
|
|
369
393
|
checkEndpoint("https://api.uluops.ai/api/v1/health"),
|
|
370
394
|
checkEndpoint("https://api.uluops.ai/api/v1/registry/health"),
|
|
371
395
|
]);
|
|
372
|
-
if (trackerOk && registryOk)
|
|
396
|
+
if (trackerOk && registryOk) {
|
|
373
397
|
ok("Health check passed — both APIs reachable");
|
|
374
|
-
|
|
375
|
-
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
// Name the failing endpoint — "some APIs" gives the user nothing to
|
|
401
|
+
// report or retry against.
|
|
402
|
+
const down = [
|
|
403
|
+
!trackerOk && "Tracker",
|
|
404
|
+
!registryOk && "Registry",
|
|
405
|
+
].filter(Boolean);
|
|
406
|
+
warn(`${down.join(" and ")} API unreachable (MCP tools may have limited functionality)`);
|
|
407
|
+
}
|
|
376
408
|
}
|
|
377
409
|
catch {
|
|
378
410
|
warn("Health check skipped (network issue)");
|
|
@@ -418,12 +450,43 @@ export async function checkConflicts(profile, localDefs) {
|
|
|
418
450
|
: profile.paths.agentsDir;
|
|
419
451
|
const srcDir = join(ASSETS_DIR, profile.name, "agents");
|
|
420
452
|
let existingFiles;
|
|
421
|
-
let assetFiles;
|
|
422
453
|
try {
|
|
423
454
|
existingFiles = await readdir(destDir);
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
if (isEnoent(err)) {
|
|
458
|
+
return; // No destination dir yet — fresh install, nothing to conflict.
|
|
459
|
+
}
|
|
460
|
+
// Unreadable destination = conflicts UNKNOWN, never "no conflicts":
|
|
461
|
+
// proceeding silently overwrites files we could not enumerate. Ask.
|
|
462
|
+
warn(`Could not read ${destDir} (${err instanceof Error ? err.message : String(err)}) — cannot check for existing agents that would be overwritten.`);
|
|
463
|
+
if (!process.stdin.isTTY) {
|
|
464
|
+
// Non-TTY can't answer the prompt; fail-safe is refusal, not a hang
|
|
465
|
+
// and not a silent overwrite. This is an OPERATIONAL failure (EACCES
|
|
466
|
+
// class), not a user policy choice — it must exit 1 for CI, so a
|
|
467
|
+
// plain Error (failed path), not ConflictRejectedError (exit 0).
|
|
468
|
+
// (--yes skips checkConflicts entirely.)
|
|
469
|
+
throw new Error(`Cannot verify conflicts in ${destDir} and no TTY to ask — refusing to risk overwriting existing files. Fix the directory permissions or pass --yes to proceed without the check.`);
|
|
470
|
+
}
|
|
471
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
472
|
+
const proceed = await confirm({
|
|
473
|
+
message: "Continue anyway (existing files may be overwritten)?",
|
|
474
|
+
default: false,
|
|
475
|
+
});
|
|
476
|
+
if (!proceed) {
|
|
477
|
+
throw new ConflictRejectedError(profile.name);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
let assetFiles;
|
|
482
|
+
try {
|
|
424
483
|
assetFiles = await readdir(srcDir);
|
|
425
484
|
}
|
|
426
|
-
catch {
|
|
485
|
+
catch (err) {
|
|
486
|
+
// The BUNDLED assets being unreadable is not a fresh-install condition —
|
|
487
|
+
// it means the package itself is broken. Don't silently skip the
|
|
488
|
+
// conflict check; say so (the copy step will surface the hard failure).
|
|
489
|
+
warn(`Could not read bundled agent assets (${err instanceof Error ? err.message : String(err)}) — conflict check skipped`);
|
|
427
490
|
return;
|
|
428
491
|
}
|
|
429
492
|
const conflicts = assetFiles.filter((f) => existingFiles.includes(f));
|
|
@@ -50,6 +50,11 @@ export interface PerHarnessResult {
|
|
|
50
50
|
* | Any declined AND zero failed | 0 |
|
|
51
51
|
* | Empty (user unchecked all, or no harnesses to run) | 0 |
|
|
52
52
|
*
|
|
53
|
+
* The implementation is deliberately a single `anyFailed` check, not four
|
|
54
|
+
* branches: rows 1, 3, and 4 all share exit 0, so the table collapses to
|
|
55
|
+
* "any operational failure → 1, everything else → 0". The table is the
|
|
56
|
+
* spec; the code is its minimal form.
|
|
57
|
+
*
|
|
53
58
|
* Rationale: CI wrapping `--harness all` should not be poisoned by
|
|
54
59
|
* user-policy choices (declines, no-op outcomes) but MUST fail on
|
|
55
60
|
* operational errors (EACCES, ENOSPC, parse-error) so deploy pipelines
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* | Any declined AND zero failed | 0 |
|
|
19
19
|
* | Empty (user unchecked all, or no harnesses to run) | 0 |
|
|
20
20
|
*
|
|
21
|
+
* The implementation is deliberately a single `anyFailed` check, not four
|
|
22
|
+
* branches: rows 1, 3, and 4 all share exit 0, so the table collapses to
|
|
23
|
+
* "any operational failure → 1, everything else → 0". The table is the
|
|
24
|
+
* spec; the code is its minimal form.
|
|
25
|
+
*
|
|
21
26
|
* Rationale: CI wrapping `--harness all` should not be poisoned by
|
|
22
27
|
* user-policy choices (declines, no-op outcomes) but MUST fail on
|
|
23
28
|
* operational errors (EACCES, ENOSPC, parse-error) so deploy pipelines
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -22,4 +22,11 @@ interface RunSetupOpts {
|
|
|
22
22
|
/** Explicit registry username (slug). Set + confirmed non-interactively when provided. */
|
|
23
23
|
username?: string;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The main install flow: resolves every target harness up front (fail-fast on
|
|
27
|
+
* typos), runs the once-per-run steps (auth, username, CLI prompts) a single
|
|
28
|
+
* time, then installs MCP config, definitions, and the metrics hook per
|
|
29
|
+
* harness with failure isolation — one harness failing does not abort the
|
|
30
|
+
* others. Exits 1 if any harness failed operationally.
|
|
31
|
+
*/
|
|
25
32
|
export declare function runSetup(opts: RunSetupOpts): Promise<void>;
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,7 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadManifest, saveManifest, } from "../lib/manifest.js";
|
|
4
4
|
import { findProjectRoot } from "../lib/paths.js";
|
|
5
|
-
import { info, printSetupSummary,
|
|
5
|
+
import { info, warn, blank, printSetupBanner, printHarnessHeader, printSetupSummary, } from "../lib/display.js";
|
|
6
6
|
import { getVersion } from "../lib/version.js";
|
|
7
7
|
import { getProfile } from "../harnesses/index.js";
|
|
8
8
|
import { acquireInstallLock, } from "../lib/install-lock.js";
|
|
@@ -10,6 +10,13 @@ import { initContext, checkConflicts, configureMcpStep, installAgentsDefs, insta
|
|
|
10
10
|
import { ConflictRejectedError } from "./errors.js";
|
|
11
11
|
import { classifyExit, } from "./per-harness.js";
|
|
12
12
|
import { maybeSetUsername } from "../steps/username.js";
|
|
13
|
+
/**
|
|
14
|
+
* The main install flow: resolves every target harness up front (fail-fast on
|
|
15
|
+
* typos), runs the once-per-run steps (auth, username, CLI prompts) a single
|
|
16
|
+
* time, then installs MCP config, definitions, and the metrics hook per
|
|
17
|
+
* harness with failure isolation — one harness failing does not abort the
|
|
18
|
+
* others. Exits 1 if any harness failed operationally.
|
|
19
|
+
*/
|
|
13
20
|
export async function runSetup(opts) {
|
|
14
21
|
if (opts.harnesses.length === 0) {
|
|
15
22
|
info(chalk.dim("Nothing to install — re-run with at least one harness selected.\n"));
|
|
@@ -20,21 +27,16 @@ export async function runSetup(opts) {
|
|
|
20
27
|
// is touched. getProfile throws HarnessNotTestedError or a friendly
|
|
21
28
|
// unknown-name error; the top-level catch in cli.ts surfaces them.
|
|
22
29
|
const profiles = opts.harnesses.map((name) => getProfile(name));
|
|
23
|
-
console.log();
|
|
24
|
-
console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")}`);
|
|
25
|
-
console.log(` ${chalk.dim("operating intelligence as infrastructure")}`);
|
|
26
|
-
console.log();
|
|
27
30
|
const targetSummary = profiles.length === 1
|
|
28
31
|
? profiles[0].displayName
|
|
29
32
|
: `${profiles.length} harnesses (${profiles.map((p) => p.displayName).join(", ")})`;
|
|
30
|
-
|
|
31
|
-
console.log();
|
|
33
|
+
printSetupBanner(version, targetSummary);
|
|
32
34
|
if (opts.dryRun) {
|
|
33
35
|
info(chalk.dim("(dry run — no changes will be made)\n"));
|
|
34
36
|
}
|
|
35
37
|
// === Once-per-run: BEFORE the per-harness loop ===
|
|
36
38
|
const { env, apiKey } = await initContext(opts);
|
|
37
|
-
|
|
39
|
+
blank();
|
|
38
40
|
// Optional, never-forced: offer to set a registry username (the one-time
|
|
39
41
|
// prerequisite for creating/publishing definitions). Skipped silently in
|
|
40
42
|
// non-interactive runs unless --username is supplied.
|
|
@@ -45,7 +47,7 @@ export async function runSetup(opts) {
|
|
|
45
47
|
dryRun: opts.dryRun,
|
|
46
48
|
emit: (msg) => info(msg),
|
|
47
49
|
});
|
|
48
|
-
|
|
50
|
+
blank();
|
|
49
51
|
// Acquire the install lock before touching any shared state. Skipped on
|
|
50
52
|
// dry-run (read-only). The lock excludes a second concurrent uluops-setup
|
|
51
53
|
// from racing the manifest / MCP config / shell-profile / settings.json
|
|
@@ -53,6 +55,7 @@ export async function runSetup(opts) {
|
|
|
53
55
|
// concurrent multi-harness installs from separate processes serialize
|
|
54
56
|
// (spec §10.6).
|
|
55
57
|
let lock = null;
|
|
58
|
+
let exitCode = 0;
|
|
56
59
|
if (!opts.dryRun) {
|
|
57
60
|
lock = await acquireInstallLock();
|
|
58
61
|
}
|
|
@@ -60,7 +63,7 @@ export async function runSetup(opts) {
|
|
|
60
63
|
const existingManifest = await loadManifest();
|
|
61
64
|
if (existingManifest && existingManifest.version !== version) {
|
|
62
65
|
info(`Updating ${chalk.dim(existingManifest.version)} → ${chalk.green(version)}`);
|
|
63
|
-
|
|
66
|
+
blank();
|
|
64
67
|
}
|
|
65
68
|
// === Per-harness loop ===
|
|
66
69
|
const perHarnessResults = [];
|
|
@@ -72,7 +75,7 @@ export async function runSetup(opts) {
|
|
|
72
75
|
// detection; using the wrong harness's prev list silently orphans
|
|
73
76
|
// files (spec §7.6.1 per-iteration state isolation).
|
|
74
77
|
const existingHarness = existingManifest?.harnesses[harnessName];
|
|
75
|
-
|
|
78
|
+
printHarnessHeader(profile.displayName);
|
|
76
79
|
if (existingHarness && !existingHarness.partial) {
|
|
77
80
|
info(chalk.dim(` Already installed at v${version} — checking for changes`));
|
|
78
81
|
}
|
|
@@ -95,10 +98,23 @@ export async function runSetup(opts) {
|
|
|
95
98
|
error: err.message,
|
|
96
99
|
});
|
|
97
100
|
warn(`[${harnessName}] skipped (user declined conflict) — continuing with remaining harnesses`);
|
|
98
|
-
|
|
101
|
+
blank();
|
|
99
102
|
continue;
|
|
100
103
|
}
|
|
101
|
-
|
|
104
|
+
// Operational failure (e.g. unreadable dest dir, non-TTY refusal):
|
|
105
|
+
// classify-and-continue like the MCP branch below — rethrowing
|
|
106
|
+
// escaped the per-harness loop, leaving installed siblings with NO
|
|
107
|
+
// manifest record and skipping later harnesses (audit pass 6,
|
|
108
|
+
// PROBE D). classifyExit yields 1 for a failed result.
|
|
109
|
+
perHarnessResults.push({
|
|
110
|
+
harnessName,
|
|
111
|
+
profile,
|
|
112
|
+
status: "failed",
|
|
113
|
+
error: err instanceof Error ? err.message : String(err),
|
|
114
|
+
});
|
|
115
|
+
warn(`[${harnessName}] conflict check failed — continuing with remaining harnesses`);
|
|
116
|
+
blank();
|
|
117
|
+
continue;
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
// MCP must succeed for a manifest entry to exist (the entry depends
|
|
@@ -116,7 +132,7 @@ export async function runSetup(opts) {
|
|
|
116
132
|
error: err instanceof Error ? err.message : String(err),
|
|
117
133
|
});
|
|
118
134
|
warn(`[${harnessName}] MCP configuration failed — continuing with remaining harnesses`);
|
|
119
|
-
|
|
135
|
+
blank();
|
|
120
136
|
continue;
|
|
121
137
|
}
|
|
122
138
|
// Subsequent steps may throw on pre-loop work (mkdir EACCES, etc.).
|
|
@@ -158,7 +174,7 @@ export async function runSetup(opts) {
|
|
|
158
174
|
metricsResult,
|
|
159
175
|
partial: failedStep,
|
|
160
176
|
});
|
|
161
|
-
|
|
177
|
+
blank();
|
|
162
178
|
}
|
|
163
179
|
// === Once-per-run: AFTER the per-harness loop ===
|
|
164
180
|
// Global @uluops/cli install — single prompt, single install across the
|
|
@@ -192,32 +208,70 @@ export async function runSetup(opts) {
|
|
|
192
208
|
// harness entry. Declined harnesses and pre-MCP failures land no entry.
|
|
193
209
|
if (!opts.dryRun) {
|
|
194
210
|
const now = new Date().toISOString();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
// Clone rather than alias: mutating the loaded object would silently
|
|
212
|
+
// change what any later `existingManifest` read sees. Today all reads
|
|
213
|
+
// precede this block — the clone keeps that a non-condition instead of
|
|
214
|
+
// an ordering invariant someone has to remember.
|
|
215
|
+
const manifest = existingManifest
|
|
216
|
+
? structuredClone(existingManifest)
|
|
217
|
+
: {
|
|
218
|
+
version,
|
|
219
|
+
installedAt: now,
|
|
220
|
+
shellModified: false,
|
|
221
|
+
harnesses: {},
|
|
222
|
+
};
|
|
201
223
|
manifest.version = version;
|
|
202
224
|
manifest.installedAt = now;
|
|
203
225
|
manifest.shellModified = shellModified || manifest.shellModified;
|
|
204
226
|
for (const r of perHarnessResults) {
|
|
205
227
|
if (!r.mcpResult)
|
|
206
228
|
continue; // no MCP success → no entry
|
|
229
|
+
// A step that THREW produced no result — falling back to [] here
|
|
230
|
+
// would replace a populated prior entry with an empty record,
|
|
231
|
+
// orphaning every previously-installed file the moment a re-run
|
|
232
|
+
// fails (uninstall trusts these lists). Undefined result = keep the
|
|
233
|
+
// prior record; the `partial` marker names what didn't complete.
|
|
234
|
+
const prevEntry = existingManifest?.harnesses[r.harnessName];
|
|
235
|
+
const newDefsScope = opts.localDefs ? "local" : "global";
|
|
236
|
+
// TWO gates, deliberately: the FILE LISTS live under defsPath and
|
|
237
|
+
// may only be inherited within the same scope (a global list against
|
|
238
|
+
// a local path points uninstall at the wrong tree). The HOOK fields
|
|
239
|
+
// live in settings.json under profile.paths — scope-independent —
|
|
240
|
+
// and gating them on defsScope falsified hooksInstalled on a scope
|
|
241
|
+
// flip (audit pass 6, PROBE C).
|
|
242
|
+
const prevLists = prevEntry && prevEntry.defsScope === newDefsScope
|
|
243
|
+
? prevEntry
|
|
244
|
+
: undefined;
|
|
245
|
+
const prevHooks = prevEntry;
|
|
246
|
+
if (prevEntry && !prevLists) {
|
|
247
|
+
// Scope flip: the prior tree at the old defsPath is no longer
|
|
248
|
+
// tracked by this manifest — say so rather than dropping it
|
|
249
|
+
// silently (cross-scope cleanup is not implemented).
|
|
250
|
+
warn(`[${r.harnessName}] defs scope changed (${prevEntry.defsScope} → ${newDefsScope}): previously installed files remain untracked at ${prevEntry.defsPath}`);
|
|
251
|
+
}
|
|
252
|
+
// A metrics result whose skippedReason is set NEVER OBSERVED the
|
|
253
|
+
// hook state ("--no-metrics" means don't touch metrics; unsupported
|
|
254
|
+
// harnesses too) — `??` alone can't express that because false is a
|
|
255
|
+
// value. Only an observing run may change the recorded hook state.
|
|
256
|
+
const metricsObserved = r.metricsResult !== undefined && !r.metricsResult.skippedReason;
|
|
207
257
|
const harnessEntry = {
|
|
208
258
|
installedAt: now,
|
|
209
259
|
setupVersion: version,
|
|
210
260
|
mcpScope: opts.scope,
|
|
211
261
|
mcpConfigPath: r.mcpResult.configPath,
|
|
212
|
-
defsScope:
|
|
262
|
+
defsScope: newDefsScope,
|
|
213
263
|
defsPath: opts.localDefs
|
|
214
264
|
? join(await findProjectRoot(), "uluops")
|
|
215
265
|
: r.profile.paths.home,
|
|
216
|
-
agents: r.agentsResult?.files ?? [],
|
|
217
|
-
commands: r.commandsResult?.files ?? [],
|
|
218
|
-
skills: r.skillsResult?.files ?? [],
|
|
219
|
-
hooksInstalled:
|
|
220
|
-
|
|
266
|
+
agents: r.agentsResult?.files ?? prevLists?.agents ?? [],
|
|
267
|
+
commands: r.commandsResult?.files ?? prevLists?.commands ?? [],
|
|
268
|
+
skills: r.skillsResult?.files ?? prevLists?.skills ?? [],
|
|
269
|
+
hooksInstalled: metricsObserved
|
|
270
|
+
? (r.metricsResult?.hookConfigured ?? false)
|
|
271
|
+
: (prevHooks?.hooksInstalled ?? false),
|
|
272
|
+
hooksInstalledVersion: metricsObserved
|
|
273
|
+
? (r.metricsResult?.hooksInstalledVersion ?? null)
|
|
274
|
+
: (prevHooks?.hooksInstalledVersion ?? null),
|
|
221
275
|
partial: r.partial ?? null,
|
|
222
276
|
};
|
|
223
277
|
manifest.harnesses[r.harnessName] = harnessEntry;
|
|
@@ -247,20 +301,31 @@ export async function runSetup(opts) {
|
|
|
247
301
|
// per-harness status icons, partial markers, re-run hints, and the
|
|
248
302
|
// aggregate counts in the header. Single-harness path preserves
|
|
249
303
|
// today's Setup-complete banner format inside the same function.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
304
|
+
try {
|
|
305
|
+
await printSetupSummary({
|
|
306
|
+
results: perHarnessResults,
|
|
307
|
+
apiKey,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
// A render failure must not invert the run outcome: the install and
|
|
312
|
+
// manifest write already happened — classifyExit below is the
|
|
313
|
+
// authority, not the pretty-printer.
|
|
314
|
+
warn(`Could not render the setup summary: ${err instanceof Error ? err.message : String(err)}`);
|
|
315
|
+
}
|
|
254
316
|
// Exit-code classifier (spec §7.5 4-tier table). One call, one place.
|
|
255
317
|
// Empty perHarnessResults already short-circuited above with the
|
|
256
318
|
// "nothing to install" message; classifyExit handles defense-in-depth.
|
|
257
|
-
|
|
258
|
-
if (exitCode !== 0) {
|
|
259
|
-
process.exit(exitCode);
|
|
260
|
-
}
|
|
319
|
+
exitCode = classifyExit(perHarnessResults);
|
|
261
320
|
}
|
|
262
321
|
finally {
|
|
263
322
|
if (lock)
|
|
264
323
|
await lock.release();
|
|
265
324
|
}
|
|
325
|
+
// process.exit inside the try would skip the finally and leave the lock
|
|
326
|
+
// held (the signal handlers are a backstop, not the contract) — classify
|
|
327
|
+
// inside, exit only after cleanup has run.
|
|
328
|
+
if (exitCode !== 0) {
|
|
329
|
+
process.exit(exitCode);
|
|
330
|
+
}
|
|
266
331
|
}
|