@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.
Files changed (58) hide show
  1. package/README.md +94 -10
  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 +78 -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 +7 -0
  9. package/dist/commands/helpers.js +80 -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 +5 -3
  13. package/dist/commands/setup.js +174 -48
  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 +121 -45
  18. package/dist/harnesses/codex.d.ts +5 -10
  19. package/dist/harnesses/codex.js +212 -22
  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 +8 -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.js +11 -1
  33. package/dist/lib/json-guards.d.ts +22 -0
  34. package/dist/lib/json-guards.js +33 -0
  35. package/dist/lib/manifest.d.ts +20 -0
  36. package/dist/lib/manifest.js +61 -12
  37. package/dist/lib/paths.d.ts +0 -17
  38. package/dist/lib/paths.js +0 -19
  39. package/dist/lib/settings-merger.js +3 -1
  40. package/dist/steps/agent-metrics-cli.d.ts +9 -1
  41. package/dist/steps/agent-metrics-cli.js +66 -20
  42. package/dist/steps/agents.d.ts +11 -0
  43. package/dist/steps/agents.js +30 -25
  44. package/dist/steps/auth.d.ts +13 -0
  45. package/dist/steps/auth.js +61 -5
  46. package/dist/steps/cli.d.ts +6 -0
  47. package/dist/steps/cli.js +29 -10
  48. package/dist/steps/commands.d.ts +10 -0
  49. package/dist/steps/commands.js +31 -30
  50. package/dist/steps/detect.js +15 -1
  51. package/dist/steps/mcp.js +1 -8
  52. package/dist/steps/metrics.js +10 -3
  53. package/dist/steps/shell.js +3 -13
  54. package/dist/steps/signup.js +14 -1
  55. package/dist/steps/skills.d.ts +14 -0
  56. package/dist/steps/skills.js +95 -0
  57. package/dist/steps/verify.js +195 -91
  58. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -3,11 +3,13 @@ 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 { resolveHarnessName, listHarnesses, detectHarnesses, HarnessNotTestedError, } from "./harnesses/index.js";
