@uluops/setup 0.6.5 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -9
- package/assets/codex/skills/uluops-operator/SKILL.md +159 -0
- package/dist/cli/select-harnesses.d.ts +91 -0
- package/dist/cli/select-harnesses.js +108 -0
- package/dist/cli.js +90 -37
- package/dist/commands/errors.d.ts +24 -0
- package/dist/commands/errors.js +28 -0
- package/dist/commands/helpers.d.ts +32 -0
- package/dist/commands/helpers.js +146 -3
- package/dist/commands/per-harness.d.ts +64 -0
- package/dist/commands/per-harness.js +37 -0
- package/dist/commands/setup.d.ts +7 -3
- package/dist/commands/setup.js +232 -71
- package/dist/commands/uninstall-filter.d.ts +36 -0
- package/dist/commands/uninstall-filter.js +69 -0
- package/dist/commands/uninstall.d.ts +12 -2
- package/dist/commands/uninstall.js +190 -82
- package/dist/harnesses/codex.d.ts +5 -10
- package/dist/harnesses/codex.js +89 -21
- package/dist/harnesses/index.js +6 -1
- package/dist/harnesses/opencode.d.ts +8 -0
- package/dist/harnesses/opencode.js +24 -1
- package/dist/harnesses/types.d.ts +2 -0
- package/dist/harnesses/types.js +5 -1
- package/dist/lib/atomic-write.js +10 -2
- package/dist/lib/config-merger.d.ts +6 -3
- package/dist/lib/config-merger.js +50 -7
- package/dist/lib/display.d.ts +21 -5
- package/dist/lib/display.js +118 -13
- package/dist/lib/file-ops.d.ts +13 -5
- package/dist/lib/file-ops.js +34 -34
- package/dist/lib/install-lock.d.ts +47 -0
- package/dist/lib/install-lock.js +251 -0
- package/dist/lib/json-guards.d.ts +22 -0
- package/dist/lib/json-guards.js +33 -0
- package/dist/lib/manifest.d.ts +28 -0
- package/dist/lib/manifest.js +61 -12
- package/dist/lib/paths.d.ts +2 -17
- package/dist/lib/paths.js +4 -19
- package/dist/lib/settings-merger.js +3 -1
- package/dist/steps/agent-metrics-cli.d.ts +72 -0
- package/dist/steps/agent-metrics-cli.js +147 -0
- package/dist/steps/agents.d.ts +11 -0
- package/dist/steps/agents.js +30 -25
- package/dist/steps/auth.d.ts +13 -0
- package/dist/steps/auth.js +61 -5
- package/dist/steps/cli.d.ts +6 -0
- package/dist/steps/cli.js +29 -10
- package/dist/steps/commands.d.ts +10 -0
- package/dist/steps/commands.js +31 -30
- package/dist/steps/detect.js +15 -1
- package/dist/steps/mcp.js +1 -8
- package/dist/steps/metrics.js +10 -3
- package/dist/steps/shell.js +3 -13
- package/dist/steps/signup.js +14 -1
- package/dist/steps/skills.d.ts +14 -0
- package/dist/steps/skills.js +95 -0
- package/dist/steps/verify.js +195 -91
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -3,10 +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 {
|
|
6
|
+
import { listHarnesses, detectHarnesses, getProfile, HarnessNotTestedError, } from "./harnesses/index.js";
|
|
7
|
+
import { InstallLockHeldError } from "./lib/install-lock.js";
|
|
7
8
|
import { runSetup } from "./commands/setup.js";
|
|
8
9
|
import { runUninstall } from "./commands/uninstall.js";
|
|
9
10
|
import { runVerify } from "./commands/verify.js";
|
|
11
|
+
import { ConflictRejectedError } from "./commands/errors.js";
|
|
12
|
+
import { selectHarnesses, HarnessSelectionError, } from "./cli/select-harnesses.js";
|
|
10
13
|
async function main() {
|
|
11
14
|
const version = await getVersion();
|
|
12
15
|
const program = new Command()
|
|
@@ -15,12 +18,15 @@ async function main() {
|
|
|
15
18
|
.version(version)
|
|
16
19
|
.option("--api-key <key>", "API key (skip prompt)")
|
|
17
20
|
.option("--signup", "Create a new account (email + password, no browser)")
|
|
18
|
-
.option("--harness <name>", `Target harness: ${listHarnesses().join(", ")} (aliases: claude, oc, gemini)
|
|
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)
|
|
19
23
|
.option("--scope <mode>", 'MCP connectivity scope: "global" (~/.claude.json) or "local" (.mcp.json)', "global")
|
|
20
24
|
.option("--local-defs", "Save agents/commands locally (./uluops/) for project isolation", false)
|
|
21
25
|
.option("--shell", "Write API key export to shell profile", false)
|
|
22
26
|
.option("--with-cli", "Install @uluops/cli globally without prompting")
|
|
23
27
|
.option("--no-cli", "Skip @uluops/cli install without prompting (takes precedence over --with-cli)")
|
|
28
|
+
.option("--with-agent-metrics-cli", "Install @uluops/agent-metrics globally without prompting")
|
|
29
|
+
.option("--no-agent-metrics-cli", "Skip @uluops/agent-metrics install without prompting (takes precedence over --with-agent-metrics-cli)")
|
|
24
30
|
.option("--skip-validation", "Accept API key without verifying", false)
|
|
25
31
|
.option("--list", "Show available agents and workflows without installing")
|
|
26
32
|
.option("--verify", "Check existing installation health")
|
|
@@ -43,7 +49,17 @@ async function main() {
|
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
45
51
|
if (opts.uninstall) {
|
|
46
|
-
|
|
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
|
+
});
|
|
47
63
|
return;
|
|
48
64
|
}
|
|
49
65
|
if (opts.scope && opts.scope !== "local" && opts.scope !== "global") {
|
|
@@ -51,42 +67,59 @@ async function main() {
|
|
|
51
67
|
process.exit(1);
|
|
52
68
|
}
|
|
53
69
|
const scope = opts.scope === "local" ? "local" : "global";
|
|
54
|
-
// Resolve harness
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
choices:
|
|
78
|
-
|
|
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
|
+
})),
|
|
79
98
|
});
|
|
80
99
|
console.log();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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);
|
|
89
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);
|
|
90
123
|
}
|
|
91
124
|
await runSetup({
|
|
92
125
|
apiKey: opts.apiKey,
|
|
@@ -96,17 +129,37 @@ async function main() {
|
|
|
96
129
|
shell: opts.shell,
|
|
97
130
|
withCli: opts.withCli,
|
|
98
131
|
cli: opts.cli,
|
|
132
|
+
withAgentMetricsCli: opts.withAgentMetricsCli,
|
|
133
|
+
agentMetricsCli: opts.agentMetricsCli,
|
|
99
134
|
skipValidation: opts.skipValidation,
|
|
100
135
|
dryRun: opts.dryRun,
|
|
101
136
|
yes: opts.yes,
|
|
102
|
-
|
|
137
|
+
harnesses: resolvedHarnesses,
|
|
103
138
|
});
|
|
104
139
|
}
|
|
105
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
|
+
}
|
|
106
152
|
if (err instanceof HarnessNotTestedError) {
|
|
107
153
|
console.error(chalk.yellow(`\n ${err.message}\n`));
|
|
108
154
|
process.exit(1);
|
|
109
155
|
}
|
|
156
|
+
if (err instanceof InstallLockHeldError) {
|
|
157
|
+
console.error(chalk.yellow(`\n ${err.message}\n`));
|
|
158
|
+
console.error(chalk.dim(" Wait for the other process to finish, or — if it crashed —\n" +
|
|
159
|
+
" the lock auto-releases after 30 minutes or when the held\n" +
|
|
160
|
+
" PID is detected as no longer running.\n"));
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
110
163
|
const msg = err instanceof Error ? err.message : String(err);
|
|
111
164
|
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
112
165
|
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,8 +2,10 @@ 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";
|
|
8
|
+
import type { AgentMetricsCliExecutor, AgentMetricsCliInstallResult } from "../steps/agent-metrics-cli.js";
|
|
7
9
|
import type { HarnessProfile } from "../harnesses/index.js";
|
|
8
10
|
/** Resolve API key via flag, env, file, signup, or interactive prompt. Returns env detection + key. */
|
|
9
11
|
export declare function initContext(opts: {
|
|
@@ -11,6 +13,7 @@ export declare function initContext(opts: {
|
|
|
11
13
|
signup: boolean;
|
|
12
14
|
skipValidation: boolean;
|
|
13
15
|
yes: boolean;
|
|
16
|
+
dryRun?: boolean;
|
|
14
17
|
}): Promise<{
|
|
15
18
|
env: Awaited<ReturnType<typeof detect>>;
|
|
16
19
|
apiKey: string;
|
|
@@ -30,6 +33,11 @@ export declare function installCommandsDefs(profile: HarnessProfile, opts: {
|
|
|
30
33
|
localDefs: boolean;
|
|
31
34
|
dryRun: boolean;
|
|
32
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>;
|
|
33
41
|
/** Install agent-metrics hook and tool files (Claude Code only). */
|
|
34
42
|
export declare function configureMetricsStep(profile: HarnessProfile, opts: {
|
|
35
43
|
dryRun: boolean;
|
|
@@ -54,6 +62,30 @@ export declare function configureCliStep(opts: {
|
|
|
54
62
|
dryRun: boolean;
|
|
55
63
|
executor?: CliExecutor;
|
|
56
64
|
}): Promise<CliInstallResult | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Decide whether to install `@uluops/agent-metrics` globally and do it.
|
|
67
|
+
*
|
|
68
|
+
* Only meaningful when the SubagentStop hook actually got configured —
|
|
69
|
+
* otherwise the CLI has no captures to read. Caller gates on
|
|
70
|
+
* `metricsResult.hookConfigured`.
|
|
71
|
+
*
|
|
72
|
+
* Decision matrix mirrors `configureCliStep`:
|
|
73
|
+
* - `--no-agent-metrics-cli` (opts.agentMetricsCli === false) → skip, no prompt
|
|
74
|
+
* - `--with-agent-metrics-cli` (opts.withAgentMetricsCli === true) → install, no prompt
|
|
75
|
+
* - Neither flag + non-interactive (--yes / --api-key / no TTY) → skip
|
|
76
|
+
* - Neither flag + interactive → prompt (default Y)
|
|
77
|
+
*
|
|
78
|
+
* Returns null when the step did not run (skipped). Returns an install result
|
|
79
|
+
* when an attempt was made, for manifest recording.
|
|
80
|
+
*/
|
|
81
|
+
export declare function configureAgentMetricsCliStep(opts: {
|
|
82
|
+
withAgentMetricsCli?: boolean;
|
|
83
|
+
agentMetricsCli?: boolean;
|
|
84
|
+
yes: boolean;
|
|
85
|
+
apiKey?: string;
|
|
86
|
+
dryRun: boolean;
|
|
87
|
+
executor?: AgentMetricsCliExecutor;
|
|
88
|
+
}): Promise<AgentMetricsCliInstallResult | null>;
|
|
57
89
|
/** Ping tracker and registry health endpoints. */
|
|
58
90
|
export declare function runHealthCheck(opts: {
|
|
59
91
|
skipValidation: boolean;
|
package/dist/commands/helpers.js
CHANGED
|
@@ -3,17 +3,20 @@ 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";
|
|
13
|
+
import { installAgentMetricsCli, AGENT_METRICS_PACKAGE, AGENT_METRICS_BIN, } from "../steps/agent-metrics-cli.js";
|
|
12
14
|
import { writeShellExport } from "../steps/shell.js";
|
|
13
15
|
import { probeHookSupport } from "../lib/settings-merger.js";
|
|
14
16
|
import { findProjectRoot, ASSETS_DIR } from "../lib/paths.js";
|
|
15
17
|
import { getHealthTimeout } from "../lib/health.js";
|
|
16
18
|
import { ok, warn, fail, info } from "../lib/display.js";
|
|
19
|
+
import { ConflictRejectedError } from "./errors.js";
|
|
17
20
|
/**
|
|
18
21
|
* Decide whether to ask the user "Are you creating a new account?".
|
|
19
22
|
* The prompt is the new-user friction-reducer — it should fire only when
|
|
@@ -46,6 +49,10 @@ async function shouldPromptForAccount(opts) {
|
|
|
46
49
|
export async function initContext(opts) {
|
|
47
50
|
const env = await detect();
|
|
48
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();
|
|
49
56
|
let creatingAccount = opts.signup;
|
|
50
57
|
if (!opts.signup && (await shouldPromptForAccount(opts))) {
|
|
51
58
|
const { confirm } = await import("@inquirer/prompts");
|
|
@@ -55,21 +62,28 @@ export async function initContext(opts) {
|
|
|
55
62
|
});
|
|
56
63
|
console.log();
|
|
57
64
|
}
|
|
65
|
+
let credsSource = "flag";
|
|
58
66
|
try {
|
|
59
67
|
if (creatingAccount) {
|
|
60
68
|
info("Create your UluOps account\n");
|
|
61
69
|
const auth = await signup();
|
|
62
70
|
apiKey = auth.apiKey;
|
|
71
|
+
resolvedEmail = auth.email;
|
|
72
|
+
credsSource = "signup";
|
|
63
73
|
ok(`Account created (${auth.email})`);
|
|
64
74
|
ok(`API key generated`);
|
|
65
75
|
}
|
|
66
76
|
else {
|
|
77
|
+
const interactive = !opts.yes && !opts.apiKey && !process.env["ULUOPS_API_KEY"];
|
|
67
78
|
const auth = await resolveApiKey({
|
|
68
79
|
apiKeyFlag: opts.apiKey,
|
|
69
80
|
skipValidation: opts.skipValidation,
|
|
70
|
-
interactive
|
|
81
|
+
interactive,
|
|
71
82
|
});
|
|
72
83
|
apiKey = auth.apiKey;
|
|
84
|
+
resolvedEmail = auth.email;
|
|
85
|
+
credsSource =
|
|
86
|
+
opts.apiKey || process.env["ULUOPS_API_KEY"] ? "flag" : "prompt";
|
|
73
87
|
if (auth.email)
|
|
74
88
|
ok(`Key validated (${auth.email})`);
|
|
75
89
|
else if (opts.skipValidation)
|
|
@@ -82,6 +96,34 @@ export async function initContext(opts) {
|
|
|
82
96
|
fail(err instanceof Error ? err.message : String(err));
|
|
83
97
|
process.exit(1);
|
|
84
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
|
+
}
|
|
85
127
|
return { env, apiKey };
|
|
86
128
|
}
|
|
87
129
|
/** Write MCP server entries to harness config and report warnings. */
|
|
@@ -106,6 +148,11 @@ export async function installAgentsDefs(profile, opts, prev) {
|
|
|
106
148
|
? "./uluops/agents/"
|
|
107
149
|
: `${profile.paths.agentsDir.replace(process.env["HOME"] ?? "", "~")}/`;
|
|
108
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
|
+
}
|
|
109
156
|
return res;
|
|
110
157
|
}
|
|
111
158
|
/** Copy slash-command definitions from assets (Claude Code only). */
|
|
@@ -127,6 +174,33 @@ export async function installCommandsDefs(profile, opts, prev) {
|
|
|
127
174
|
? "./uluops/commands/"
|
|
128
175
|
: `${profile.paths.commandsDir.replace(process.env["HOME"] ?? "", "~")}/`;
|
|
129
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
|
+
}
|
|
130
204
|
return res;
|
|
131
205
|
}
|
|
132
206
|
/** Install agent-metrics hook and tool files (Claude Code only). */
|
|
@@ -213,6 +287,71 @@ export async function configureCliStep(opts) {
|
|
|
213
287
|
}
|
|
214
288
|
return res;
|
|
215
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Decide whether to install `@uluops/agent-metrics` globally and do it.
|
|
292
|
+
*
|
|
293
|
+
* Only meaningful when the SubagentStop hook actually got configured —
|
|
294
|
+
* otherwise the CLI has no captures to read. Caller gates on
|
|
295
|
+
* `metricsResult.hookConfigured`.
|
|
296
|
+
*
|
|
297
|
+
* Decision matrix mirrors `configureCliStep`:
|
|
298
|
+
* - `--no-agent-metrics-cli` (opts.agentMetricsCli === false) → skip, no prompt
|
|
299
|
+
* - `--with-agent-metrics-cli` (opts.withAgentMetricsCli === true) → install, no prompt
|
|
300
|
+
* - Neither flag + non-interactive (--yes / --api-key / no TTY) → skip
|
|
301
|
+
* - Neither flag + interactive → prompt (default Y)
|
|
302
|
+
*
|
|
303
|
+
* Returns null when the step did not run (skipped). Returns an install result
|
|
304
|
+
* when an attempt was made, for manifest recording.
|
|
305
|
+
*/
|
|
306
|
+
export async function configureAgentMetricsCliStep(opts) {
|
|
307
|
+
if (opts.agentMetricsCli === false) {
|
|
308
|
+
info(chalk.dim(`Skipped global ${AGENT_METRICS_PACKAGE} install (--no-agent-metrics-cli)`));
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
let shouldInstall;
|
|
312
|
+
if (opts.withAgentMetricsCli === true) {
|
|
313
|
+
shouldInstall = true;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const nonInteractive = opts.yes || !!opts.apiKey || !process.stdin.isTTY;
|
|
317
|
+
if (nonInteractive) {
|
|
318
|
+
info(chalk.dim(`Skipped global ${AGENT_METRICS_PACKAGE} install (non-interactive — pass --with-agent-metrics-cli to install)`));
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
322
|
+
shouldInstall = await confirm({
|
|
323
|
+
message: `Install ${AGENT_METRICS_PACKAGE} globally (provides the ${chalk.cyan(AGENT_METRICS_BIN)} command for reading captures)?`,
|
|
324
|
+
default: true,
|
|
325
|
+
});
|
|
326
|
+
if (!shouldInstall) {
|
|
327
|
+
info(chalk.dim(`Skipped global ${AGENT_METRICS_PACKAGE} install`));
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const res = await installAgentMetricsCli({
|
|
332
|
+
dryRun: opts.dryRun,
|
|
333
|
+
executor: opts.executor,
|
|
334
|
+
});
|
|
335
|
+
if (opts.dryRun && !res.alreadyPresent) {
|
|
336
|
+
ok(`Would install ${AGENT_METRICS_PACKAGE} globally`);
|
|
337
|
+
return res;
|
|
338
|
+
}
|
|
339
|
+
if (res.alreadyPresent) {
|
|
340
|
+
ok(`${AGENT_METRICS_PACKAGE} already installed${res.version ? ` (${res.version})` : ""} — no change`);
|
|
341
|
+
return res;
|
|
342
|
+
}
|
|
343
|
+
if (res.installed) {
|
|
344
|
+
ok(`${AGENT_METRICS_PACKAGE} installed globally${res.version ? ` (${res.version})` : ""}`);
|
|
345
|
+
return res;
|
|
346
|
+
}
|
|
347
|
+
warn(`Could not install ${AGENT_METRICS_PACKAGE} globally — try ${chalk.cyan(`npm install -g ${AGENT_METRICS_PACKAGE}`)} manually`);
|
|
348
|
+
if (res.error) {
|
|
349
|
+
const oneLine = res.error.split("\n")[0]?.slice(0, 120) ?? "";
|
|
350
|
+
if (oneLine)
|
|
351
|
+
info(chalk.dim(` ${oneLine}`));
|
|
352
|
+
}
|
|
353
|
+
return res;
|
|
354
|
+
}
|
|
216
355
|
/** Ping tracker and registry health endpoints. */
|
|
217
356
|
export async function runHealthCheck(opts) {
|
|
218
357
|
if (!opts.skipValidation && !opts.dryRun) {
|
|
@@ -294,7 +433,11 @@ export async function checkConflicts(profile, localDefs) {
|
|
|
294
433
|
const { confirm } = await import("@inquirer/prompts");
|
|
295
434
|
const proceed = await confirm({ message: "Continue?", default: true });
|
|
296
435
|
if (!proceed) {
|
|
297
|
-
process.exit
|
|
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);
|
|
298
441
|
}
|
|
299
442
|
}
|
|
300
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
|
+
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
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,7 +8,10 @@ export declare function runSetup(opts: {
|
|
|
7
8
|
skipValidation: boolean;
|
|
8
9
|
dryRun: boolean;
|
|
9
10
|
yes: boolean;
|
|
10
|
-
|
|
11
|
+
harnesses: string[];
|
|
11
12
|
withCli?: boolean;
|
|
12
13
|
cli?: boolean;
|
|
13
|
-
|
|
14
|
+
withAgentMetricsCli?: boolean;
|
|
15
|
+
agentMetricsCli?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function runSetup(opts: RunSetupOpts): Promise<void>;
|