@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.
Files changed (59) hide show
  1. package/README.md +91 -9
  2. package/assets/codex/skills/uluops-operator/SKILL.md +159 -0
  3. package/dist/cli/select-harnesses.d.ts +91 -0
  4. package/dist/cli/select-harnesses.js +108 -0
  5. package/dist/cli.js +90 -37
  6. package/dist/commands/errors.d.ts +24 -0
  7. package/dist/commands/errors.js +28 -0
  8. package/dist/commands/helpers.d.ts +32 -0
  9. package/dist/commands/helpers.js +146 -3
  10. package/dist/commands/per-harness.d.ts +64 -0
  11. package/dist/commands/per-harness.js +37 -0
  12. package/dist/commands/setup.d.ts +7 -3
  13. package/dist/commands/setup.js +232 -71
  14. package/dist/commands/uninstall-filter.d.ts +36 -0
  15. package/dist/commands/uninstall-filter.js +69 -0
  16. package/dist/commands/uninstall.d.ts +12 -2
  17. package/dist/commands/uninstall.js +190 -82
  18. package/dist/harnesses/codex.d.ts +5 -10
  19. package/dist/harnesses/codex.js +89 -21
  20. package/dist/harnesses/index.js +6 -1
  21. package/dist/harnesses/opencode.d.ts +8 -0
  22. package/dist/harnesses/opencode.js +24 -1
  23. package/dist/harnesses/types.d.ts +2 -0
  24. package/dist/harnesses/types.js +5 -1
  25. package/dist/lib/atomic-write.js +10 -2
  26. package/dist/lib/config-merger.d.ts +6 -3
  27. package/dist/lib/config-merger.js +50 -7
  28. package/dist/lib/display.d.ts +21 -5
  29. package/dist/lib/display.js +118 -13
  30. package/dist/lib/file-ops.d.ts +13 -5
  31. package/dist/lib/file-ops.js +34 -34
  32. package/dist/lib/install-lock.d.ts +47 -0
  33. package/dist/lib/install-lock.js +251 -0
  34. package/dist/lib/json-guards.d.ts +22 -0
  35. package/dist/lib/json-guards.js +33 -0
  36. package/dist/lib/manifest.d.ts +28 -0
  37. package/dist/lib/manifest.js +61 -12
  38. package/dist/lib/paths.d.ts +2 -17
  39. package/dist/lib/paths.js +4 -19
  40. package/dist/lib/settings-merger.js +3 -1
  41. package/dist/steps/agent-metrics-cli.d.ts +72 -0
  42. package/dist/steps/agent-metrics-cli.js +147 -0
  43. package/dist/steps/agents.d.ts +11 -0
  44. package/dist/steps/agents.js +30 -25
  45. package/dist/steps/auth.d.ts +13 -0
  46. package/dist/steps/auth.js +61 -5
  47. package/dist/steps/cli.d.ts +6 -0
  48. package/dist/steps/cli.js +29 -10
  49. package/dist/steps/commands.d.ts +10 -0
  50. package/dist/steps/commands.js +31 -30
  51. package/dist/steps/detect.js +15 -1
  52. package/dist/steps/mcp.js +1 -8
  53. package/dist/steps/metrics.js +10 -3
  54. package/dist/steps/shell.js +3 -13
  55. package/dist/steps/signup.js +14 -1
  56. package/dist/steps/skills.d.ts +14 -0
  57. package/dist/steps/skills.js +95 -0
  58. package/dist/steps/verify.js +195 -91
  59. package/package.json +3 -2
@@ -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
- /** Verify a single harness entry, appending results to checks. Returns false if any check fails. */
8
- async function verifyHarness(harnessName, hm, checks) {
9
- let allOk = true;
10
- let profile;
11
- try {
12
- profile = getProfile(harnessName);
13
- }
14
- catch {
15
- checks.push({ label: `Harness: ${harnessName}`, passed: false, detail: "Unknown harness in manifest" });
16
- return false;
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
- // If config was modified AFTER install, it's a proxy for the harness having read it.
24
- // We add a 100ms buffer to handle near-simultaneous writes.
25
- const isReady = configTime > (installedTime + 100);
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
- // If we can't stat it, we'll catch the error in the MCP config check below.
39
+ // Stat failure surfaces in checkMcpConfig as a "cannot read" error;
40
+ // suppress here to avoid double-reporting.
41
+ return true;
34
42
  }
35
- // 2. MCP config
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({ label: `[${profile.displayName}] MCP config present in ${hm.mcpConfigPath} (2 servers)`, passed: true });
40
- }
41
- else {
42
- checks.push({ label: `[${profile.displayName}] MCP config`, passed: false, detail: "UluOps servers not found in config" });
43
- allOk = false;
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({ label: `[${profile.displayName}] MCP config`, passed: false, detail: `Cannot read ${hm.mcpConfigPath}` });
48
- allOk = false;
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
- // Agent files
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: found === hm.agents.length,
58
- detail: found < hm.agents.length ? `Missing ${hm.agents.length - found} agent(s)` : undefined,
80
+ passed: ok,
81
+ detail: ok ? undefined : `Missing ${hm.agents.length - found} agent(s)`,
59
82
  });
60
- if (found < hm.agents.length)
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}] ${found}/${hm.commands.length} commands`,
82
- passed: found === hm.commands.length,
83
- detail: found < hm.commands.length ? `Missing ${hm.commands.length - found} command(s)` : undefined,
87
+ label: `[${profile.displayName}] Agent files`,
88
+ passed: false,
89
+ detail: `Directory not found: ${agentsDir}`,
84
90
  });
85
- if (found < hm.commands.length)
86
- allOk = false;
91
+ return false;
87
92
  }
88
- // Hooks
89
- if (hm.hooksInstalled && profile.hooks && profile.paths.settingsPath) {
90
- const hookPresent = await profile.hooks.check(profile.paths.settingsPath);
91
- let hookFilePresent = false;
92
- if (profile.paths.toolsDir) {
93
- try {
94
- await access(join(profile.paths.toolsDir, "dist", "hook.js"));
95
- hookFilePresent = true;
96
- }
97
- catch { /* Missing */ }
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
- if (hookPresent && hookFilePresent) {
100
- // Existence is necessary but not sufficient — check version drift against the
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
- else {
122
- const missing = [
123
- ...(!hookPresent ? ["hook not in settings"] : []),
124
- ...(!hookFilePresent ? ["hook.js not found"] : []),
125
- ].join(", ");
126
- checks.push({ label: `[${profile.displayName}] Agent metrics`, passed: false, detail: missing });
127
- allOk = false;
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
- return allOk;
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
- const data = (await res.json());
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${data.email ? ` (user: ${data.email})` : ""}`,
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.6.5",
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": "3.2.4"
61
+ "vitest": "4.1.8"
61
62
  }
62
63
  }