@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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow guards for JSON responses from the UluOps API.
|
|
3
|
+
*
|
|
4
|
+
* Centralised so the same envelope shape (`{ data: { ... } }`) is decoded
|
|
5
|
+
* consistently across call sites (`steps/auth.ts`, `steps/verify.ts`,
|
|
6
|
+
* `steps/signup.ts`). Each guard returns `null` on absent/wrong-typed fields
|
|
7
|
+
* and throws a plain `Error` only when the top-level body shape is so wrong
|
|
8
|
+
* that proceeding would silently coerce garbage into a typed result.
|
|
9
|
+
*
|
|
10
|
+
* Why plain `Error`, not `TypeError`: call sites translate `TypeError` into
|
|
11
|
+
* "Can't reach api.uluops.ai" (fetch's network-failure shape). A `TypeError`
|
|
12
|
+
* here would be misclassified as a network outage.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Narrow `{ data: { email: string } }` from an unknown response body.
|
|
16
|
+
* Returns the email string when present and well-typed, null otherwise.
|
|
17
|
+
* Throws when `body` is not an object at all — that indicates the endpoint
|
|
18
|
+
* is no longer the one we expect (HTML error page, redirect to a captive
|
|
19
|
+
* portal, schema breakage) and should surface to the user rather than be
|
|
20
|
+
* papered over as "logged in with no email."
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractEmail(body: unknown): string | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow guards for JSON responses from the UluOps API.
|
|
3
|
+
*
|
|
4
|
+
* Centralised so the same envelope shape (`{ data: { ... } }`) is decoded
|
|
5
|
+
* consistently across call sites (`steps/auth.ts`, `steps/verify.ts`,
|
|
6
|
+
* `steps/signup.ts`). Each guard returns `null` on absent/wrong-typed fields
|
|
7
|
+
* and throws a plain `Error` only when the top-level body shape is so wrong
|
|
8
|
+
* that proceeding would silently coerce garbage into a typed result.
|
|
9
|
+
*
|
|
10
|
+
* Why plain `Error`, not `TypeError`: call sites translate `TypeError` into
|
|
11
|
+
* "Can't reach api.uluops.ai" (fetch's network-failure shape). A `TypeError`
|
|
12
|
+
* here would be misclassified as a network outage.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Narrow `{ data: { email: string } }` from an unknown response body.
|
|
16
|
+
* Returns the email string when present and well-typed, null otherwise.
|
|
17
|
+
* Throws when `body` is not an object at all — that indicates the endpoint
|
|
18
|
+
* is no longer the one we expect (HTML error page, redirect to a captive
|
|
19
|
+
* portal, schema breakage) and should surface to the user rather than be
|
|
20
|
+
* papered over as "logged in with no email."
|
|
21
|
+
*/
|
|
22
|
+
export function extractEmail(body) {
|
|
23
|
+
if (typeof body !== "object" || body === null) {
|
|
24
|
+
throw new Error("API returned an unexpected response shape (not an object). The endpoint may have changed — try --skip-validation to continue offline.");
|
|
25
|
+
}
|
|
26
|
+
const data = body.data;
|
|
27
|
+
if (data === undefined || data === null)
|
|
28
|
+
return null;
|
|
29
|
+
if (typeof data !== "object")
|
|
30
|
+
return null;
|
|
31
|
+
const email = data.email;
|
|
32
|
+
return typeof email === "string" ? email : null;
|
|
33
|
+
}
|
package/dist/lib/manifest.d.ts
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
* assumption is documented in the schema, not the comments.
|
|
12
12
|
*/
|
|
13
13
|
export type HarnessInstanceKey = string;
|
|
14
|
+
/**
|
|
15
|
+
* Names of per-harness pipeline steps that can throw before producing a
|
|
16
|
+
* complete result. Recorded in `HarnessManifest.partial` so verify can warn
|
|
17
|
+
* and re-runs can re-prompt conflicts (spec §7.6.3). `configureMcpStep` is
|
|
18
|
+
* NOT in this set — an MCP throw produces no manifest entry at all because
|
|
19
|
+
* the entry depends on the MCP-derived `mcpConfigPath`.
|
|
20
|
+
*/
|
|
21
|
+
export type PartialStep = "agents" | "commands" | "skills" | "metrics";
|
|
14
22
|
/** Per-harness installation state. */
|
|
15
23
|
export interface HarnessManifest {
|
|
16
24
|
installedAt: string;
|
|
@@ -21,6 +29,7 @@ export interface HarnessManifest {
|
|
|
21
29
|
defsPath: string;
|
|
22
30
|
agents: string[];
|
|
23
31
|
commands: string[];
|
|
32
|
+
skills?: string[];
|
|
24
33
|
hooksInstalled: boolean;
|
|
25
34
|
/**
|
|
26
35
|
* Version of @uluops/agent-metrics whose dist/ was copied into the harness tree.
|
|
@@ -29,6 +38,17 @@ export interface HarnessManifest {
|
|
|
29
38
|
* the shared version ledger across the setup↔agent-metrics seam.
|
|
30
39
|
*/
|
|
31
40
|
hooksInstalledVersion?: string | null;
|
|
41
|
+
/**
|
|
42
|
+
* When non-null, names the per-harness pipeline step that threw before
|
|
43
|
+
* producing a complete result. Earlier steps' file lists are accurate;
|
|
44
|
+
* later steps were never attempted. Verify surfaces this as a WARNING.
|
|
45
|
+
* Re-run treats a partial entry as "checkConflicts must run again" (the
|
|
46
|
+
* user never confirmed it on the failing run — spec §7.6.5).
|
|
47
|
+
*
|
|
48
|
+
* Absent (undefined) on manifests written by pre-multi-target versions —
|
|
49
|
+
* those harnesses are assumed fully installed.
|
|
50
|
+
*/
|
|
51
|
+
partial?: PartialStep | null;
|
|
32
52
|
}
|
|
33
53
|
/** Top-level manifest with per-harness entries. */
|
|
34
54
|
export interface Manifest {
|
|
@@ -45,6 +65,14 @@ export interface Manifest {
|
|
|
45
65
|
cliInstalled?: boolean;
|
|
46
66
|
/** Version reported by `ulu --version` at install time, for drift detection. */
|
|
47
67
|
cliInstalledVersion?: string | null;
|
|
68
|
+
/**
|
|
69
|
+
* Tracks whether `@uluops/agent-metrics` was installed globally by this
|
|
70
|
+
* setup run. Same ownership semantics as `cliInstalled` — uninstall only
|
|
71
|
+
* removes the global package when this is true.
|
|
72
|
+
*/
|
|
73
|
+
agentMetricsCliInstalled?: boolean;
|
|
74
|
+
/** Version reported by `agent-metrics --version` at install time. */
|
|
75
|
+
agentMetricsCliInstalledVersion?: string | null;
|
|
48
76
|
contentHash?: string;
|
|
49
77
|
}
|
|
50
78
|
export interface ManifestValidationResult {
|
package/dist/lib/manifest.js
CHANGED
|
@@ -13,8 +13,15 @@ function isNewManifest(obj) {
|
|
|
13
13
|
typeof m["harnesses"] !== "object" ||
|
|
14
14
|
m["harnesses"] === null)
|
|
15
15
|
return false;
|
|
16
|
-
//
|
|
16
|
+
// An on-disk manifest must reference at least one harness installation.
|
|
17
|
+
// The empty-harnesses case used to pass vacuously (the for-loop iterated
|
|
18
|
+
// zero times), letting a truncated `{...harnesses:{}}` file masquerade as
|
|
19
|
+
// valid — and a subsequent uninstall would then iterate zero harnesses,
|
|
20
|
+
// delete the manifest, and report success while leaving every MCP config,
|
|
21
|
+
// agent, hook, and shell export in place.
|
|
17
22
|
const harnesses = m["harnesses"];
|
|
23
|
+
if (Object.keys(harnesses).length === 0)
|
|
24
|
+
return false;
|
|
18
25
|
for (const h of Object.values(harnesses)) {
|
|
19
26
|
if (typeof h !== "object" || h === null)
|
|
20
27
|
return false;
|
|
@@ -23,6 +30,14 @@ function isNewManifest(obj) {
|
|
|
23
30
|
return false;
|
|
24
31
|
if (!Array.isArray(hm["agents"]) || !Array.isArray(hm["commands"]))
|
|
25
32
|
return false;
|
|
33
|
+
if ("skills" in hm && !Array.isArray(hm["skills"]))
|
|
34
|
+
return false;
|
|
35
|
+
if ("partial" in hm) {
|
|
36
|
+
const p = hm["partial"];
|
|
37
|
+
if (p !== null && p !== "agents" && p !== "commands" && p !== "skills" && p !== "metrics") {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
}
|
|
27
42
|
return true;
|
|
28
43
|
}
|
|
@@ -83,20 +98,54 @@ export async function validateManifest(manifest) {
|
|
|
83
98
|
warnings.push(`[${harnessName}] Command files missing from disk: ${missing.join(", ")}`);
|
|
84
99
|
}
|
|
85
100
|
}
|
|
101
|
+
if ((hm.skills?.length ?? 0) > 0 && defsExists) {
|
|
102
|
+
const missing = await findMissingFiles(hm.defsPath, "skills", hm.skills ?? []);
|
|
103
|
+
if (missing.length > 0) {
|
|
104
|
+
warnings.push(`[${harnessName}] Skill files missing from disk: ${missing.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
86
107
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
// Hash verification reads whichever manifest file actually exists. The
|
|
109
|
+
// previous implementation hardcoded the new path, which produced a false
|
|
110
|
+
// "Cannot read manifest file to verify content hash" warning on every
|
|
111
|
+
// uninstall after `loadManifest` migrated a legacy manifest in memory
|
|
112
|
+
// without writing it back to the new location. Silently skip the hash
|
|
113
|
+
// check when no manifest is on disk in either location (in-memory-only
|
|
114
|
+
// manifest, or both locations missing).
|
|
115
|
+
let raw = null;
|
|
116
|
+
for (const candidate of [getManifestPath(), getLegacyManifestPath()]) {
|
|
117
|
+
try {
|
|
118
|
+
raw = await readFile(candidate, "utf-8");
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Try next candidate
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
if (raw !== null) {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(raw);
|
|
128
|
+
const { contentHash: storedHash, ...withoutHash } = parsed;
|
|
129
|
+
const canonical = JSON.stringify(withoutHash, null, 2) + "\n";
|
|
130
|
+
const currentHash = fileHash(canonical);
|
|
131
|
+
// Typeof check rather than truthiness — a malformed manifest with
|
|
132
|
+
// `contentHash: 0`, `contentHash: false`, `contentHash: ""`, or no
|
|
133
|
+
// contentHash key at all would all evaluate `storedHash && ...` to
|
|
134
|
+
// false and silently skip tamper detection. Only a missing-key
|
|
135
|
+
// (undefined) manifest is legitimately exempt (legacy / pre-hash);
|
|
136
|
+
// a present-but-not-a-string value is suspect and should warn.
|
|
137
|
+
if (typeof storedHash === "string") {
|
|
138
|
+
if (storedHash !== currentHash) {
|
|
139
|
+
warnings.push("Manifest file has been modified since installation — content hash mismatch");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (storedHash !== undefined) {
|
|
143
|
+
warnings.push(`Manifest contentHash has wrong type (${typeof storedHash}); tamper detection skipped`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
warnings.push("Manifest file is unparseable JSON");
|
|
148
|
+
}
|
|
100
149
|
}
|
|
101
150
|
return { valid: errors.length === 0, errors, warnings };
|
|
102
151
|
}
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -15,25 +15,10 @@ export declare function getLocalMcpPath(): Promise<string>;
|
|
|
15
15
|
export declare function getUluopsDir(): string;
|
|
16
16
|
/** Return the path to the UluOps install manifest file (new location). */
|
|
17
17
|
export declare function getManifestPath(): string;
|
|
18
|
+
/** Return the directory used as a process-level install/uninstall mutex. */
|
|
19
|
+
export declare function getInstallLockDir(): string;
|
|
18
20
|
/** Return the legacy manifest path for migration. */
|
|
19
21
|
export declare function getLegacyManifestPath(): string;
|
|
20
|
-
/**
|
|
21
|
-
* Return the backup directory for a harness's **config** files.
|
|
22
|
-
*
|
|
23
|
-
* Scope is deliberately narrow: this directory holds copies of mutable
|
|
24
|
-
* user-owned config surfaces (the MCP config file, the shell profile),
|
|
25
|
-
* NOT vendor-owned tool files in `~/.claude/tools/agent-metrics/`. Tool
|
|
26
|
-
* files are treated as disposable — they can be regenerated by re-running
|
|
27
|
-
* setup, and the source of truth lives in the npm-installed
|
|
28
|
-
* `@uluops/agent-metrics` package. Backing them up would require a
|
|
29
|
-
* different ritual (versioned snapshots tied to the manifest's
|
|
30
|
-
* hooksInstalledVersion) and is not provided here.
|
|
31
|
-
*
|
|
32
|
-
* Renaming this to `getConfigBackupDir` would be more honest but is a
|
|
33
|
-
* public surface change deferred until we have a tool-file backup to
|
|
34
|
-
* disambiguate against.
|
|
35
|
-
*/
|
|
36
|
-
export declare function getBackupDir(harnessName: string): string;
|
|
37
22
|
/** Detect the user's shell and return its name and profile path, or null if unsupported. */
|
|
38
23
|
export declare function getShellProfile(): {
|
|
39
24
|
shell: string;
|
package/dist/lib/paths.js
CHANGED
|
@@ -90,29 +90,14 @@ export function getUluopsDir() {
|
|
|
90
90
|
export function getManifestPath() {
|
|
91
91
|
return join(getUluopsDir(), "manifest.json");
|
|
92
92
|
}
|
|
93
|
+
/** Return the directory used as a process-level install/uninstall mutex. */
|
|
94
|
+
export function getInstallLockDir() {
|
|
95
|
+
return join(getUluopsDir(), "install.lock");
|
|
96
|
+
}
|
|
93
97
|
/** Return the legacy manifest path for migration. */
|
|
94
98
|
export function getLegacyManifestPath() {
|
|
95
99
|
return join(getClaudeHome(), "uluops-manifest.json");
|
|
96
100
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Return the backup directory for a harness's **config** files.
|
|
99
|
-
*
|
|
100
|
-
* Scope is deliberately narrow: this directory holds copies of mutable
|
|
101
|
-
* user-owned config surfaces (the MCP config file, the shell profile),
|
|
102
|
-
* NOT vendor-owned tool files in `~/.claude/tools/agent-metrics/`. Tool
|
|
103
|
-
* files are treated as disposable — they can be regenerated by re-running
|
|
104
|
-
* setup, and the source of truth lives in the npm-installed
|
|
105
|
-
* `@uluops/agent-metrics` package. Backing them up would require a
|
|
106
|
-
* different ritual (versioned snapshots tied to the manifest's
|
|
107
|
-
* hooksInstalledVersion) and is not provided here.
|
|
108
|
-
*
|
|
109
|
-
* Renaming this to `getConfigBackupDir` would be more honest but is a
|
|
110
|
-
* public surface change deferred until we have a tool-file backup to
|
|
111
|
-
* disambiguate against.
|
|
112
|
-
*/
|
|
113
|
-
export function getBackupDir(harnessName) {
|
|
114
|
-
return join(getUluopsDir(), "backups", harnessName);
|
|
115
|
-
}
|
|
116
101
|
/** Detect the user's shell and return its name and profile path, or null if unsupported. */
|
|
117
102
|
export function getShellProfile() {
|
|
118
103
|
const shell = process.env["SHELL"] ?? "";
|
|
@@ -82,7 +82,9 @@ export async function readSettings(path) {
|
|
|
82
82
|
* Write settings back to file with stable formatting.
|
|
83
83
|
*/
|
|
84
84
|
export async function writeSettings(path, settings) {
|
|
85
|
-
await atomicWrite(path, JSON.stringify(settings, null, 2) + "\n"
|
|
85
|
+
await atomicWrite(path, JSON.stringify(settings, null, 2) + "\n", {
|
|
86
|
+
mode: 0o600,
|
|
87
|
+
});
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
88
90
|
* Merge the UluOps hook into settings, preserving all other
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global install of the @uluops/agent-metrics CLI.
|
|
3
|
+
*
|
|
4
|
+
* Companion to src/steps/cli.ts (which handles @uluops/cli). The agent-metrics
|
|
5
|
+
* package is ALREADY copied into the harness tree by src/steps/metrics.ts
|
|
6
|
+
* (so the SubagentStop hook can invoke dist/hook.js at a fixed path), but
|
|
7
|
+
* that copy never goes on PATH. This step makes the `agent-metrics` command
|
|
8
|
+
* available to users who want to inspect the captures the hook produces.
|
|
9
|
+
*
|
|
10
|
+
* Gated externally by the helper in src/commands/helpers.ts — only invoked
|
|
11
|
+
* when the metrics hook actually got configured.
|
|
12
|
+
*/
|
|
13
|
+
export declare const AGENT_METRICS_PACKAGE = "@uluops/agent-metrics";
|
|
14
|
+
export declare const AGENT_METRICS_BIN = "agent-metrics";
|
|
15
|
+
export interface AgentMetricsCliExecutor {
|
|
16
|
+
/** Returns the installed CLI version, or null if `agent-metrics` is not on PATH or fails to run. */
|
|
17
|
+
detect: () => string | null;
|
|
18
|
+
/** Best-effort `npm install -g`. Returns ok + captured error for surface display. */
|
|
19
|
+
install: () => {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
};
|
|
23
|
+
/** Best-effort `npm uninstall -g`. Returns ok + captured error. */
|
|
24
|
+
uninstall: () => {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse `npm ls -g --json` output and extract the installed version of
|
|
31
|
+
* AGENT_METRICS_PACKAGE, or null if absent or unparseable.
|
|
32
|
+
*
|
|
33
|
+
* Exported for direct unit testing — keeps the JSON shape contract explicit.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseGlobalAgentMetricsVersion(stdout: string | undefined): string | null;
|
|
36
|
+
export declare function detectGlobalAgentMetrics(): string | null;
|
|
37
|
+
/** Default executor — queries npm directly to avoid npx-transient-PATH false positives. */
|
|
38
|
+
export declare const defaultAgentMetricsExecutor: AgentMetricsCliExecutor;
|
|
39
|
+
export interface AgentMetricsCliInstallResult {
|
|
40
|
+
/** `agent-metrics` is on PATH after this step, regardless of how it got there. */
|
|
41
|
+
installed: boolean;
|
|
42
|
+
/** Version string from `agent-metrics --version`, if detectable. */
|
|
43
|
+
version: string | null;
|
|
44
|
+
/** True when `agent-metrics` was already on PATH before we did anything. */
|
|
45
|
+
alreadyPresent: boolean;
|
|
46
|
+
/** Set when our `npm install -g` attempt failed; caller decides how to surface. */
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Install `@uluops/agent-metrics` globally if not already present.
|
|
51
|
+
*
|
|
52
|
+
* Mirrors `installCli` semantics — never aborts the parent setup flow:
|
|
53
|
+
* - If `agent-metrics` is already on PATH, returns `{ installed: true, alreadyPresent: true }` without touching npm.
|
|
54
|
+
* - If `npm install -g` fails, returns `{ installed: false, error }` so the caller can warn-and-continue.
|
|
55
|
+
* - In dryRun mode, no executor calls happen.
|
|
56
|
+
*/
|
|
57
|
+
export declare function installAgentMetricsCli(opts: {
|
|
58
|
+
dryRun: boolean;
|
|
59
|
+
executor?: AgentMetricsCliExecutor;
|
|
60
|
+
}): Promise<AgentMetricsCliInstallResult>;
|
|
61
|
+
export interface AgentMetricsCliUninstallResult {
|
|
62
|
+
removed: boolean;
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Uninstall `@uluops/agent-metrics` globally. Best-effort: if the package
|
|
67
|
+
* isn't there, npm exits non-zero on some platforms — we treat that as success.
|
|
68
|
+
*/
|
|
69
|
+
export declare function uninstallAgentMetricsCli(opts: {
|
|
70
|
+
dryRun: boolean;
|
|
71
|
+
executor?: AgentMetricsCliExecutor;
|
|
72
|
+
}): Promise<AgentMetricsCliUninstallResult>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Global install of the @uluops/agent-metrics CLI.
|
|
4
|
+
*
|
|
5
|
+
* Companion to src/steps/cli.ts (which handles @uluops/cli). The agent-metrics
|
|
6
|
+
* package is ALREADY copied into the harness tree by src/steps/metrics.ts
|
|
7
|
+
* (so the SubagentStop hook can invoke dist/hook.js at a fixed path), but
|
|
8
|
+
* that copy never goes on PATH. This step makes the `agent-metrics` command
|
|
9
|
+
* available to users who want to inspect the captures the hook produces.
|
|
10
|
+
*
|
|
11
|
+
* Gated externally by the helper in src/commands/helpers.ts — only invoked
|
|
12
|
+
* when the metrics hook actually got configured.
|
|
13
|
+
*/
|
|
14
|
+
export const AGENT_METRICS_PACKAGE = "@uluops/agent-metrics";
|
|
15
|
+
export const AGENT_METRICS_BIN = "agent-metrics";
|
|
16
|
+
/**
|
|
17
|
+
* Parse `npm ls -g --json` output and extract the installed version of
|
|
18
|
+
* AGENT_METRICS_PACKAGE, or null if absent or unparseable.
|
|
19
|
+
*
|
|
20
|
+
* Exported for direct unit testing — keeps the JSON shape contract explicit.
|
|
21
|
+
*/
|
|
22
|
+
export function parseGlobalAgentMetricsVersion(stdout) {
|
|
23
|
+
if (!stdout)
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(stdout);
|
|
27
|
+
const entry = parsed.dependencies?.[AGENT_METRICS_PACKAGE];
|
|
28
|
+
return entry?.version ?? null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Detect whether `@uluops/agent-metrics` is installed in npm's GLOBAL prefix.
|
|
36
|
+
*
|
|
37
|
+
* IMPORTANT: this cannot be a simple `spawnSync("agent-metrics", ["--version"])`.
|
|
38
|
+
* `@uluops/agent-metrics` is a runtime dependency of `@uluops/setup` (used by
|
|
39
|
+
* `findMetricsSource` to resolve files to copy into the harness tree). When
|
|
40
|
+
* setup runs under `npx @uluops/setup`, npx prepends its transient cache
|
|
41
|
+
* `.bin/` to PATH for the spawned process — `agent-metrics` resolves there
|
|
42
|
+
* even when the user has nothing installed globally. The check then returns
|
|
43
|
+
* "already installed" and setup skips the global install, leaving the user
|
|
44
|
+
* with `command not found` after npx exits. This was the actual bug behavior
|
|
45
|
+
* observed in v0.7.0 on the first ship.
|
|
46
|
+
*
|
|
47
|
+
* We query npm directly via `npm ls -g --depth=0 --json` — answers the actual
|
|
48
|
+
* question ("is it in the user's global install") instead of a PATH-resolution
|
|
49
|
+
* proxy. `npm ls` exits non-zero when the queried package is missing but still
|
|
50
|
+
* emits valid JSON, so we rely on the JSON content rather than the exit code.
|
|
51
|
+
*/
|
|
52
|
+
/** Same bounds as the @uluops/cli installer — see src/steps/cli.ts rationale. */
|
|
53
|
+
const NPM_TIMEOUT_MS = 5 * 60_000;
|
|
54
|
+
const DETECT_TIMEOUT_MS = 30_000;
|
|
55
|
+
function summarizeNpmResult(r, op) {
|
|
56
|
+
if (r.status === 0)
|
|
57
|
+
return { ok: true };
|
|
58
|
+
if (r.signal === "SIGTERM" && r.status === null) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: `npm ${op} exceeded ${NPM_TIMEOUT_MS / 1000}s timeout and was terminated`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const stderr = (r.stderr ?? "").toString().trim();
|
|
65
|
+
const stdout = (r.stdout ?? "").toString().trim();
|
|
66
|
+
return { ok: false, error: stderr || stdout || `exit ${r.status}` };
|
|
67
|
+
}
|
|
68
|
+
export function detectGlobalAgentMetrics() {
|
|
69
|
+
const r = spawnSync("npm", ["ls", "-g", AGENT_METRICS_PACKAGE, "--depth=0", "--json"], {
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
72
|
+
timeout: DETECT_TIMEOUT_MS,
|
|
73
|
+
});
|
|
74
|
+
return parseGlobalAgentMetricsVersion(r.stdout);
|
|
75
|
+
}
|
|
76
|
+
/** Default executor — queries npm directly to avoid npx-transient-PATH false positives. */
|
|
77
|
+
export const defaultAgentMetricsExecutor = {
|
|
78
|
+
detect: detectGlobalAgentMetrics,
|
|
79
|
+
install: () => {
|
|
80
|
+
const r = spawnSync("npm", ["install", "-g", AGENT_METRICS_PACKAGE], {
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
83
|
+
timeout: NPM_TIMEOUT_MS,
|
|
84
|
+
});
|
|
85
|
+
return summarizeNpmResult(r, "install");
|
|
86
|
+
},
|
|
87
|
+
uninstall: () => {
|
|
88
|
+
const r = spawnSync("npm", ["uninstall", "-g", AGENT_METRICS_PACKAGE], {
|
|
89
|
+
encoding: "utf-8",
|
|
90
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
91
|
+
timeout: NPM_TIMEOUT_MS,
|
|
92
|
+
});
|
|
93
|
+
return summarizeNpmResult(r, "uninstall");
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Install `@uluops/agent-metrics` globally if not already present.
|
|
98
|
+
*
|
|
99
|
+
* Mirrors `installCli` semantics — never aborts the parent setup flow:
|
|
100
|
+
* - If `agent-metrics` is already on PATH, returns `{ installed: true, alreadyPresent: true }` without touching npm.
|
|
101
|
+
* - If `npm install -g` fails, returns `{ installed: false, error }` so the caller can warn-and-continue.
|
|
102
|
+
* - In dryRun mode, no executor calls happen.
|
|
103
|
+
*/
|
|
104
|
+
export async function installAgentMetricsCli(opts) {
|
|
105
|
+
const executor = opts.executor ?? defaultAgentMetricsExecutor;
|
|
106
|
+
const existing = executor.detect();
|
|
107
|
+
if (existing !== null) {
|
|
108
|
+
return { installed: true, version: existing, alreadyPresent: true };
|
|
109
|
+
}
|
|
110
|
+
if (opts.dryRun) {
|
|
111
|
+
return { installed: false, version: null, alreadyPresent: false };
|
|
112
|
+
}
|
|
113
|
+
const res = executor.install();
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
return {
|
|
116
|
+
installed: false,
|
|
117
|
+
version: null,
|
|
118
|
+
alreadyPresent: false,
|
|
119
|
+
error: res.error,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const after = executor.detect();
|
|
123
|
+
return {
|
|
124
|
+
installed: after !== null,
|
|
125
|
+
version: after,
|
|
126
|
+
alreadyPresent: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Uninstall `@uluops/agent-metrics` globally. Best-effort: if the package
|
|
131
|
+
* isn't there, npm exits non-zero on some platforms — we treat that as success.
|
|
132
|
+
*/
|
|
133
|
+
export async function uninstallAgentMetricsCli(opts) {
|
|
134
|
+
const executor = opts.executor ?? defaultAgentMetricsExecutor;
|
|
135
|
+
if (opts.dryRun)
|
|
136
|
+
return { removed: true };
|
|
137
|
+
const before = executor.detect();
|
|
138
|
+
if (before === null)
|
|
139
|
+
return { removed: true };
|
|
140
|
+
const res = executor.uninstall();
|
|
141
|
+
if (res.ok)
|
|
142
|
+
return { removed: true };
|
|
143
|
+
const after = executor.detect();
|
|
144
|
+
if (after === null)
|
|
145
|
+
return { removed: true };
|
|
146
|
+
return { removed: false, error: res.error };
|
|
147
|
+
}
|
package/dist/steps/agents.d.ts
CHANGED
|
@@ -4,6 +4,17 @@ export interface AgentsResult {
|
|
|
4
4
|
skipped: number;
|
|
5
5
|
removed: number;
|
|
6
6
|
files: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Per-file copy failures. The loop continues past errors so a single bad
|
|
9
|
+
* file (EACCES, ENOSPC, ENAMETOOLONG) cannot abort the whole install and
|
|
10
|
+
* leave the destination half-populated. The caller surfaces these via
|
|
11
|
+
* `warn()` and a re-run will pick up the failed files. Files in this list
|
|
12
|
+
* are NOT counted in `copied` (or `skipped`).
|
|
13
|
+
*/
|
|
14
|
+
failures: {
|
|
15
|
+
file: string;
|
|
16
|
+
error: string;
|
|
17
|
+
}[];
|
|
7
18
|
}
|
|
8
19
|
/** Copy pre-rendered agent definitions from harness-specific assets to the target directory. */
|
|
9
20
|
export declare function installAgents(profile: HarnessProfile, localDefs: boolean, dryRun: boolean, existingManifestAgents?: string[]): Promise<AgentsResult>;
|
package/dist/steps/agents.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readdir, mkdir
|
|
1
|
+
import { readdir, mkdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { ASSETS_DIR, findProjectRoot } from "../lib/paths.js";
|
|
4
|
-
import { copyIfChanged, unlinkFiles } from "../lib/file-ops.js";
|
|
4
|
+
import { copyIfChanged, unlinkFiles, removeStaleFiles, } from "../lib/file-ops.js";
|
|
5
5
|
/** Copy pre-rendered agent definitions from harness-specific assets to the target directory. */
|
|
6
6
|
export async function installAgents(profile, localDefs, dryRun, existingManifestAgents) {
|
|
7
7
|
const srcDir = join(ASSETS_DIR, profile.name, "agents");
|
|
@@ -17,35 +17,40 @@ export async function installAgents(profile, localDefs, dryRun, existingManifest
|
|
|
17
17
|
files = (await readdir(srcDir)).filter((f) => f.endsWith(ext));
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
|
-
return { copied: 0, skipped: 0, removed: 0, files: [] };
|
|
20
|
+
return { copied: 0, skipped: 0, removed: 0, files: [], failures: [] };
|
|
21
21
|
}
|
|
22
22
|
let copied = 0;
|
|
23
23
|
let skipped = 0;
|
|
24
|
+
const installedFiles = [];
|
|
25
|
+
const failures = [];
|
|
24
26
|
for (const file of files) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
copied
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
try {
|
|
28
|
+
const result = await copyIfChanged(join(srcDir, file), join(destDir, file), dryRun);
|
|
29
|
+
if (result === "copied")
|
|
30
|
+
copied++;
|
|
31
|
+
else
|
|
32
|
+
skipped++;
|
|
33
|
+
// Only files that actually landed on disk enter installedFiles.
|
|
34
|
+
// A failed copy must not appear here — the manifest treats this list
|
|
35
|
+
// as authoritative, and uninstall would later attempt to unlink a
|
|
36
|
+
// file that was never written. Aligned with installCommands /
|
|
37
|
+
// installSkills behavior; required by the multi-target install
|
|
38
|
+
// partial-state contract (§7.6.2 / §7.6.6).
|
|
39
|
+
installedFiles.push(file);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
// Continue past per-file failures so one bad file (EACCES, ENOSPC,
|
|
43
|
+
// ENAMETOOLONG) does not abort the whole install and leave the
|
|
44
|
+
// destination half-populated. The caller `warn()`s these and a re-run
|
|
45
|
+
// will retry — setup is idempotent on success.
|
|
46
|
+
failures.push({
|
|
47
|
+
file,
|
|
48
|
+
error: err instanceof Error ? err.message : String(err),
|
|
49
|
+
});
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
|
-
|
|
52
|
+
const removed = await removeStaleFiles(destDir, existingManifestAgents, installedFiles, dryRun);
|
|
53
|
+
return { copied, skipped, removed, files: installedFiles, failures };
|
|
49
54
|
}
|
|
50
55
|
/** Remove previously installed agent files by name. */
|
|
51
56
|
export async function uninstallAgents(files, defsPath) {
|
package/dist/steps/auth.d.ts
CHANGED
|
@@ -8,6 +8,19 @@ export interface AuthResult {
|
|
|
8
8
|
* Existence-only — does not validate the file's shape or contents.
|
|
9
9
|
*/
|
|
10
10
|
export declare function hasCredentialsFile(): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Persist an API key to ~/.uluops/credentials.json so @uluops/cli and the SDK
|
|
13
|
+
* can resolve it from disk without ULUOPS_API_KEY in the shell environment.
|
|
14
|
+
*
|
|
15
|
+
* Merges into the existing file when present: only the `default` profile is
|
|
16
|
+
* replaced; any other named profiles are preserved. Creates ~/.uluops/ with
|
|
17
|
+
* mode 0o700 if missing. Writes the file at 0o600 via atomicWrite.
|
|
18
|
+
*/
|
|
19
|
+
export declare function writeCredentialsFile(apiKey: string, opts?: {
|
|
20
|
+
email?: string | null;
|
|
21
|
+
source?: "signup" | "flag" | "prompt";
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
}): Promise<void>;
|
|
11
24
|
/**
|
|
12
25
|
* Resolve API key from flags, env, credentials file, or interactive prompt.
|
|
13
26
|
*/
|