@uluops/setup 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +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 +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.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/steps/verify.js
CHANGED
|
@@ -4,130 +4,217 @@ import { loadManifest } from "../lib/manifest.js";
|
|
|
4
4
|
import { getHealthTimeout } from "../lib/health.js";
|
|
5
5
|
import { getProfile } from "../harnesses/index.js";
|
|
6
6
|
import { readInstalledMetricsVersion } from "./metrics.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// 1. Readiness Check (Harness Restart)
|
|
7
|
+
import { extractEmail } from "../lib/json-guards.js";
|
|
8
|
+
/**
|
|
9
|
+
* Readiness probe: did the harness pick up the config after install?
|
|
10
|
+
* The proxy signal is `configFile.mtimeMs > installedAt + 100ms` — a config
|
|
11
|
+
* touched after install means the harness wrote it back during startup.
|
|
12
|
+
* Always passes (true) — readiness state is reported in `detail`, not the
|
|
13
|
+
* boolean — so the caller never fails the run on this alone. Only the
|
|
14
|
+
* "installedAt is corrupt" branch returns false, because that indicates
|
|
15
|
+
* the manifest itself is broken.
|
|
16
|
+
*/
|
|
17
|
+
async function checkReadiness(profile, hm, checks) {
|
|
19
18
|
try {
|
|
20
19
|
const configStat = await stat(hm.mcpConfigPath);
|
|
21
20
|
const installedTime = new Date(hm.installedAt).getTime();
|
|
22
21
|
const configTime = configStat.mtimeMs;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
if (Number.isNaN(installedTime)) {
|
|
23
|
+
checks.push({
|
|
24
|
+
label: `[${profile.displayName}] Readiness`,
|
|
25
|
+
passed: false,
|
|
26
|
+
detail: `Cannot determine readiness — manifest installedAt is invalid (${hm.installedAt})`,
|
|
27
|
+
});
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const isReady = configTime > installedTime + 100;
|
|
26
31
|
checks.push({
|
|
27
32
|
label: `[${profile.displayName}] Readiness`,
|
|
28
33
|
passed: true,
|
|
29
34
|
detail: isReady ? "Active (loaded)" : "Waiting for restart",
|
|
30
35
|
});
|
|
36
|
+
return true;
|
|
31
37
|
}
|
|
32
38
|
catch {
|
|
33
|
-
//
|
|
39
|
+
// Stat failure surfaces in checkMcpConfig as a "cannot read" error;
|
|
40
|
+
// suppress here to avoid double-reporting.
|
|
41
|
+
return true;
|
|
34
42
|
}
|
|
35
|
-
|
|
43
|
+
}
|
|
44
|
+
/** MCP config present and contains UluOps server entries. */
|
|
45
|
+
async function checkMcpConfig(profile, hm, checks) {
|
|
36
46
|
try {
|
|
37
47
|
const config = await profile.mcpConfig.read(hm.mcpConfigPath);
|
|
38
48
|
if (profile.mcpConfig.check(config)) {
|
|
39
|
-
checks.push({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
checks.push({
|
|
50
|
+
label: `[${profile.displayName}] MCP config present in ${hm.mcpConfigPath} (2 servers)`,
|
|
51
|
+
passed: true,
|
|
52
|
+
});
|
|
53
|
+
return true;
|
|
44
54
|
}
|
|
55
|
+
checks.push({
|
|
56
|
+
label: `[${profile.displayName}] MCP config`,
|
|
57
|
+
passed: false,
|
|
58
|
+
detail: "UluOps servers not found in config",
|
|
59
|
+
});
|
|
60
|
+
return false;
|
|
45
61
|
}
|
|
46
62
|
catch {
|
|
47
|
-
checks.push({
|
|
48
|
-
|
|
63
|
+
checks.push({
|
|
64
|
+
label: `[${profile.displayName}] MCP config`,
|
|
65
|
+
passed: false,
|
|
66
|
+
detail: `Cannot read ${hm.mcpConfigPath}`,
|
|
67
|
+
});
|
|
68
|
+
return false;
|
|
49
69
|
}
|
|
50
|
-
|
|
70
|
+
}
|
|
71
|
+
/** Agent files: every file recorded in the manifest exists on disk. */
|
|
72
|
+
async function checkAgents(profile, hm, checks) {
|
|
51
73
|
const agentsDir = join(hm.defsPath, "agents");
|
|
52
74
|
try {
|
|
53
75
|
const agentFiles = await readdir(agentsDir);
|
|
54
76
|
const found = hm.agents.filter((a) => agentFiles.includes(a)).length;
|
|
77
|
+
const ok = found === hm.agents.length;
|
|
55
78
|
checks.push({
|
|
56
79
|
label: `[${profile.displayName}] ${found}/${hm.agents.length} agents in ${agentsDir}`,
|
|
57
|
-
passed:
|
|
58
|
-
detail:
|
|
80
|
+
passed: ok,
|
|
81
|
+
detail: ok ? undefined : `Missing ${hm.agents.length - found} agent(s)`,
|
|
59
82
|
});
|
|
60
|
-
|
|
61
|
-
allOk = false;
|
|
83
|
+
return ok;
|
|
62
84
|
}
|
|
63
85
|
catch {
|
|
64
|
-
checks.push({ label: `[${profile.displayName}] Agent files`, passed: false, detail: `Directory not found: ${agentsDir}` });
|
|
65
|
-
allOk = false;
|
|
66
|
-
}
|
|
67
|
-
// Command files
|
|
68
|
-
if (hm.commands.length > 0) {
|
|
69
|
-
const commandsDir = join(hm.defsPath, "commands");
|
|
70
|
-
const cmdResults = await Promise.all(hm.commands.map(async (cmd) => {
|
|
71
|
-
try {
|
|
72
|
-
await access(join(commandsDir, cmd));
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
}));
|
|
79
|
-
const found = cmdResults.filter(Boolean).length;
|
|
80
86
|
checks.push({
|
|
81
|
-
label: `[${profile.displayName}]
|
|
82
|
-
passed:
|
|
83
|
-
detail:
|
|
87
|
+
label: `[${profile.displayName}] Agent files`,
|
|
88
|
+
passed: false,
|
|
89
|
+
detail: `Directory not found: ${agentsDir}`,
|
|
84
90
|
});
|
|
85
|
-
|
|
86
|
-
allOk = false;
|
|
91
|
+
return false;
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Command files: every command recorded in the manifest exists on disk.
|
|
96
|
+
* No-op (returns true) when the manifest records zero commands — keeps the
|
|
97
|
+
* "(0/0 commands)" noise out of the report for harnesses without commands.
|
|
98
|
+
*/
|
|
99
|
+
async function checkCommands(profile, hm, checks) {
|
|
100
|
+
if (hm.commands.length === 0)
|
|
101
|
+
return true;
|
|
102
|
+
const commandsDir = join(hm.defsPath, "commands");
|
|
103
|
+
const cmdResults = await Promise.all(hm.commands.map(async (cmd) => {
|
|
104
|
+
try {
|
|
105
|
+
await access(join(commandsDir, cmd));
|
|
106
|
+
return true;
|
|
98
107
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// currently-resolvable agent-metrics. Without this, verify cannot detect that
|
|
102
|
-
// a fresh setup release is shipping while the installed hook is stale.
|
|
103
|
-
const installedVersion = profile.paths.toolsDir
|
|
104
|
-
? await readInstalledMetricsVersion(profile.paths.toolsDir)
|
|
105
|
-
: null;
|
|
106
|
-
const recordedVersion = hm.hooksInstalledVersion ?? null;
|
|
107
|
-
const detail = installedVersion
|
|
108
|
-
? `v${installedVersion}${recordedVersion && recordedVersion !== installedVersion ? ` (manifest records v${recordedVersion} — out of sync)` : ""}`
|
|
109
|
-
: "version unknown";
|
|
110
|
-
checks.push({
|
|
111
|
-
label: `[${profile.displayName}] Agent metrics hook configured`,
|
|
112
|
-
passed: true,
|
|
113
|
-
detail,
|
|
114
|
-
});
|
|
115
|
-
if (recordedVersion && installedVersion && recordedVersion !== installedVersion) {
|
|
116
|
-
// Drift between manifest record and on-disk hook copy — not a hard failure
|
|
117
|
-
// because verify is read-only, but surface it so the user re-runs setup.
|
|
118
|
-
allOk = false;
|
|
119
|
-
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
120
110
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
111
|
+
}));
|
|
112
|
+
const found = cmdResults.filter(Boolean).length;
|
|
113
|
+
const ok = found === hm.commands.length;
|
|
114
|
+
checks.push({
|
|
115
|
+
label: `[${profile.displayName}] ${found}/${hm.commands.length} commands`,
|
|
116
|
+
passed: ok,
|
|
117
|
+
detail: ok ? undefined : `Missing ${hm.commands.length - found} command(s)`,
|
|
118
|
+
});
|
|
119
|
+
return ok;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Agent-metrics hook: present in settings + hook.js exists + manifest version
|
|
123
|
+
* matches on-disk version. Each piece is necessary; only all three together
|
|
124
|
+
* means the hook will actually run on the next harness invocation.
|
|
125
|
+
*
|
|
126
|
+
* No-op (returns true) when the manifest says hooks weren't installed or the
|
|
127
|
+
* profile doesn't expose a hook strategy. Version drift returns false even
|
|
128
|
+
* though hooks are technically "present" — drift means setup was upgraded
|
|
129
|
+
* but never re-ran, which silently ships a stale hook against a fresh CLI.
|
|
130
|
+
*/
|
|
131
|
+
async function checkHooks(profile, hm, checks) {
|
|
132
|
+
if (!hm.hooksInstalled || !profile.hooks || !profile.paths.settingsPath) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
const hookPresent = await profile.hooks.check(profile.paths.settingsPath);
|
|
136
|
+
let hookFilePresent = false;
|
|
137
|
+
if (profile.paths.toolsDir) {
|
|
138
|
+
try {
|
|
139
|
+
await access(join(profile.paths.toolsDir, "dist", "hook.js"));
|
|
140
|
+
hookFilePresent = true;
|
|
128
141
|
}
|
|
142
|
+
catch {
|
|
143
|
+
/* Missing */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!hookPresent || !hookFilePresent) {
|
|
147
|
+
const missing = [
|
|
148
|
+
...(!hookPresent ? ["hook not in settings"] : []),
|
|
149
|
+
...(!hookFilePresent ? ["hook.js not found"] : []),
|
|
150
|
+
].join(", ");
|
|
151
|
+
checks.push({
|
|
152
|
+
label: `[${profile.displayName}] Agent metrics`,
|
|
153
|
+
passed: false,
|
|
154
|
+
detail: missing,
|
|
155
|
+
});
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const installedVersion = profile.paths.toolsDir
|
|
159
|
+
? await readInstalledMetricsVersion(profile.paths.toolsDir)
|
|
160
|
+
: null;
|
|
161
|
+
const recordedVersion = hm.hooksInstalledVersion ?? null;
|
|
162
|
+
const drift = !!recordedVersion &&
|
|
163
|
+
!!installedVersion &&
|
|
164
|
+
recordedVersion !== installedVersion;
|
|
165
|
+
const detail = installedVersion
|
|
166
|
+
? `v${installedVersion}${drift ? ` (manifest records v${recordedVersion} — out of sync)` : ""}`
|
|
167
|
+
: "version unknown";
|
|
168
|
+
checks.push({
|
|
169
|
+
label: `[${profile.displayName}] Agent metrics hook configured`,
|
|
170
|
+
passed: true,
|
|
171
|
+
detail,
|
|
172
|
+
});
|
|
173
|
+
// Drift is reported as passed=true (verify is read-only) but flips allOk
|
|
174
|
+
// so the user knows a re-run is needed.
|
|
175
|
+
return !drift;
|
|
176
|
+
}
|
|
177
|
+
/** Verify a single harness entry, appending results to checks. Returns false if any check fails. */
|
|
178
|
+
async function verifyHarness(harnessName, hm, checks) {
|
|
179
|
+
let profile;
|
|
180
|
+
try {
|
|
181
|
+
profile = getProfile(harnessName);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
checks.push({
|
|
185
|
+
label: `Harness: ${harnessName}`,
|
|
186
|
+
passed: false,
|
|
187
|
+
detail: "Unknown harness in manifest",
|
|
188
|
+
});
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
// Partial install gets surfaced before any per-file check runs. The
|
|
192
|
+
// recorded file lists are honest (post-Phase-0.5 contract) so the
|
|
193
|
+
// file-existence checks will pass, but the user needs to know the
|
|
194
|
+
// install never completed and a re-run is required. Reported as a
|
|
195
|
+
// failed check so verify exits 1 — partial state is not "passes". The
|
|
196
|
+
// remaining checks still run so the report shows exactly what landed
|
|
197
|
+
// before the step threw. (Spec §7.6.4.)
|
|
198
|
+
let partialOk = true;
|
|
199
|
+
if (hm.partial) {
|
|
200
|
+
checks.push({
|
|
201
|
+
label: `[${profile.displayName}] partial install — failed at "${hm.partial}"`,
|
|
202
|
+
passed: false,
|
|
203
|
+
detail: `Re-run: npx @uluops/setup --harness ${harnessName}`,
|
|
204
|
+
});
|
|
205
|
+
partialOk = false;
|
|
129
206
|
}
|
|
130
|
-
|
|
207
|
+
// Run sequentially: the checks share a `checks` array and the user-visible
|
|
208
|
+
// report depends on insertion order (readiness → mcp → agents → commands →
|
|
209
|
+
// hooks). Promise.all would scramble that order. AND together at the end so
|
|
210
|
+
// any single failure flips the harness result while every check still gets
|
|
211
|
+
// to push its row.
|
|
212
|
+
const readinessOk = await checkReadiness(profile, hm, checks);
|
|
213
|
+
const mcpOk = await checkMcpConfig(profile, hm, checks);
|
|
214
|
+
const agentsOk = await checkAgents(profile, hm, checks);
|
|
215
|
+
const commandsOk = await checkCommands(profile, hm, checks);
|
|
216
|
+
const hooksOk = await checkHooks(profile, hm, checks);
|
|
217
|
+
return partialOk && readinessOk && mcpOk && agentsOk && commandsOk && hooksOk;
|
|
131
218
|
}
|
|
132
219
|
/** Run all verification checks against the current installation and return structured results. */
|
|
133
220
|
export async function verify() {
|
|
@@ -162,9 +249,26 @@ export async function verify() {
|
|
|
162
249
|
signal: AbortSignal.timeout(getHealthTimeout()),
|
|
163
250
|
});
|
|
164
251
|
if (res.ok) {
|
|
165
|
-
|
|
252
|
+
// Decode via the shared envelope guard. Bare `as { email?: string }`
|
|
253
|
+
// would silently produce a "key valid (no user)" label on HTML error
|
|
254
|
+
// pages or schema drift. extractEmail throws when the body is not an
|
|
255
|
+
// object at all (surfaced below as a check failure) and returns null
|
|
256
|
+
// when data/email are absent or wrong-typed.
|
|
257
|
+
let email;
|
|
258
|
+
try {
|
|
259
|
+
email = extractEmail(await res.json());
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
checks.push({
|
|
263
|
+
label: "API key valid",
|
|
264
|
+
passed: false,
|
|
265
|
+
detail: err instanceof Error ? err.message : "Unexpected response shape",
|
|
266
|
+
});
|
|
267
|
+
allOk = false;
|
|
268
|
+
return { ok: allOk, checks };
|
|
269
|
+
}
|
|
166
270
|
checks.push({
|
|
167
|
-
label: `API key valid${
|
|
271
|
+
label: `API key valid${email ? ` (user: ${email})` : ""}`,
|
|
168
272
|
passed: true,
|
|
169
273
|
});
|
|
170
274
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uluops/setup",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Zero-friction installer for UluOps agentic harnesses",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist/cli.js",
|
|
31
31
|
"dist/cli.d.ts",
|
|
32
|
+
"dist/cli/**",
|
|
32
33
|
"dist/lib/**",
|
|
33
34
|
"dist/steps/**",
|
|
34
35
|
"dist/harnesses/**",
|
|
@@ -57,6 +58,6 @@
|
|
|
57
58
|
"@types/node": "22.19.15",
|
|
58
59
|
"tsx": "4.21.0",
|
|
59
60
|
"typescript": "5.9.3",
|
|
60
|
-
"vitest": "
|
|
61
|
+
"vitest": "4.1.8"
|
|
61
62
|
}
|
|
62
63
|
}
|