6
+ import { listHarnesses, detectHarnesses, 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";
10
10
  import { runVerify } from "./commands/verify.js";
11
+ import { ConflictRejectedError } from "./commands/errors.js";
12
+ import { selectHarnesses, HarnessSelectionError, } from "./cli/select-harnesses.js";
11
13
  async function main() {
12
14
  const version = await getVersion();
13
15
  const program = new Command()
@@ -16,7 +18,8 @@ async function main() {
16
18
  .version(version)
17
19
  .option("--api-key <key>", "API key (skip prompt)")
18
20
  .option("--signup", "Create a new account (email + password, no browser)")
19
- .option("--harness <name>", `Target harness: ${listHarnesses().join(", ")} (aliases: claude, oc, gemini)`, "claude-code")
21
+ .option("--harness <name>", `Target harness: ${listHarnesses().join(", ")} (aliases: claude, oc, gemini). Accepts comma-separated subset ("claude-code,codex") or "all" to install into every detected stable harness.`, "claude-code")
22
+ .option("--all-detected", "Install into every detected stable harness. Canonical form; equivalent to --harness all.", false)
20
23
  .option("--scope <mode>", 'MCP connectivity scope: "global" (~/.claude.json) or "local" (.mcp.json)', "global")
21
24
  .option("--local-defs", "Save agents/commands locally (./uluops/) for project isolation", false)
22
25
  .option("--shell", "Write API key export to shell profile", false)
@@ -46,7 +49,17 @@ async function main() {
46
49
  return;
47
50
  }
48
51
  if (opts.uninstall) {
49
- await runUninstall({ dryRun: opts.dryRun });
52
+ // --harness / --all-detected on uninstall acts as a FILTER over the
53
+ // manifest's recorded harnesses, not a selection over what's detected
54
+ // on disk. Pass through the raw flag values; runUninstall delegates
55
+ // parsing + validation to resolveUninstallFilter (which mirrors the
56
+ // install-side conflict detection).
57
+ await runUninstall({
58
+ dryRun: opts.dryRun,
59
+ harnessArg: opts.harness,
60
+ harnessFromCli: program.getOptionValueSource("harness") === "cli",
61
+ allDetected: opts.allDetected,
62
+ });
50
63
  return;
51
64
  }
52
65
  if (opts.scope && opts.scope !== "local" && opts.scope !== "global") {
@@ -54,42 +67,59 @@ async function main() {
54
67
  process.exit(1);
55
68
  }
56
69
  const scope = opts.scope === "local" ? "local" : "global";
57
- // Resolve harness: if --harness was passed explicitly, honor it as-is.
58
- // Otherwise auto-detect — single match wins silently, multiple matches
59
- // prompt the user, no matches falls back to the default (claude-code)
60
- // to preserve the landing-page "just run npx @uluops/setup" promise.
61
- const harnessExplicit = program.getOptionValueSource("harness") === "cli";
62
- let harnessName;
63
- if (harnessExplicit) {
64
- harnessName = resolveHarnessName(opts.harness);
65
- }
66
- else {
67
- const detected = detectHarnesses();
68
- if (detected.length === 1) {
69
- harnessName = detected[0].name;
70
- if (harnessName !== "claude-code") {
71
- info(chalk.dim(`Detected ${chalk.cyan(detected[0].displayName)} — using as target (pass --harness to override)`));
72
- }
73
- }
74
- else if (detected.length > 1) {
75
- const isInteractive = !opts.yes && !opts.apiKey && !process.env["ULUOPS_API_KEY"] && process.stdin.isTTY;
76
- if (isInteractive) {
77
- const { select } = await import("@inquirer/prompts");
78
- harnessName = await select({
79
- message: "Multiple harnesses detected — which one are you setting up?",
80
- choices: detected.map((p) => ({ name: p.displayName, value: p.name })),
81
- default: detected[0].name,
70
+ // Resolve harness selection through the pure selection module so the
71
+ // matrix of (--harness, --all-detected, detection count, TTY) lives in
72
+ // one tested place. cli.ts only wires the prompt and emit-info callbacks.
73
+ const detected = detectHarnesses();
74
+ const isInteractive = !opts.yes &&
75
+ !opts.apiKey &&
76
+ !process.env["ULUOPS_API_KEY"] &&
77
+ !!process.stdin.isTTY;
78
+ let harnessNames;
79
+ try {
80
+ harnessNames = await selectHarnesses({
81
+ harnessArg: opts.harness,
82
+ harnessFromCli: program.getOptionValueSource("harness") === "cli",
83
+ allDetected: opts.allDetected,
84
+ detected,
85
+ defaultHarness: "claude-code",
86
+ isInteractive,
87
+ emitInfo: (msg) => info(chalk.dim(msg)),
88
+ promptCheckbox: async (profiles) => {
89
+ const { checkbox } = await import("@inquirer/prompts");
90
+ const chosen = await checkbox({
91
+ message: "Multiple harnesses detected. Which would you like to install into?",
92
+ instructions: " (use space to toggle, enter to confirm)",
93
+ choices: profiles.map((p) => ({
94
+ name: p.displayName,
95
+ value: p.name,
96
+ checked: true,
97
+ })),
82
98
  });
83
99
  console.log();
84
- }
85
- else {
86
- harnessName = detected[0].name;
87
- info(chalk.dim(`Multiple harnesses detected (${detected.map((p) => p.displayName).join(", ")}); defaulting to ${chalk.cyan(detected[0].displayName)} — pass --harness to choose`));
88
- }
89
- }
90
- else {
91
- harnessName = resolveHarnessName(opts.harness);
100
+ return chosen;
101
+ },
102
+ });
103
+ }
104
+ catch (err) {
105
+ if (err instanceof HarnessSelectionError) {
106
+ console.error(chalk.red(`\n ${err.message}\n`));
107
+ process.exit(1);
92
108
  }
109
+ throw err;
110
+ }
111
+ // Resolve every name through getProfile up front. This catches typos
112
+ // (e.g. `--harness claude-cod,codex`) before any state is touched and
113
+ // surfaces the canonical "Available: ..." error. Aliases ('claude' →
114
+ // 'claude-code') are normalized here as a side effect.
115
+ let resolvedHarnesses;
116
+ try {
117
+ resolvedHarnesses = harnessNames.map((n) => getProfile(n).name);
118
+ }
119
+ catch (err) {
120
+ const msg = err instanceof Error ? err.message : String(err);
121
+ console.error(chalk.red(`\n ${msg}\n`));
122
+ process.exit(1);
93
123
  }
94
124
  await runSetup({
95
125
  apiKey: opts.apiKey,
@@ -104,10 +134,21 @@ async function main() {
104
134
  skipValidation: opts.skipValidation,
105
135
  dryRun: opts.dryRun,
106
136
  yes: opts.yes,
107
- harness: harnessName,
137
+ harnesses: resolvedHarnesses,
108
138
  });
109
139
  }
110
140
  main().catch((err) => {
141
+ if (err instanceof ConflictRejectedError) {
142
+ // Single-harness path: user declined the conflict prompt. Exit 0
143
+ // (today's UX — no error). The multi-harness orchestrator catches
144
+ // this inside its loop and never lets it bubble to here when there
145
+ // are siblings to install; this handler only fires when the declined
146
+ // harness was the only target (the loop continues, finds nothing
147
+ // installed, and... actually the loop swallows it before propagating,
148
+ // so this handler is defense-in-depth for any future caller of
149
+ // checkConflicts outside the loop). Either way: exit 0, no message.
150
+ process.exit(0);
151
+ }
111
152
  if (err instanceof HarnessNotTestedError) {
112
153
  console.error(chalk.yellow(`\n ${err.message}\n`));
113
154
  process.exit(1);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Typed setup errors. Each class corresponds to a distinct failure category
3
+ * the per-harness orchestrator (or the top-level CLI handler) needs to
4
+ * discriminate to choose the right exit code, message, and continue/abort
5
+ * decision.
6
+ *
7
+ * Discrimination at the type level — not on error.message — because messages
8
+ * are user-facing and will drift; constructors are stable.
9
+ */
10
+ /**
11
+ * Raised when the user declines a "Continue?" conflict prompt during a
12
+ * first-install for a harness. The single-harness path catches this in
13
+ * the CLI top-level handler and exits 0 cleanly (today's UX). The
14
+ * multi-harness path catches it in the per-harness loop, marks that
15
+ * harness as `declined`, and continues with siblings.
16
+ *
17
+ * NOT an operational failure — this is a deliberate user choice. The
18
+ * exit-code classifier (spec §7.5) treats `declined` as exit 0 unless
19
+ * combined with an actual `failed` harness in the same run.
20
+ */
21
+ export declare class ConflictRejectedError extends Error {
22
+ readonly harnessName: string;
23
+ constructor(harnessName: string);
24
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Typed setup errors. Each class corresponds to a distinct failure category
3
+ * the per-harness orchestrator (or the top-level CLI handler) needs to
4
+ * discriminate to choose the right exit code, message, and continue/abort
5
+ * decision.
6
+ *
7
+ * Discrimination at the type level — not on error.message — because messages
8
+ * are user-facing and will drift; constructors are stable.
9
+ */
10
+ /**
11
+ * Raised when the user declines a "Continue?" conflict prompt during a
12
+ * first-install for a harness. The single-harness path catches this in
13
+ * the CLI top-level handler and exits 0 cleanly (today's UX). The
14
+ * multi-harness path catches it in the per-harness loop, marks that
15
+ * harness as `declined`, and continues with siblings.
16
+ *
17
+ * NOT an operational failure — this is a deliberate user choice. The
18
+ * exit-code classifier (spec §7.5) treats `declined` as exit 0 unless
19
+ * combined with an actual `failed` harness in the same run.
20
+ */
21
+ export class ConflictRejectedError extends Error {
22
+ harnessName;
23
+ constructor(harnessName) {
24
+ super(`User declined conflict prompt for ${harnessName}`);
25
+ this.harnessName = harnessName;
26
+ this.name = "ConflictRejectedError";
27
+ }
28
+ }
@@ -2,6 +2,7 @@ import { detect } from "../steps/detect.js";
2
2
  import type { McpResult } from "../steps/mcp.js";
3
3
  import type { AgentsResult } from "../steps/agents.js";
4
4
  import type { CommandsResult } from "../steps/commands.js";
5
+ import type { SkillsResult } from "../steps/skills.js";
5
6
  import type { MetricsResult } from "../steps/metrics.js";
6
7
  import type { CliExecutor, CliInstallResult } from "../steps/cli.js";
7
8
  import type { AgentMetricsCliExecutor, AgentMetricsCliInstallResult } from "../steps/agent-metrics-cli.js";
@@ -12,6 +13,7 @@ export declare function initContext(opts: {
12
13
  signup: boolean;
13
14
  skipValidation: boolean;
14
15
  yes: boolean;
16
+ dryRun?: boolean;
15
17
  }): Promise<{
16
18
  env: Awaited<ReturnType<typeof detect>>;
17
19
  apiKey: string;
@@ -31,6 +33,11 @@ export declare function installCommandsDefs(profile: HarnessProfile, opts: {
31
33
  localDefs: boolean;
32
34
  dryRun: boolean;
33
35
  }, prev?: string[]): Promise<CommandsResult>;
36
+ /** Copy Codex-style skills from assets to the harness directory. */
37
+ export declare function installSkillsDefs(profile: HarnessProfile, opts: {
38
+ localDefs: boolean;
39
+ dryRun: boolean;
40
+ }, prev?: string[]): Promise<SkillsResult>;
34
41
  /** Install agent-metrics hook and tool files (Claude Code only). */
35
42
  export declare function configureMetricsStep(profile: HarnessProfile, opts: {
36
43
  dryRun: boolean;
@@ -3,10 +3,11 @@ import { join } from "node:path";
3
3
  import { readdir } from "node:fs/promises";
4
4
  import { detect } from "../steps/detect.js";
5
5
  import { signup } from "../steps/signup.js";
6
- import { resolveApiKey, hasCredentialsFile } from "../steps/auth.js";
6
+ import { resolveApiKey, hasCredentialsFile, writeCredentialsFile } from "../steps/auth.js";
7
7
  import { installMcp } from "../steps/mcp.js";
8
8
  import { installAgents } from "../steps/agents.js";
9
9
  import { installCommands } from "../steps/commands.js";
10
+ import { installSkills } from "../steps/skills.js";
10
11
  import { installMetrics } from "../steps/metrics.js";
11
12
  import { installCli, CLI_PACKAGE } from "../steps/cli.js";
12
13
  import { installAgentMetricsCli, AGENT_METRICS_PACKAGE, AGENT_METRICS_BIN, } from "../steps/agent-metrics-cli.js";
@@ -15,6 +16,7 @@ import { probeHookSupport } from "../lib/settings-merger.js";
15
16
  import { findProjectRoot, ASSETS_DIR } from "../lib/paths.js";
16
17
  import { getHealthTimeout } from "../lib/health.js";
17
18
  import { ok, warn, fail, info } from "../lib/display.js";
19
+ import { ConflictRejectedError } from "./errors.js";
18
20
  /**
19
21
  * Decide whether to ask the user "Are you creating a new account?".
20
22
  * The prompt is the new-user friction-reducer — it should fire only when
@@ -47,6 +49,10 @@ async function shouldPromptForAccount(opts) {
47
49
  export async function initContext(opts) {
48
50
  const env = await detect();
49
51
  let apiKey;
52
+ let resolvedEmail = null;
53
+ // Capture before auth runs — a successful resolve via the credentials file
54
+ // would otherwise create the appearance of "no prior file" after the fact.
55
+ const credsExistedAtStart = await hasCredentialsFile();
50
56
  let creatingAccount = opts.signup;
51
57
  if (!opts.signup && (await shouldPromptForAccount(opts))) {
52
58
  const { confirm } = await import("@inquirer/prompts");
@@ -56,21 +62,28 @@ export async function initContext(opts) {
56
62
  });
57
63
  console.log();
58
64
  }
65
+ let credsSource = "flag";
59
66
  try {
60
67
  if (creatingAccount) {
61
68
  info("Create your UluOps account\n");
62
69
  const auth = await signup();
63
70
  apiKey = auth.apiKey;
71
+ resolvedEmail = auth.email;
72
+ credsSource = "signup";
64
73
  ok(`Account created (${auth.email})`);
65
74
  ok(`API key generated`);
66
75
  }
67
76
  else {
77
+ const interactive = !opts.yes && !opts.apiKey && !process.env["ULUOPS_API_KEY"];
68
78
  const auth = await resolveApiKey({
69
79
  apiKeyFlag: opts.apiKey,
70
80
  skipValidation: opts.skipValidation,
71
- interactive: !opts.yes && !opts.apiKey && !process.env["ULUOPS_API_KEY"],
81
+ interactive,
72
82
  });
73
83
  apiKey = auth.apiKey;
84
+ resolvedEmail = auth.email;
85
+ credsSource =
86
+ opts.apiKey || process.env["ULUOPS_API_KEY"] ? "flag" : "prompt";
74
87
  if (auth.email)
75
88
  ok(`Key validated (${auth.email})`);
76
89
  else if (opts.skipValidation)
@@ -83,6 +96,34 @@ export async function initContext(opts) {
83
96
  fail(err instanceof Error ? err.message : String(err));
84
97
  process.exit(1);
85
98
  }
99
+ // Persist the key locally so @uluops/cli and the SDK can read it from disk.
100
+ // Always write after signup (key was just minted, nothing else holds it).
101
+ // Otherwise write only if there was no prior file — avoids churning
102
+ // createdAt on every returning-user invocation.
103
+ if (credsSource === "signup" || !credsExistedAtStart) {
104
+ try {
105
+ await writeCredentialsFile(apiKey, {
106
+ email: resolvedEmail,
107
+ source: credsSource,
108
+ dryRun: opts.dryRun,
109
+ });
110
+ if (!opts.dryRun)
111
+ ok("Credentials saved → ~/.uluops/credentials.json");
112
+ }
113
+ catch (err) {
114
+ const msg = err instanceof Error ? err.message : String(err);
115
+ warn(`Could not save credentials to ~/.uluops/credentials.json: ${msg}`);
116
+ // On signup, the warn() above is the ONLY surface that names the failure.
117
+ // The key was just minted; if we don't persist it and don't surface it,
118
+ // the user has no path back to it short of minting another one.
119
+ // Print recovery context so the message is actionable, not just a flag.
120
+ if (credsSource === "signup") {
121
+ warn(" Your key was generated but is not on disk for @uluops/cli to read.");
122
+ warn(" Copy it from the MCP config block written above (look for ULUOPS_API_KEY)");
123
+ warn(" or pass --skip-validation --api-key <key> on the next run.");
124
+ }
125
+ }
126
+ }
86
127
  return { env, apiKey };
87
128
  }
88
129
  /** Write MCP server entries to harness config and report warnings. */
@@ -107,6 +148,11 @@ export async function installAgentsDefs(profile, opts, prev) {
107
148
  ? "./uluops/agents/"
108
149
  : `${profile.paths.agentsDir.replace(process.env["HOME"] ?? "", "~")}/`;
109
150
  ok(`${res.files.length} agents → ${dest}${parts.length ? ` (${parts.join(", ")})` : ""}`);
151
+ // Per-file copy failures are non-fatal — surface them so the user knows
152
+ // re-running setup will retry the unwritten files.
153
+ for (const f of res.failures) {
154
+ warn(`Failed to copy agent ${f.file}: ${f.error} — re-run setup to retry`);
155
+ }
110
156
  return res;
111
157
  }
112
158
  /** Copy slash-command definitions from assets (Claude Code only). */
@@ -128,6 +174,33 @@ export async function installCommandsDefs(profile, opts, prev) {
128
174
  ? "./uluops/commands/"
129
175
  : `${profile.paths.commandsDir.replace(process.env["HOME"] ?? "", "~")}/`;
130
176
  ok(`${res.files.length} commands → ${dest}${parts.length ? ` (${parts.join(", ")})` : ""}`);
177
+ // Per-file copy failures are non-fatal — surface them so the user knows
178
+ // re-running setup will retry the unwritten files.
179
+ for (const f of res.failures) {
180
+ warn(`Failed to copy command ${f.file}: ${f.error} — re-run setup to retry`);
181
+ }
182
+ return res;
183
+ }
184
+ /** Copy Codex-style skills from assets to the harness directory. */
185
+ export async function installSkillsDefs(profile, opts, prev) {
186
+ const res = await installSkills(profile, opts.localDefs, opts.dryRun, prev);
187
+ if (res.skippedReason === "not-supported") {
188
+ return res;
189
+ }
190
+ const parts = [];
191
+ if (res.copied > 0)
192
+ parts.push(`${res.copied} copied`);
193
+ if (res.skipped > 0)
194
+ parts.push(`${res.skipped} unchanged`);
195
+ if (res.removed > 0)
196
+ parts.push(`${res.removed} removed`);
197
+ const dest = opts.localDefs
198
+ ? "./uluops/skills/"
199
+ : `${(profile.paths.skillsDir ?? "").replace(process.env["HOME"] ?? "", "~")}/`;
200
+ ok(`${res.files.length} skills → ${dest}${parts.length ? ` (${parts.join(", ")})` : ""}`);
201
+ for (const f of res.failures) {
202
+ warn(`Failed to copy skill ${f.file}: ${f.error} — re-run setup to retry`);
203
+ }
131
204
  return res;
132
205
  }
133
206
  /** Install agent-metrics hook and tool files (Claude Code only). */
@@ -360,7 +433,11 @@ export async function checkConflicts(profile, localDefs) {
360
433
  const { confirm } = await import("@inquirer/prompts");
361
434
  const proceed = await confirm({ message: "Continue?", default: true });
362
435
  if (!proceed) {
363
- process.exit(0);
436
+ // Throw instead of process.exit so the multi-harness loop can catch and
437
+ // continue with sibling harnesses. The single-harness CLI top-level
438
+ // handler catches ConflictRejectedError and exits 0 cleanly, preserving
439
+ // today's UX. (Spec §7.5; replaces the prior process.exit(0).)
440
+ throw new ConflictRejectedError(profile.name);
364
441
  }
365
442
  }
366
443
  /** Fetch a URL and return true if the response is OK, false on any failure. */
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Per-harness orchestration types + the exit-code classifier.
3
+ *
4
+ * Lives in its own module so both `runSetup` (which produces these) and
5
+ * `printSetupSummary` (which renders them) can import without circular
6
+ * dependency. The classifier is testable in isolation — every row of the
7
+ * spec §7.5 4-tier table is one test.
8
+ */
9
+ import type { HarnessProfile } from "../harnesses/index.js";
10
+ import type { PartialStep } from "../lib/manifest.js";
11
+ import type { AgentsResult } from "../steps/agents.js";
12
+ import type { CommandsResult } from "../steps/commands.js";
13
+ import type { SkillsResult } from "../steps/skills.js";
14
+ import type { MetricsResult } from "../steps/metrics.js";
15
+ import type { McpResult } from "../steps/mcp.js";
16
+ /**
17
+ * Per-harness outcome captured by the orchestrator loop.
18
+ *
19
+ * `status` semantics:
20
+ * ok — every per-harness step succeeded; the user's choice was
21
+ * carried out and recorded in the manifest.
22
+ * failed — an OPERATIONAL error occurred (MCP write failed,
23
+ * agents-step mkdir EACCES, etc.). Drives exit code 1.
24
+ * declined — the user actively rejected the conflict prompt. NOT an
25
+ * error from the tool's perspective — it's a deliberate
26
+ * choice. Drives exit code 0 (with a non-zero summary line
27
+ * surfacing the count).
28
+ */
29
+ export interface PerHarnessResult {
30
+ harnessName: string;
31
+ profile: HarnessProfile;
32
+ status: "ok" | "failed" | "declined";
33
+ error?: string;
34
+ mcpResult?: McpResult;
35
+ agentsResult?: AgentsResult;
36
+ commandsResult?: CommandsResult;
37
+ skillsResult?: SkillsResult;
38
+ metricsResult?: MetricsResult;
39
+ partial?: PartialStep | null;
40
+ }
41
+ /**
42
+ * Decide the process exit code from a set of per-harness results.
43
+ *
44
+ * Spec §7.5 4-tier table:
45
+ *
46
+ * | Outcome | Exit |
47
+ * |------------------------------------------------------|------|
48
+ * | Every harness ok | 0 |
49
+ * | Any harness failed (operational) | 1 |
50
+ * | Any declined AND zero failed | 0 |
51
+ * | Empty (user unchecked all, or no harnesses to run) | 0 |
52
+ *
53
+ * Rationale: CI wrapping `--harness all` should not be poisoned by
54
+ * user-policy choices (declines, no-op outcomes) but MUST fail on
55
+ * operational errors (EACCES, ENOSPC, parse-error) so deploy pipelines
56
+ * can detect partial failure and re-run.
57
+ *
58
+ * `partial` is a property of a `failed` result (post-MCP-success step
59
+ * threw), not its own status — it's already covered by the failed
60
+ * branch. A successful run with file-level `failures[]` arrays is
61
+ * still `status: "ok"` (file failures are recoverable on re-run; the
62
+ * user has working state).
63
+ */
64
+ export declare function classifyExit(results: PerHarnessResult[]): number;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-harness orchestration types + the exit-code classifier.
3
+ *
4
+ * Lives in its own module so both `runSetup` (which produces these) and
5
+ * `printSetupSummary` (which renders them) can import without circular
6
+ * dependency. The classifier is testable in isolation — every row of the
7
+ * spec §7.5 4-tier table is one test.
8
+ */
9
+ /**
10
+ * Decide the process exit code from a set of per-harness results.
11
+ *
12
+ * Spec §7.5 4-tier table:
13
+ *
14
+ * | Outcome | Exit |
15
+ * |------------------------------------------------------|------|
16
+ * | Every harness ok | 0 |
17
+ * | Any harness failed (operational) | 1 |
18
+ * | Any declined AND zero failed | 0 |
19
+ * | Empty (user unchecked all, or no harnesses to run) | 0 |
20
+ *
21
+ * Rationale: CI wrapping `--harness all` should not be poisoned by
22
+ * user-policy choices (declines, no-op outcomes) but MUST fail on
23
+ * operational errors (EACCES, ENOSPC, parse-error) so deploy pipelines
24
+ * can detect partial failure and re-run.
25
+ *
26
+ * `partial` is a property of a `failed` result (post-MCP-success step
27
+ * threw), not its own status — it's already covered by the failed
28
+ * branch. A successful run with file-level `failures[]` arrays is
29
+ * still `status: "ok"` (file failures are recoverable on re-run; the
30
+ * user has working state).
31
+ */
32
+ export function classifyExit(results) {
33
+ if (results.length === 0)
34
+ return 0;
35
+ const anyFailed = results.some((r) => r.status === "failed");
36
+ return anyFailed ? 1 : 0;
37
+ }
@@ -1,4 +1,5 @@
1
- export declare function runSetup(opts: {
1
+ export type { PerHarnessResult } from "./per-harness.js";
2
+ interface RunSetupOpts {
2
3
  apiKey?: string;
3
4
  signup: boolean;
4
5
  scope: "global" | "local";
@@ -7,9 +8,10 @@ export declare function runSetup(opts: {
7
8
  skipValidation: boolean;
8
9
  dryRun: boolean;
9
10
  yes: boolean;
10
- harness: string;
11
+ harnesses: string[];
11
12
  withCli?: boolean;
12
13
  cli?: boolean;
13
14
  withAgentMetricsCli?: boolean;
14
15
  agentMetricsCli?: boolean;
15
- }): Promise<void>;
16
+ }
17
+ export declare function runSetup(opts: RunSetupOpts): Promise<void>;