@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.
- package/README.md +94 -10
- 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 +78 -37
- package/dist/commands/errors.d.ts +24 -0
- package/dist/commands/errors.js +28 -0
- package/dist/commands/helpers.d.ts +7 -0
- package/dist/commands/helpers.js +80 -3
- package/dist/commands/per-harness.d.ts +64 -0
- package/dist/commands/per-harness.js +37 -0
- package/dist/commands/setup.d.ts +5 -3
- package/dist/commands/setup.js +174 -48
- 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 +121 -45
- package/dist/harnesses/codex.d.ts +5 -10
- package/dist/harnesses/codex.js +212 -22
- 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 +8 -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.js +11 -1
- package/dist/lib/json-guards.d.ts +22 -0
- package/dist/lib/json-guards.js +33 -0
- package/dist/lib/manifest.d.ts +20 -0
- package/dist/lib/manifest.js +61 -12
- package/dist/lib/paths.d.ts +0 -17
- package/dist/lib/paths.js +0 -19
- package/dist/lib/settings-merger.js +3 -1
- package/dist/steps/agent-metrics-cli.d.ts +9 -1
- package/dist/steps/agent-metrics-cli.js +66 -20
- 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,53 +2,155 @@ 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
8
|
import { acquireInstallLock, } from "../lib/install-lock.js";
|
|
9
|
-
import { initContext, checkConflicts, configureMcpStep, installAgentsDefs, installCommandsDefs, configureMetricsStep, configureCliStep, configureAgentMetricsCliStep, runHealthCheck, configureShell, } from "./helpers.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";
|
|
10
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
|
+
}
|
|
11
17
|
const version = await getVersion();
|
|
12
|
-
|
|
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));
|
|
13
22
|
console.log();
|
|
14
23
|
console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")}`);
|
|
15
24
|
console.log(` ${chalk.dim("operating intelligence as infrastructure")}`);
|
|
16
25
|
console.log();
|
|
17
|
-
|
|
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)}`);
|
|
18
30
|
console.log();
|
|
19
31
|
if (opts.dryRun) {
|
|
20
32
|
info(chalk.dim("(dry run — no changes will be made)\n"));
|
|
21
33
|
}
|
|
34
|
+
// === Once-per-run: BEFORE the per-harness loop ===
|
|
22
35
|
const { env, apiKey } = await initContext(opts);
|
|
23
36
|
console.log();
|
|
24
37
|
// Acquire the install lock before touching any shared state. Skipped on
|
|
25
|
-
// dry-run (read-only). The lock excludes a second concurrent
|
|
38
|
+
// dry-run (read-only). The lock excludes a second concurrent uluops-setup
|
|
26
39
|
// from racing the manifest / MCP config / shell-profile / settings.json
|
|
27
|
-
// read-merge-write windows.
|
|
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).
|
|
28
43
|
let lock = null;
|
|
29
44
|
if (!opts.dryRun) {
|
|
30
45
|
lock = await acquireInstallLock();
|
|
31
46
|
}
|
|
32
47
|
try {
|
|
33
|
-
// Load existing manifest for update detection
|
|
34
48
|
const existingManifest = await loadManifest();
|
|
35
|
-
const existingHarness = existingManifest?.harnesses[profile.name];
|
|
36
49
|
if (existingManifest && existingManifest.version !== version) {
|
|
37
50
|
info(`Updating ${chalk.dim(existingManifest.version)} → ${chalk.green(version)}`);
|
|
38
51
|
console.log();
|
|
39
52
|
}
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
});
|
|
42
149
|
console.log();
|
|
43
150
|
}
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
const mcpResult = await configureMcpStep(profile, apiKey, opts);
|
|
49
|
-
const agentsResult = await installAgentsDefs(profile, opts, existingHarness?.agents);
|
|
50
|
-
const commandsResult = await installCommandsDefs(profile, opts, existingHarness?.commands);
|
|
51
|
-
const metricsResult = await configureMetricsStep(profile, opts);
|
|
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.
|
|
52
154
|
const cliResult = await configureCliStep({
|
|
53
155
|
withCli: opts.withCli,
|
|
54
156
|
cli: opts.cli,
|
|
@@ -56,10 +158,14 @@ export async function runSetup(opts) {
|
|
|
56
158
|
apiKey: opts.apiKey,
|
|
57
159
|
dryRun: opts.dryRun,
|
|
58
160
|
});
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
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
|
|
63
169
|
? await configureAgentMetricsCliStep({
|
|
64
170
|
withAgentMetricsCli: opts.withAgentMetricsCli,
|
|
65
171
|
agentMetricsCli: opts.agentMetricsCli,
|
|
@@ -70,23 +176,10 @@ export async function runSetup(opts) {
|
|
|
70
176
|
: null;
|
|
71
177
|
await runHealthCheck(opts);
|
|
72
178
|
const shellModified = await configureShell(env, apiKey, opts);
|
|
73
|
-
// Save manifest
|
|
179
|
+
// Save manifest ONCE at end, aggregating every successful or partial
|
|
180
|
+
// harness entry. Declined harnesses and pre-MCP failures land no entry.
|
|
74
181
|
if (!opts.dryRun) {
|
|
75
182
|
const now = new Date().toISOString();
|
|
76
|
-
const harnessEntry = {
|
|
77
|
-
installedAt: now,
|
|
78
|
-
setupVersion: version,
|
|
79
|
-
mcpScope: opts.scope,
|
|
80
|
-
mcpConfigPath: mcpResult.configPath,
|
|
81
|
-
defsScope: opts.localDefs ? "local" : "global",
|
|
82
|
-
defsPath: opts.localDefs
|
|
83
|
-
? join(await findProjectRoot(), "uluops")
|
|
84
|
-
: profile.paths.home,
|
|
85
|
-
agents: agentsResult.files,
|
|
86
|
-
commands: commandsResult.files,
|
|
87
|
-
hooksInstalled: metricsResult.hookConfigured,
|
|
88
|
-
hooksInstalledVersion: metricsResult.hooksInstalledVersion,
|
|
89
|
-
};
|
|
90
183
|
const manifest = existingManifest ?? {
|
|
91
184
|
version,
|
|
92
185
|
installedAt: now,
|
|
@@ -96,30 +189,63 @@ export async function runSetup(opts) {
|
|
|
96
189
|
manifest.version = version;
|
|
97
190
|
manifest.installedAt = now;
|
|
98
191
|
manifest.shellModified = shellModified || manifest.shellModified;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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).
|
|
103
215
|
if (cliResult && cliResult.installed && !cliResult.alreadyPresent) {
|
|
104
216
|
manifest.cliInstalled = true;
|
|
105
217
|
manifest.cliInstalledVersion = cliResult.version;
|
|
106
218
|
}
|
|
107
|
-
// Same ownership rule for agent-metrics CLI — only manifest a global install
|
|
108
|
-
// we performed ourselves.
|
|
109
219
|
if (agentMetricsCliResult &&
|
|
110
220
|
agentMetricsCliResult.installed &&
|
|
111
221
|
!agentMetricsCliResult.alreadyPresent) {
|
|
112
222
|
manifest.agentMetricsCliInstalled = true;
|
|
113
|
-
manifest.agentMetricsCliInstalledVersion =
|
|
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);
|
|
114
231
|
}
|
|
115
|
-
await saveManifest(manifest);
|
|
116
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.
|
|
117
238
|
await printSetupSummary({
|
|
118
|
-
|
|
119
|
-
agentCount: agentsResult.files.length,
|
|
120
|
-
commandCount: commandsResult.files.length,
|
|
239
|
+
results: perHarnessResults,
|
|
121
240
|
apiKey,
|
|
122
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
|
+
}
|
|
123
249
|
}
|
|
124
250
|
finally {
|
|
125
251
|
if (lock)
|
|
@@ -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>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { loadManifest, deleteManifest, validateManifest, } from "../lib/manifest.js";
|
|
2
|
+
import { loadManifest, saveManifest, deleteManifest, validateManifest, } from "../lib/manifest.js";
|
|
3
3
|
import { acquireInstallLock, } from "../lib/install-lock.js";
|
|
4
4
|
import { uninstallMcp } from "../steps/mcp.js";
|
|
5
5
|
import { uninstallAgents } from "../steps/agents.js";
|
|
6
6
|
import { uninstallCommands } from "../steps/commands.js";
|
|
7
|
+
import { uninstallSkills } from "../steps/skills.js";
|
|
7
8
|
import { removeShellExport } from "../steps/shell.js";
|
|
8
9
|
import { uninstallMetrics } from "../steps/metrics.js";
|
|
9
10
|
import { uninstallCli, CLI_PACKAGE } from "../steps/cli.js";
|
|
@@ -11,6 +12,7 @@ import { uninstallAgentMetricsCli, AGENT_METRICS_PACKAGE, } from "../steps/agent
|
|
|
11
12
|
import { ok, warn, fail, info } from "../lib/display.js";
|
|
12
13
|
import { getVersion } from "../lib/version.js";
|
|
13
14
|
import { getProfile } from "../harnesses/index.js";
|
|
15
|
+
import { resolveUninstallFilter, UninstallFilterError, } from "./uninstall-filter.js";
|
|
14
16
|
export async function runUninstall(opts) {
|
|
15
17
|
const version = await getVersion();
|
|
16
18
|
console.log();
|
|
@@ -45,7 +47,40 @@ export async function runUninstall(opts) {
|
|
|
45
47
|
warn(w);
|
|
46
48
|
console.log();
|
|
47
49
|
}
|
|
48
|
-
|
|
50
|
+
// Resolve the subset of harnesses to uninstall. `null` means "all
|
|
51
|
+
// harnesses in the manifest" (today's no-filter behavior). String[]
|
|
52
|
+
// means "only these — leave the rest of the manifest intact".
|
|
53
|
+
const manifestHarnesses = Object.keys(manifest.harnesses);
|
|
54
|
+
let filter;
|
|
55
|
+
try {
|
|
56
|
+
filter = resolveUninstallFilter({
|
|
57
|
+
harnessArg: opts.harnessArg,
|
|
58
|
+
harnessFromCli: opts.harnessFromCli ?? false,
|
|
59
|
+
allDetected: opts.allDetected ?? false,
|
|
60
|
+
manifestHarnesses,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err instanceof UninstallFilterError) {
|
|
65
|
+
fail(err.message);
|
|
66
|
+
console.log();
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
const toUninstall = filter ?? manifestHarnesses;
|
|
72
|
+
const isFullUninstall = toUninstall.length === manifestHarnesses.length;
|
|
73
|
+
if (!isFullUninstall) {
|
|
74
|
+
info(chalk.dim(`Uninstalling subset (${toUninstall.length} of ${manifestHarnesses.length}): ${toUninstall.join(", ")}`));
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
for (const harnessName of toUninstall) {
|
|
78
|
+
const hm = manifest.harnesses[harnessName];
|
|
79
|
+
if (!hm) {
|
|
80
|
+
// Defense — filter already validated, but keep the guard so a
|
|
81
|
+
// mid-run manifest mutation doesn't crash.
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
49
84
|
let profile;
|
|
50
85
|
try {
|
|
51
86
|
profile = getProfile(harnessName);
|
|
@@ -71,6 +106,15 @@ export async function runUninstall(opts) {
|
|
|
71
106
|
ok(`Would remove ${hm.commands.length} command(s)`);
|
|
72
107
|
}
|
|
73
108
|
}
|
|
109
|
+
if ((hm.skills?.length ?? 0) > 0) {
|
|
110
|
+
if (!opts.dryRun) {
|
|
111
|
+
const skillCount = await uninstallSkills(hm.skills ?? [], hm.defsPath);
|
|
112
|
+
ok(`Removed ${skillCount} skill file(s)`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
ok(`Would remove ${hm.skills?.length ?? 0} skill file(s)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
74
118
|
if (!opts.dryRun) {
|
|
75
119
|
try {
|
|
76
120
|
await uninstallMcp(profile, hm.mcpConfigPath);
|
|
@@ -94,62 +138,94 @@ export async function runUninstall(opts) {
|
|
|
94
138
|
}
|
|
95
139
|
console.log();
|
|
96
140
|
}
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
141
|
+
// Globals (CLI, agent-metrics CLI, shell export) are only removed on
|
|
142
|
+
// a FULL uninstall — they're shared infrastructure that may serve
|
|
143
|
+
// remaining harnesses or other tools after a subset uninstall.
|
|
144
|
+
if (isFullUninstall) {
|
|
145
|
+
if (manifest.cliInstalled) {
|
|
146
|
+
const res = await uninstallCli({ dryRun: opts.dryRun });
|
|
147
|
+
if (opts.dryRun) {
|
|
148
|
+
ok(`Would remove ${CLI_PACKAGE} (global)`);
|
|
149
|
+
}
|
|
150
|
+
else if (res.removed) {
|
|
151
|
+
ok(`Removed ${CLI_PACKAGE} (global)`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
warn(`Could not remove ${CLI_PACKAGE} (global) — try \`npm uninstall -g ${CLI_PACKAGE}\` manually`);
|
|
155
|
+
if (res.error) {
|
|
156
|
+
const oneLine = res.error.split("\n")[0]?.slice(0, 120) ?? "";
|
|
157
|
+
if (oneLine)
|
|
158
|
+
info(` ${oneLine}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
103
161
|
}
|
|
104
|
-
|
|
105
|
-
|
|
162
|
+
if (manifest.agentMetricsCliInstalled) {
|
|
163
|
+
const res = await uninstallAgentMetricsCli({ dryRun: opts.dryRun });
|
|
164
|
+
if (opts.dryRun) {
|
|
165
|
+
ok(`Would remove ${AGENT_METRICS_PACKAGE} (global)`);
|
|
166
|
+
}
|
|
167
|
+
else if (res.removed) {
|
|
168
|
+
ok(`Removed ${AGENT_METRICS_PACKAGE} (global)`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
warn(`Could not remove ${AGENT_METRICS_PACKAGE} (global) — try \`npm uninstall -g ${AGENT_METRICS_PACKAGE}\` manually`);
|
|
172
|
+
if (res.error) {
|
|
173
|
+
const oneLine = res.error.split("\n")[0]?.slice(0, 120) ?? "";
|
|
174
|
+
if (oneLine)
|
|
175
|
+
info(` ${oneLine}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
106
178
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
179
|
+
if (manifest.shellModified) {
|
|
180
|
+
const { getShellProfile } = await import("../lib/paths.js");
|
|
181
|
+
const shellProfile = getShellProfile();
|
|
182
|
+
if (shellProfile && !opts.dryRun) {
|
|
183
|
+
await removeShellExport(shellProfile.path);
|
|
184
|
+
ok(`Removed export from ${shellProfile.path}`);
|
|
185
|
+
}
|
|
186
|
+
else if (shellProfile) {
|
|
187
|
+
ok(`Would remove export from ${shellProfile.path}`);
|
|
113
188
|
}
|
|
114
189
|
}
|
|
115
190
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
191
|
+
else {
|
|
192
|
+
info(chalk.dim(`Preserving global @uluops/cli, @uluops/agent-metrics, and shell export — subset uninstall does not touch shared infrastructure`));
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
// Manifest update / delete logic:
|
|
196
|
+
// full uninstall → delete the manifest entirely (today's behavior)
|
|
197
|
+
// subset uninstall, entries remain → save the trimmed manifest
|
|
198
|
+
// subset uninstall, no entries remain → delete (manifest loader
|
|
199
|
+
// would reject an empty-harnesses file)
|
|
200
|
+
if (!opts.dryRun) {
|
|
201
|
+
if (isFullUninstall) {
|
|
202
|
+
await deleteManifest();
|
|
203
|
+
ok("Manifest deleted");
|
|
124
204
|
}
|
|
125
205
|
else {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
206
|
+
for (const name of toUninstall) {
|
|
207
|
+
delete manifest.harnesses[name];
|
|
208
|
+
}
|
|
209
|
+
const remaining = Object.keys(manifest.harnesses).length;
|
|
210
|
+
if (remaining > 0) {
|
|
211
|
+
await saveManifest(manifest);
|
|
212
|
+
ok(`Manifest updated — ${remaining} harness(es) remain: ${Object.keys(manifest.harnesses).join(", ")}`);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
await deleteManifest();
|
|
216
|
+
ok("Manifest deleted (no harnesses remain)");
|
|
131
217
|
}
|
|
132
218
|
}
|
|
133
219
|
}
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
const shellProfile = getShellProfile();
|
|
138
|
-
if (shellProfile && !opts.dryRun) {
|
|
139
|
-
await removeShellExport(shellProfile.path);
|
|
140
|
-
ok(`Removed export from ${shellProfile.path}`);
|
|
141
|
-
}
|
|
142
|
-
else if (shellProfile) {
|
|
143
|
-
ok(`Would remove export from ${shellProfile.path}`);
|
|
144
|
-
}
|
|
220
|
+
console.log();
|
|
221
|
+
if (isFullUninstall) {
|
|
222
|
+
info("UluOps has been removed. Restart your harness to complete.");
|
|
145
223
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
224
|
+
else {
|
|
225
|
+
const removed = toUninstall.join(", ");
|
|
226
|
+
info(`Removed UluOps from ${removed}. Restart the affected harness(es) to complete.`);
|
|
149
227
|
}
|
|
150
228
|
console.log();
|
|
151
|
-
info("UluOps has been removed. Restart your harness to complete.");
|
|
152
|
-
console.log();
|
|
153
229
|
}
|
|
154
230
|
finally {
|
|
155
231
|
if (lock)
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codex Harness Profile
|
|
2
|
+
* Codex Harness Profile
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Codex uses TOML config with `mcp_servers` key (nested tables).
|
|
4
|
+
* Codex uses TOML config with `mcp_servers` nested tables.
|
|
6
5
|
* Agent definitions are TOML, not markdown.
|
|
7
|
-
* Skills
|
|
8
|
-
*
|
|
9
|
-
* NOT YET TESTED with UluOps agents. McpConfigStrategy throws
|
|
10
|
-
* until integration testing is complete.
|
|
11
|
-
*
|
|
12
|
-
* Will require `smol-toml` dependency when fully implemented.
|
|
6
|
+
* Skills live under ~/.codex/skills and are the preferred Codex-native
|
|
7
|
+
* surface for UluOps operator workflows.
|
|
13
8
|
*/
|
|
14
|
-
import type
|
|
9
|
+
import { type HarnessProfile } from "./types.js";
|
|
15
10
|
export declare const codexProfile: HarnessProfile;
|