@uluops/setup 0.6.5 → 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.
- package/README.md +91 -9
- package/assets/codex/skills/uluops-operator/SKILL.md +159 -0
- package/dist/cli/select-harnesses.d.ts +91 -0
- package/dist/cli/select-harnesses.js +108 -0
- package/dist/cli.js +90 -37
- package/dist/commands/errors.d.ts +24 -0
- package/dist/commands/errors.js +28 -0
- package/dist/commands/helpers.d.ts +32 -0
- package/dist/commands/helpers.js +146 -3
- package/dist/commands/per-harness.d.ts +64 -0
- package/dist/commands/per-harness.js +37 -0
- package/dist/commands/setup.d.ts +7 -3
- package/dist/commands/setup.js +232 -71
- package/dist/commands/uninstall-filter.d.ts +36 -0
- package/dist/commands/uninstall-filter.js +69 -0
- package/dist/commands/uninstall.d.ts +12 -2
- package/dist/commands/uninstall.js +190 -82
- package/dist/harnesses/codex.d.ts +5 -10
- package/dist/harnesses/codex.js +89 -21
- package/dist/harnesses/index.js +6 -1
- package/dist/harnesses/opencode.d.ts +8 -0
- package/dist/harnesses/opencode.js +24 -1
- package/dist/harnesses/types.d.ts +2 -0
- package/dist/harnesses/types.js +5 -1
- package/dist/lib/atomic-write.js +10 -2
- package/dist/lib/config-merger.d.ts +6 -3
- package/dist/lib/config-merger.js +50 -7
- package/dist/lib/display.d.ts +21 -5
- package/dist/lib/display.js +118 -13
- package/dist/lib/file-ops.d.ts +13 -5
- package/dist/lib/file-ops.js +34 -34
- package/dist/lib/install-lock.d.ts +47 -0
- package/dist/lib/install-lock.js +251 -0
- package/dist/lib/json-guards.d.ts +22 -0
- package/dist/lib/json-guards.js +33 -0
- package/dist/lib/manifest.d.ts +28 -0
- package/dist/lib/manifest.js +61 -12
- package/dist/lib/paths.d.ts +2 -17
- package/dist/lib/paths.js +4 -19
- package/dist/lib/settings-merger.js +3 -1
- package/dist/steps/agent-metrics-cli.d.ts +72 -0
- package/dist/steps/agent-metrics-cli.js +147 -0
- package/dist/steps/agents.d.ts +11 -0
- package/dist/steps/agents.js +30 -25
- package/dist/steps/auth.d.ts +13 -0
- package/dist/steps/auth.js +61 -5
- package/dist/steps/cli.d.ts +6 -0
- package/dist/steps/cli.js +29 -10
- package/dist/steps/commands.d.ts +10 -0
- package/dist/steps/commands.js +31 -30
- package/dist/steps/detect.js +15 -1
- package/dist/steps/mcp.js +1 -8
- package/dist/steps/metrics.js +10 -3
- package/dist/steps/shell.js +3 -13
- package/dist/steps/signup.js +14 -1
- package/dist/steps/skills.d.ts +14 -0
- package/dist/steps/skills.js +95 -0
- package/dist/steps/verify.js +195 -91
- package/package.json +3 -2
package/dist/commands/setup.js
CHANGED
|
@@ -2,92 +2,253 @@ 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 } from "../lib/display.js";
|
|
5
|
+
import { info, printSetupSummary, warn } from "../lib/display.js";
|
|
6
6
|
import { getVersion } from "../lib/version.js";
|
|
7
7
|
import { getProfile } from "../harnesses/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import { acquireInstallLock, } from "../lib/install-lock.js";
|
|
9
|
+
import { initContext, checkConflicts, configureMcpStep, installAgentsDefs, installCommandsDefs, installSkillsDefs, configureMetricsStep, configureCliStep, configureAgentMetricsCliStep, runHealthCheck, configureShell, } from "./helpers.js";
|
|
10
|
+
import { ConflictRejectedError } from "./errors.js";
|
|
11
|
+
import { classifyExit, } from "./per-harness.js";
|
|
9
12
|
export async function runSetup(opts) {
|
|
13
|
+
if (opts.harnesses.length === 0) {
|
|
14
|
+
info(chalk.dim("Nothing to install — re-run with at least one harness selected.\n"));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
10
17
|
const version = await getVersion();
|
|
11
|
-
|
|
18
|
+
// Resolve every harness up front so a typo fails fast before any state
|
|
19
|
+
// is touched. getProfile throws HarnessNotTestedError or a friendly
|
|
20
|
+
// unknown-name error; the top-level catch in cli.ts surfaces them.
|
|
21
|
+
const profiles = opts.harnesses.map((name) => getProfile(name));
|
|
12
22
|
console.log();
|
|
13
23
|
console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")}`);
|
|
14
24
|
console.log(` ${chalk.dim("operating intelligence as infrastructure")}`);
|
|
15
25
|
console.log();
|
|
16
|
-
|
|
26
|
+
const targetSummary = profiles.length === 1
|
|
27
|
+
? profiles[0].displayName
|
|
28
|
+
: `${profiles.length} harnesses (${profiles.map((p) => p.displayName).join(", ")})`;
|
|
29
|
+
console.log(` Setup v${version} — ${chalk.bold(targetSummary)}`);
|
|
17
30
|
console.log();
|
|
18
31
|
if (opts.dryRun) {
|
|
19
32
|
info(chalk.dim("(dry run — no changes will be made)\n"));
|
|
20
33
|
}
|
|
34
|
+
// === Once-per-run: BEFORE the per-harness loop ===
|
|
21
35
|
const { env, apiKey } = await initContext(opts);
|
|
22
36
|
console.log();
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
else if (existingHarness) {
|
|
31
|
-
info(chalk.dim(`Already at v${version} — checking for changes`));
|
|
32
|
-
console.log();
|
|
33
|
-
}
|
|
34
|
-
// Check for conflicts on first install for this harness
|
|
35
|
-
if (!existingHarness && !opts.yes && !opts.dryRun) {
|
|
36
|
-
await checkConflicts(profile, opts.localDefs);
|
|
37
|
-
}
|
|
38
|
-
const mcpResult = await configureMcpStep(profile, apiKey, opts);
|
|
39
|
-
const agentsResult = await installAgentsDefs(profile, opts, existingHarness?.agents);
|
|
40
|
-
const commandsResult = await installCommandsDefs(profile, opts, existingHarness?.commands);
|
|
41
|
-
const metricsResult = await configureMetricsStep(profile, opts);
|
|
42
|
-
const cliResult = await configureCliStep({
|
|
43
|
-
withCli: opts.withCli,
|
|
44
|
-
cli: opts.cli,
|
|
45
|
-
yes: opts.yes,
|
|
46
|
-
apiKey: opts.apiKey,
|
|
47
|
-
dryRun: opts.dryRun,
|
|
48
|
-
});
|
|
49
|
-
await runHealthCheck(opts);
|
|
50
|
-
const shellModified = await configureShell(env, apiKey, opts);
|
|
51
|
-
// Save manifest
|
|
37
|
+
// Acquire the install lock before touching any shared state. Skipped on
|
|
38
|
+
// dry-run (read-only). The lock excludes a second concurrent uluops-setup
|
|
39
|
+
// from racing the manifest / MCP config / shell-profile / settings.json
|
|
40
|
+
// read-merge-write windows. Held across the entire multi-harness loop —
|
|
41
|
+
// concurrent multi-harness installs from separate processes serialize
|
|
42
|
+
// (spec §10.6).
|
|
43
|
+
let lock = null;
|
|
52
44
|
if (!opts.dryRun) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
defsPath: opts.localDefs
|
|
61
|
-
? join(await findProjectRoot(), "uluops")
|
|
62
|
-
: profile.paths.home,
|
|
63
|
-
agents: agentsResult.files,
|
|
64
|
-
commands: commandsResult.files,
|
|
65
|
-
hooksInstalled: metricsResult.hookConfigured,
|
|
66
|
-
hooksInstalledVersion: metricsResult.hooksInstalledVersion,
|
|
67
|
-
};
|
|
68
|
-
const manifest = existingManifest ?? {
|
|
69
|
-
version,
|
|
70
|
-
installedAt: now,
|
|
71
|
-
shellModified: false,
|
|
72
|
-
harnesses: {},
|
|
73
|
-
};
|
|
74
|
-
manifest.version = version;
|
|
75
|
-
manifest.installedAt = now;
|
|
76
|
-
manifest.shellModified = shellModified || manifest.shellModified;
|
|
77
|
-
manifest.harnesses[profile.name] = harnessEntry;
|
|
78
|
-
// Only flip cliInstalled to true when WE installed it (not when user-installed).
|
|
79
|
-
// Once true, persist across re-runs so uninstall remains symmetric — until the
|
|
80
|
-
// user explicitly removes it with --no-cli + uninstall, this manifest owns it.
|
|
81
|
-
if (cliResult && cliResult.installed && !cliResult.alreadyPresent) {
|
|
82
|
-
manifest.cliInstalled = true;
|
|
83
|
-
manifest.cliInstalledVersion = cliResult.version;
|
|
45
|
+
lock = await acquireInstallLock();
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const existingManifest = await loadManifest();
|
|
49
|
+
if (existingManifest && existingManifest.version !== version) {
|
|
50
|
+
info(`Updating ${chalk.dim(existingManifest.version)} → ${chalk.green(version)}`);
|
|
51
|
+
console.log();
|
|
84
52
|
}
|
|
85
|
-
|
|
53
|
+
// === Per-harness loop ===
|
|
54
|
+
const perHarnessResults = [];
|
|
55
|
+
for (const profile of profiles) {
|
|
56
|
+
const harnessName = profile.name;
|
|
57
|
+
// Read each harness's own slice of prior state every iteration. Never
|
|
58
|
+
// reuse a single existingHarness across iterations — installAgents/
|
|
59
|
+
// Commands/Skills consume prev lists from existingHarness for drift
|
|
60
|
+
// detection; using the wrong harness's prev list silently orphans
|
|
61
|
+
// files (spec §7.6.1 per-iteration state isolation).
|
|
62
|
+
const existingHarness = existingManifest?.harnesses[harnessName];
|
|
63
|
+
console.log(chalk.dim(`▸ ${profile.displayName}`));
|
|
64
|
+
if (existingHarness && !existingHarness.partial) {
|
|
65
|
+
info(chalk.dim(` Already installed at v${version} — checking for changes`));
|
|
66
|
+
}
|
|
67
|
+
// First-install conflict check, OR re-run after a partial install
|
|
68
|
+
// (the user never confirmed the conflict on the failing run, so we
|
|
69
|
+
// re-prompt — spec §7.6.5).
|
|
70
|
+
const needsConflictCheck = !opts.yes &&
|
|
71
|
+
!opts.dryRun &&
|
|
72
|
+
(!existingHarness || existingHarness.partial != null);
|
|
73
|
+
if (needsConflictCheck) {
|
|
74
|
+
try {
|
|
75
|
+
await checkConflicts(profile, opts.localDefs);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err instanceof ConflictRejectedError) {
|
|
79
|
+
perHarnessResults.push({
|
|
80
|
+
harnessName,
|
|
81
|
+
profile,
|
|
82
|
+
status: "declined",
|
|
83
|
+
error: err.message,
|
|
84
|
+
});
|
|
85
|
+
warn(`[${harnessName}] skipped (user declined conflict) — continuing with remaining harnesses`);
|
|
86
|
+
console.log();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// MCP must succeed for a manifest entry to exist (the entry depends
|
|
93
|
+
// on mcpResult.configPath). An MCP throw means no manifest entry —
|
|
94
|
+
// harness marked failed and the loop continues to siblings.
|
|
95
|
+
let mcpResult;
|
|
96
|
+
try {
|
|
97
|
+
mcpResult = await configureMcpStep(profile, apiKey, opts);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
perHarnessResults.push({
|
|
101
|
+
harnessName,
|
|
102
|
+
profile,
|
|
103
|
+
status: "failed",
|
|
104
|
+
error: err instanceof Error ? err.message : String(err),
|
|
105
|
+
});
|
|
106
|
+
warn(`[${harnessName}] MCP configuration failed — continuing with remaining harnesses`);
|
|
107
|
+
console.log();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Subsequent steps may throw on pre-loop work (mkdir EACCES, etc.).
|
|
111
|
+
// If they do, build a partial manifest entry naming the failed step
|
|
112
|
+
// (spec §7.6.2 Case B). The per-file failures[] arrays on each
|
|
113
|
+
// result are surfaced inside the helper's warn() calls.
|
|
114
|
+
let agentsResult;
|
|
115
|
+
let commandsResult;
|
|
116
|
+
let skillsResult;
|
|
117
|
+
let metricsResult;
|
|
118
|
+
let failedStep = null;
|
|
119
|
+
let failedError;
|
|
120
|
+
try {
|
|
121
|
+
agentsResult = await installAgentsDefs(profile, opts, existingHarness?.agents);
|
|
122
|
+
commandsResult = await installCommandsDefs(profile, opts, existingHarness?.commands);
|
|
123
|
+
skillsResult = await installSkillsDefs(profile, opts, existingHarness?.skills);
|
|
124
|
+
metricsResult = await configureMetricsStep(profile, opts);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
failedError = err instanceof Error ? err.message : String(err);
|
|
128
|
+
failedStep = !agentsResult
|
|
129
|
+
? "agents"
|
|
130
|
+
: !commandsResult
|
|
131
|
+
? "commands"
|
|
132
|
+
: !skillsResult
|
|
133
|
+
? "skills"
|
|
134
|
+
: "metrics";
|
|
135
|
+
warn(`[${harnessName}] ${failedStep} step failed: ${failedError} — recording partial state`);
|
|
136
|
+
}
|
|
137
|
+
perHarnessResults.push({
|
|
138
|
+
harnessName,
|
|
139
|
+
profile,
|
|
140
|
+
status: failedStep ? "failed" : "ok",
|
|
141
|
+
error: failedError,
|
|
142
|
+
mcpResult,
|
|
143
|
+
agentsResult,
|
|
144
|
+
commandsResult,
|
|
145
|
+
skillsResult,
|
|
146
|
+
metricsResult,
|
|
147
|
+
partial: failedStep,
|
|
148
|
+
});
|
|
149
|
+
console.log();
|
|
150
|
+
}
|
|
151
|
+
// === Once-per-run: AFTER the per-harness loop ===
|
|
152
|
+
// Global @uluops/cli install — single prompt, single install across the
|
|
153
|
+
// whole run regardless of harness count.
|
|
154
|
+
const cliResult = await configureCliStep({
|
|
155
|
+
withCli: opts.withCli,
|
|
156
|
+
cli: opts.cli,
|
|
157
|
+
yes: opts.yes,
|
|
158
|
+
apiKey: opts.apiKey,
|
|
159
|
+
dryRun: opts.dryRun,
|
|
160
|
+
});
|
|
161
|
+
// Agent-metrics CLI install — gated on AT LEAST ONE harness having
|
|
162
|
+
// successfully configured the hook. Aggregate gate replaces the
|
|
163
|
+
// per-iteration gate from the single-harness version. Without this,
|
|
164
|
+
// a multi-harness run where only one harness supports hooks (e.g.,
|
|
165
|
+
// Claude Code among Codex/Gemini siblings) would never prompt for the
|
|
166
|
+
// agent-metrics CLI (spec §7.6.2 aggregation).
|
|
167
|
+
const anyHookConfigured = perHarnessResults.some((r) => r.status === "ok" && r.metricsResult?.hookConfigured);
|
|
168
|
+
const agentMetricsCliResult = anyHookConfigured
|
|
169
|
+
? await configureAgentMetricsCliStep({
|
|
170
|
+
withAgentMetricsCli: opts.withAgentMetricsCli,
|
|
171
|
+
agentMetricsCli: opts.agentMetricsCli,
|
|
172
|
+
yes: opts.yes,
|
|
173
|
+
apiKey: opts.apiKey,
|
|
174
|
+
dryRun: opts.dryRun,
|
|
175
|
+
})
|
|
176
|
+
: null;
|
|
177
|
+
await runHealthCheck(opts);
|
|
178
|
+
const shellModified = await configureShell(env, apiKey, opts);
|
|
179
|
+
// Save manifest ONCE at end, aggregating every successful or partial
|
|
180
|
+
// harness entry. Declined harnesses and pre-MCP failures land no entry.
|
|
181
|
+
if (!opts.dryRun) {
|
|
182
|
+
const now = new Date().toISOString();
|
|
183
|
+
const manifest = existingManifest ?? {
|
|
184
|
+
version,
|
|
185
|
+
installedAt: now,
|
|
186
|
+
shellModified: false,
|
|
187
|
+
harnesses: {},
|
|
188
|
+
};
|
|
189
|
+
manifest.version = version;
|
|
190
|
+
manifest.installedAt = now;
|
|
191
|
+
manifest.shellModified = shellModified || manifest.shellModified;
|
|
192
|
+
for (const r of perHarnessResults) {
|
|
193
|
+
if (!r.mcpResult)
|
|
194
|
+
continue; // no MCP success → no entry
|
|
195
|
+
const harnessEntry = {
|
|
196
|
+
installedAt: now,
|
|
197
|
+
setupVersion: version,
|
|
198
|
+
mcpScope: opts.scope,
|
|
199
|
+
mcpConfigPath: r.mcpResult.configPath,
|
|
200
|
+
defsScope: opts.localDefs ? "local" : "global",
|
|
201
|
+
defsPath: opts.localDefs
|
|
202
|
+
? join(await findProjectRoot(), "uluops")
|
|
203
|
+
: r.profile.paths.home,
|
|
204
|
+
agents: r.agentsResult?.files ?? [],
|
|
205
|
+
commands: r.commandsResult?.files ?? [],
|
|
206
|
+
skills: r.skillsResult?.files ?? [],
|
|
207
|
+
hooksInstalled: r.metricsResult?.hookConfigured ?? false,
|
|
208
|
+
hooksInstalledVersion: r.metricsResult?.hooksInstalledVersion ?? null,
|
|
209
|
+
partial: r.partial ?? null,
|
|
210
|
+
};
|
|
211
|
+
manifest.harnesses[r.harnessName] = harnessEntry;
|
|
212
|
+
}
|
|
213
|
+
// Same ownership rule as before — only manifest a global install we
|
|
214
|
+
// performed ourselves (not user-installed).
|
|
215
|
+
if (cliResult && cliResult.installed && !cliResult.alreadyPresent) {
|
|
216
|
+
manifest.cliInstalled = true;
|
|
217
|
+
manifest.cliInstalledVersion = cliResult.version;
|
|
218
|
+
}
|
|
219
|
+
if (agentMetricsCliResult &&
|
|
220
|
+
agentMetricsCliResult.installed &&
|
|
221
|
+
!agentMetricsCliResult.alreadyPresent) {
|
|
222
|
+
manifest.agentMetricsCliInstalled = true;
|
|
223
|
+
manifest.agentMetricsCliInstalledVersion =
|
|
224
|
+
agentMetricsCliResult.version;
|
|
225
|
+
}
|
|
226
|
+
// Only save the manifest if it has at least one harness entry — the
|
|
227
|
+
// isNewManifest loader rejects empty-harnesses manifests, and saving
|
|
228
|
+
// one would write an unloadable file on every all-declined run.
|
|
229
|
+
if (Object.keys(manifest.harnesses).length > 0) {
|
|
230
|
+
await saveManifest(manifest);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// === Summary ===
|
|
234
|
+
// Single rendering call producing the full multi-harness block with
|
|
235
|
+
// per-harness status icons, partial markers, re-run hints, and the
|
|
236
|
+
// aggregate counts in the header. Single-harness path preserves
|
|
237
|
+
// today's Setup-complete banner format inside the same function.
|
|
238
|
+
await printSetupSummary({
|
|
239
|
+
results: perHarnessResults,
|
|
240
|
+
apiKey,
|
|
241
|
+
});
|
|
242
|
+
// Exit-code classifier (spec §7.5 4-tier table). One call, one place.
|
|
243
|
+
// Empty perHarnessResults already short-circuited above with the
|
|
244
|
+
// "nothing to install" message; classifyExit handles defense-in-depth.
|
|
245
|
+
const exitCode = classifyExit(perHarnessResults);
|
|
246
|
+
if (exitCode !== 0) {
|
|
247
|
+
process.exit(exitCode);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
if (lock)
|
|
252
|
+
await lock.release();
|
|
86
253
|
}
|
|
87
|
-
await printSetupSummary({
|
|
88
|
-
profile,
|
|
89
|
-
agentCount: agentsResult.files.length,
|
|
90
|
-
commandCount: commandsResult.files.length,
|
|
91
|
-
apiKey,
|
|
92
|
-
});
|
|
93
254
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter parser for `runUninstall`. Mirrors the install-side selection
|
|
3
|
+
* syntax (`--harness all`, comma-split, `--all-detected` synonym, conflict
|
|
4
|
+
* detection) but resolves against the manifest's recorded harnesses
|
|
5
|
+
* rather than detection — uninstall only acts on what's actually
|
|
6
|
+
* installed.
|
|
7
|
+
*
|
|
8
|
+
* Returns the resolved subset of harness names to uninstall, or `null`
|
|
9
|
+
* meaning "every harness in the manifest" (the no-filter default —
|
|
10
|
+
* today's behavior preserved).
|
|
11
|
+
*
|
|
12
|
+
* Throws on:
|
|
13
|
+
* - flag conflict: --harness <name> + --all-detected
|
|
14
|
+
* - unknown harness in filter: every named harness must exist in the
|
|
15
|
+
* manifest, otherwise the user typed something that won't do what
|
|
16
|
+
* they expect (fail fast, don't pretend to uninstall a no-op)
|
|
17
|
+
*/
|
|
18
|
+
export declare class UninstallFilterError extends Error {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
export interface UninstallFilterInput {
|
|
22
|
+
/** Value of --harness (may be undefined when not passed). */
|
|
23
|
+
harnessArg?: string;
|
|
24
|
+
/** Whether --harness was actually passed on the CLI (not just defaulted). */
|
|
25
|
+
harnessFromCli: boolean;
|
|
26
|
+
/** Whether --all-detected was passed. */
|
|
27
|
+
allDetected: boolean;
|
|
28
|
+
/** Names of harnesses actually present in the on-disk manifest. */
|
|
29
|
+
manifestHarnesses: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @returns string[] — subset of manifestHarnesses to uninstall
|
|
33
|
+
* @returns null — uninstall every harness in the manifest (today's
|
|
34
|
+
* default behavior; preserved for back-compat)
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveUninstallFilter(input: UninstallFilterInput): string[] | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter parser for `runUninstall`. Mirrors the install-side selection
|
|
3
|
+
* syntax (`--harness all`, comma-split, `--all-detected` synonym, conflict
|
|
4
|
+
* detection) but resolves against the manifest's recorded harnesses
|
|
5
|
+
* rather than detection — uninstall only acts on what's actually
|
|
6
|
+
* installed.
|
|
7
|
+
*
|
|
8
|
+
* Returns the resolved subset of harness names to uninstall, or `null`
|
|
9
|
+
* meaning "every harness in the manifest" (the no-filter default —
|
|
10
|
+
* today's behavior preserved).
|
|
11
|
+
*
|
|
12
|
+
* Throws on:
|
|
13
|
+
* - flag conflict: --harness <name> + --all-detected
|
|
14
|
+
* - unknown harness in filter: every named harness must exist in the
|
|
15
|
+
* manifest, otherwise the user typed something that won't do what
|
|
16
|
+
* they expect (fail fast, don't pretend to uninstall a no-op)
|
|
17
|
+
*/
|
|
18
|
+
export class UninstallFilterError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "UninstallFilterError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @returns string[] — subset of manifestHarnesses to uninstall
|
|
26
|
+
* @returns null — uninstall every harness in the manifest (today's
|
|
27
|
+
* default behavior; preserved for back-compat)
|
|
28
|
+
*/
|
|
29
|
+
export function resolveUninstallFilter(input) {
|
|
30
|
+
// Conflict: --all-detected with --harness <specific-name> is ambiguous.
|
|
31
|
+
// The 'all' literal is exempt because both flags mean the same thing
|
|
32
|
+
// there (uninstall everything).
|
|
33
|
+
if (input.allDetected &&
|
|
34
|
+
input.harnessFromCli &&
|
|
35
|
+
input.harnessArg &&
|
|
36
|
+
input.harnessArg.trim() !== "all") {
|
|
37
|
+
throw new UninstallFilterError(`--harness ${input.harnessArg} conflicts with --all-detected; pick one`);
|
|
38
|
+
}
|
|
39
|
+
// --all-detected (with or without --harness all) → no filter, uninstall all.
|
|
40
|
+
if (input.allDetected)
|
|
41
|
+
return null;
|
|
42
|
+
// No --harness passed → no filter, uninstall all (today's behavior).
|
|
43
|
+
if (!input.harnessFromCli || !input.harnessArg)
|
|
44
|
+
return null;
|
|
45
|
+
// --harness all → no filter, uninstall all.
|
|
46
|
+
const trimmed = input.harnessArg.trim();
|
|
47
|
+
if (trimmed === "all")
|
|
48
|
+
return null;
|
|
49
|
+
// Otherwise parse comma-split subset.
|
|
50
|
+
const requested = trimmed
|
|
51
|
+
.split(",")
|
|
52
|
+
.map((s) => s.trim())
|
|
53
|
+
.filter((s) => s.length > 0);
|
|
54
|
+
if (requested.length === 0)
|
|
55
|
+
return null; // pathological — treat as no filter
|
|
56
|
+
// Validate every requested name exists in the manifest. A typo or a
|
|
57
|
+
// harness the user never installed should fail fast — silently
|
|
58
|
+
// skipping would let `--uninstall --harness claude-coed` succeed with
|
|
59
|
+
// zero work and the user thinking they uninstalled something.
|
|
60
|
+
const unknown = requested.filter((n) => !input.manifestHarnesses.includes(n));
|
|
61
|
+
if (unknown.length > 0) {
|
|
62
|
+
const listed = input.manifestHarnesses.length > 0
|
|
63
|
+
? input.manifestHarnesses.join(", ")
|
|
64
|
+
: "(none)";
|
|
65
|
+
throw new UninstallFilterError(`Unknown harness in --harness filter: ${unknown.join(", ")}. ` +
|
|
66
|
+
`Manifest contains: ${listed}`);
|
|
67
|
+
}
|
|
68
|
+
return requested;
|
|
69
|
+
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface RunUninstallOpts {
|
|
2
2
|
dryRun: boolean;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Value of --harness (may be 'all', comma-split list, or a single name).
|
|
5
|
+
* Undefined when --harness wasn't passed.
|
|
6
|
+
*/
|
|
7
|
+
harnessArg?: string;
|
|
8
|
+
/** Whether --harness was explicitly passed (vs. its commander default). */
|
|
9
|
+
harnessFromCli?: boolean;
|
|
10
|
+
/** Whether --all-detected was passed. */
|
|
11
|
+
allDetected?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function runUninstall(opts: RunUninstallOpts): Promise<void>;
|