@uluops/setup 0.2.0 → 0.4.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.
- package/README.md +56 -53
- package/assets/agents/anxiety-reader-agent.md +464 -0
- package/assets/commands/agents/anxiety-reader.md +160 -0
- package/assets/commands/agents/api-contract.md +1 -0
- package/assets/commands/agents/architect.md +1 -0
- package/assets/commands/agents/aristotle-analyst.md +1 -0
- package/assets/commands/agents/aristotle-explorer.md +1 -0
- package/assets/commands/agents/aristotle-forecaster.md +1 -0
- package/assets/commands/agents/aristotle-validator.md +1 -0
- package/assets/commands/agents/assumption-excavator.md +1 -0
- package/assets/commands/agents/audit.md +1 -0
- package/assets/commands/agents/{validate.md → code-validate.md} +6 -5
- package/assets/commands/agents/docs-validate.md +1 -0
- package/assets/commands/agents/frontend.md +1 -0
- package/assets/commands/agents/mcp-validate.md +1 -0
- package/assets/commands/agents/optimize.md +1 -0
- package/assets/commands/agents/pattern-analyzer.md +1 -0
- package/assets/commands/agents/prompt-quality.md +1 -0
- package/assets/commands/agents/prompt-validate.md +1 -0
- package/assets/commands/agents/public-interface.md +1 -0
- package/assets/commands/agents/release.md +1 -0
- package/assets/commands/agents/security.md +1 -0
- package/assets/commands/agents/test-review.md +1 -0
- package/assets/commands/agents/type-safety.md +1 -0
- package/assets/commands/agents/workflow-synthesis.md +1 -0
- package/assets/commands/pipelines/aristotle.md +143 -0
- package/assets/commands/pipelines/ship.md +188 -0
- package/assets/commands/workflows/prompt-audit.md +37 -747
- package/dist/cli.js +251 -207
- package/dist/harnesses/claude-code.d.ts +8 -0
- package/dist/harnesses/claude-code.js +72 -0
- package/dist/harnesses/codex.d.ts +15 -0
- package/dist/harnesses/codex.js +53 -0
- package/dist/harnesses/gemini-cli.d.ts +16 -0
- package/dist/harnesses/gemini-cli.js +54 -0
- package/dist/harnesses/index.d.ts +18 -0
- package/dist/harnesses/index.js +45 -0
- package/dist/harnesses/opencode.d.ts +14 -0
- package/dist/harnesses/opencode.js +130 -0
- package/dist/harnesses/types.d.ts +87 -0
- package/dist/harnesses/types.js +24 -0
- package/dist/lib/agent-transform.d.ts +12 -0
- package/dist/lib/agent-transform.js +129 -0
- package/dist/lib/asset-catalog.d.ts +9 -0
- package/dist/lib/asset-catalog.js +56 -0
- package/dist/lib/atomic-write.d.ts +11 -0
- package/dist/lib/atomic-write.js +28 -0
- package/dist/lib/config-merger.d.ts +7 -1
- package/dist/lib/config-merger.js +34 -5
- package/dist/lib/display.d.ts +14 -0
- package/dist/lib/display.js +66 -0
- package/dist/lib/file-ops.d.ts +6 -0
- package/dist/lib/file-ops.js +22 -1
- package/dist/lib/hash.d.ts +1 -0
- package/dist/lib/hash.js +1 -0
- package/dist/lib/health.d.ts +2 -0
- package/dist/lib/health.js +10 -0
- package/dist/lib/manifest.d.ts +22 -5
- package/dist/lib/manifest.js +148 -13
- package/dist/lib/paths.d.ts +15 -3
- package/dist/lib/paths.js +71 -13
- package/dist/lib/settings-merger.d.ts +9 -1
- package/dist/lib/settings-merger.js +45 -17
- package/dist/steps/agents.d.ts +5 -1
- package/dist/steps/agents.js +59 -9
- package/dist/steps/auth.js +26 -10
- package/dist/steps/commands.d.ts +6 -1
- package/dist/steps/commands.js +87 -9
- package/dist/steps/detect.d.ts +3 -0
- package/dist/steps/detect.js +7 -0
- package/dist/steps/mcp.d.ts +6 -2
- package/dist/steps/mcp.js +46 -21
- package/dist/steps/metrics.d.ts +14 -10
- package/dist/steps/metrics.js +59 -89
- package/dist/steps/shell.d.ts +2 -0
- package/dist/steps/shell.js +16 -9
- package/dist/steps/signup.d.ts +6 -3
- package/dist/steps/signup.js +26 -14
- package/dist/steps/verify.d.ts +2 -2
- package/dist/steps/verify.js +84 -117
- package/package.json +32 -7
- package/assets/commands/workflows/aristotle.md +0 -543
- package/assets/commands/workflows/ship.md +0 -721
- package/dist/test/auth.test.d.ts +0 -1
- package/dist/test/auth.test.js +0 -43
- package/dist/test/config-io.test.d.ts +0 -1
- package/dist/test/config-io.test.js +0 -56
- package/dist/test/config-merger.test.d.ts +0 -1
- package/dist/test/config-merger.test.js +0 -94
- package/dist/test/detect.test.d.ts +0 -1
- package/dist/test/detect.test.js +0 -25
- package/dist/test/file-ops.test.d.ts +0 -1
- package/dist/test/file-ops.test.js +0 -100
- package/dist/test/hash.test.d.ts +0 -1
- package/dist/test/hash.test.js +0 -14
- package/dist/test/manifest.test.d.ts +0 -1
- package/dist/test/manifest.test.js +0 -78
- package/dist/test/paths.test.d.ts +0 -1
- package/dist/test/paths.test.js +0 -30
- package/dist/test/settings-merger.test.d.ts +0 -1
- package/dist/test/settings-merger.test.js +0 -167
- package/dist/test/shell-profile.test.d.ts +0 -1
- package/dist/test/shell-profile.test.js +0 -40
- package/dist/test/shell.test.d.ts +0 -1
- package/dist/test/shell.test.js +0 -71
- package/dist/test/signup.test.d.ts +0 -1
- package/dist/test/signup.test.js +0 -83
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CommandEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
model: string;
|
|
5
|
+
}
|
|
6
|
+
/** Get all agent command entries from assets. */
|
|
7
|
+
export declare function getAgentCommands(): Promise<CommandEntry[]>;
|
|
8
|
+
/** Get all workflow command entries from assets. */
|
|
9
|
+
export declare function getWorkflowCommands(): Promise<CommandEntry[]>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ASSETS_DIR } from "./paths.js";
|
|
4
|
+
/**
|
|
5
|
+
* Parse YAML-like frontmatter from a markdown file.
|
|
6
|
+
* Returns key-value pairs between the opening and closing `---`.
|
|
7
|
+
*/
|
|
8
|
+
function parseFrontmatter(content) {
|
|
9
|
+
const lines = content.split("\n");
|
|
10
|
+
if (lines[0]?.trim() !== "---")
|
|
11
|
+
return {};
|
|
12
|
+
const result = {};
|
|
13
|
+
for (let i = 1; i < lines.length; i++) {
|
|
14
|
+
const line = lines[i];
|
|
15
|
+
if (line.trim() === "---")
|
|
16
|
+
break;
|
|
17
|
+
const colonIdx = line.indexOf(":");
|
|
18
|
+
if (colonIdx > 0) {
|
|
19
|
+
const key = line.slice(0, colonIdx).trim();
|
|
20
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
21
|
+
result[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
/** Scan a commands subdirectory and return sorted entries with frontmatter metadata. */
|
|
27
|
+
async function scanCommandDir(dir) {
|
|
28
|
+
let files;
|
|
29
|
+
try {
|
|
30
|
+
files = await readdir(dir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
37
|
+
const content = await readFile(join(dir, file), "utf-8");
|
|
38
|
+
const fm = parseFrontmatter(content);
|
|
39
|
+
if (fm["name"]) {
|
|
40
|
+
entries.push({
|
|
41
|
+
name: fm["name"],
|
|
42
|
+
description: fm["description"] ?? "",
|
|
43
|
+
model: fm["model"] ?? "sonnet",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
+
}
|
|
49
|
+
/** Get all agent command entries from assets. */
|
|
50
|
+
export async function getAgentCommands() {
|
|
51
|
+
return scanCommandDir(join(ASSETS_DIR, "commands", "agents"));
|
|
52
|
+
}
|
|
53
|
+
/** Get all workflow command entries from assets. */
|
|
54
|
+
export async function getWorkflowCommands() {
|
|
55
|
+
return scanCommandDir(join(ASSETS_DIR, "commands", "workflows"));
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic Write
|
|
3
|
+
*
|
|
4
|
+
* Write-to-temp-then-rename pattern to prevent partial writes
|
|
5
|
+
* from corrupting user config files.
|
|
6
|
+
*/
|
|
7
|
+
export interface AtomicWriteOptions {
|
|
8
|
+
/** File mode (permissions). Defaults to Node's default (0o666 before umask). */
|
|
9
|
+
mode?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function atomicWrite(path: string, content: string, options?: AtomicWriteOptions): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic Write
|
|
3
|
+
*
|
|
4
|
+
* Write-to-temp-then-rename pattern to prevent partial writes
|
|
5
|
+
* from corrupting user config files.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFile, rename, unlink, chmod } from "node:fs/promises";
|
|
8
|
+
export async function atomicWrite(path, content, options) {
|
|
9
|
+
const tmp = `${path}.uluops-tmp`;
|
|
10
|
+
try {
|
|
11
|
+
await writeFile(tmp, content, { encoding: "utf-8", mode: options?.mode });
|
|
12
|
+
if (options?.mode) {
|
|
13
|
+
// Ensure mode is applied even if umask is permissive
|
|
14
|
+
await chmod(tmp, options.mode);
|
|
15
|
+
}
|
|
16
|
+
await rename(tmp, path);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
// Clean up temp file on failure
|
|
20
|
+
try {
|
|
21
|
+
await unlink(tmp);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Temp file may not exist
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -3,12 +3,18 @@ interface McpServerConfig {
|
|
|
3
3
|
args: string[];
|
|
4
4
|
env: Record<string, string>;
|
|
5
5
|
}
|
|
6
|
-
interface ClaudeConfig {
|
|
6
|
+
export interface ClaudeConfig {
|
|
7
7
|
mcpServers?: Record<string, McpServerConfig>;
|
|
8
8
|
[key: string]: unknown;
|
|
9
9
|
}
|
|
10
|
+
/** Check whether the UluOps MCP client packages exist on the npm registry. Returns lists of available and missing packages. */
|
|
11
|
+
export declare function checkMcpPackageAvailability(): Promise<{
|
|
12
|
+
available: string[];
|
|
13
|
+
missing: string[];
|
|
14
|
+
}>;
|
|
10
15
|
/**
|
|
11
16
|
* Read an existing config file, or return empty object if it doesn't exist.
|
|
17
|
+
* Throws on malformed JSON to prevent silent data loss during merge+write.
|
|
12
18
|
*/
|
|
13
19
|
export declare function readConfig(path: string): Promise<ClaudeConfig>;
|
|
14
20
|
/**
|
|
@@ -1,15 +1,42 @@
|
|
|
1
|
-
import { readFile
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { atomicWrite } from "./atomic-write.js";
|
|
3
|
+
const MCP_PACKAGES = ["uluops-tracker-mcp-client", "uluops-registry-mcp-client"];
|
|
4
|
+
/** Check whether the UluOps MCP client packages exist on the npm registry. Returns lists of available and missing packages. */
|
|
5
|
+
export async function checkMcpPackageAvailability() {
|
|
6
|
+
const available = [];
|
|
7
|
+
const missing = [];
|
|
8
|
+
const results = await Promise.allSettled(MCP_PACKAGES.map((pkg) => fetch(`https://registry.npmjs.org/${pkg}`, {
|
|
9
|
+
method: "HEAD",
|
|
10
|
+
signal: AbortSignal.timeout(5000),
|
|
11
|
+
redirect: "follow",
|
|
12
|
+
}).then((res) => ({ pkg, ok: res.ok }))));
|
|
13
|
+
for (let i = 0; i < results.length; i++) {
|
|
14
|
+
const result = results[i];
|
|
15
|
+
if (result.status === "fulfilled" && result.value.ok) {
|
|
16
|
+
available.push(result.value.pkg);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const pkg = result.status === "fulfilled"
|
|
20
|
+
? result.value.pkg
|
|
21
|
+
: MCP_PACKAGES[i] ?? "unknown";
|
|
22
|
+
missing.push(pkg);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { available, missing };
|
|
26
|
+
}
|
|
2
27
|
/**
|
|
3
28
|
* Read an existing config file, or return empty object if it doesn't exist.
|
|
29
|
+
* Throws on malformed JSON to prevent silent data loss during merge+write.
|
|
4
30
|
*/
|
|
5
31
|
export async function readConfig(path) {
|
|
32
|
+
let raw;
|
|
6
33
|
try {
|
|
7
|
-
|
|
8
|
-
return JSON.parse(raw);
|
|
34
|
+
raw = await readFile(path, "utf-8");
|
|
9
35
|
}
|
|
10
36
|
catch {
|
|
11
|
-
return {};
|
|
37
|
+
return {}; // File doesn't exist — fresh config
|
|
12
38
|
}
|
|
39
|
+
return JSON.parse(raw);
|
|
13
40
|
}
|
|
14
41
|
/**
|
|
15
42
|
* Merge UluOps MCP server entries into a config, preserving all other keys.
|
|
@@ -59,5 +86,7 @@ export function removeUluopsMcp(config) {
|
|
|
59
86
|
* Write config back to file, preserving formatting.
|
|
60
87
|
*/
|
|
61
88
|
export async function writeConfig(path, config) {
|
|
62
|
-
await
|
|
89
|
+
await atomicWrite(path, JSON.stringify(config, null, 2) + "\n", {
|
|
90
|
+
mode: 0o600,
|
|
91
|
+
});
|
|
63
92
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HarnessProfile } from "../harnesses/index.js";
|
|
2
|
+
declare const ok: (msg: string) => void;
|
|
3
|
+
declare const warn: (msg: string) => void;
|
|
4
|
+
declare const fail: (msg: string) => void;
|
|
5
|
+
declare const info: (msg: string) => void;
|
|
6
|
+
export { ok, warn, fail, info };
|
|
7
|
+
export declare function printSetupSummary(opts: {
|
|
8
|
+
profile: HarnessProfile;
|
|
9
|
+
agentCount: number;
|
|
10
|
+
commandCount: number;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
export declare function maskKey(key: string): string;
|
|
14
|
+
export declare function printAgentList(): Promise<void>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { getAgentCommands, getWorkflowCommands } from "./asset-catalog.js";
|
|
3
|
+
const ok = (msg) => console.log(` ${chalk.green("✓")} ${msg}`);
|
|
4
|
+
const warn = (msg) => console.log(` ${chalk.yellow("⚠")} ${msg}`);
|
|
5
|
+
const fail = (msg) => console.log(` ${chalk.red("✗")} ${msg}`);
|
|
6
|
+
const info = (msg) => console.log(` ${msg}`);
|
|
7
|
+
export { ok, warn, fail, info };
|
|
8
|
+
export async function printSetupSummary(opts) {
|
|
9
|
+
console.log();
|
|
10
|
+
console.log(` ${chalk.dim("━".repeat(46))}`);
|
|
11
|
+
console.log();
|
|
12
|
+
const parts = [`${opts.agentCount} agents`];
|
|
13
|
+
if (opts.commandCount > 0)
|
|
14
|
+
parts.push(`${opts.commandCount} slash commands`);
|
|
15
|
+
if (opts.profile.hooks)
|
|
16
|
+
parts.push("metrics");
|
|
17
|
+
console.log(` ${chalk.bold("Setup complete!")} ${chalk.dim(`(${opts.profile.displayName})`)} ${parts.join(" · ")}`);
|
|
18
|
+
console.log();
|
|
19
|
+
if (opts.profile.name === "claude-code") {
|
|
20
|
+
await printAgentList();
|
|
21
|
+
}
|
|
22
|
+
const masked = maskKey(opts.apiKey);
|
|
23
|
+
info("For SDK/CLI usage, add to your shell profile:");
|
|
24
|
+
info(` ${chalk.cyan(`export ULUOPS_API_KEY="${masked}"`)}`);
|
|
25
|
+
console.log();
|
|
26
|
+
info(`Run again to update: ${chalk.cyan("npx @uluops/setup")}`);
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(` ${chalk.dim("━".repeat(46))}`);
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(` ${chalk.yellow.bold(`Restart ${opts.profile.displayName} to load agents.`)}`);
|
|
31
|
+
console.log();
|
|
32
|
+
}
|
|
33
|
+
export function maskKey(key) {
|
|
34
|
+
if (!key || key.length <= 4)
|
|
35
|
+
return "****";
|
|
36
|
+
const last4 = key.slice(-4);
|
|
37
|
+
return `${"*".repeat(Math.max(4, key.length - 4))}${last4}`;
|
|
38
|
+
}
|
|
39
|
+
export async function printAgentList() {
|
|
40
|
+
const workflows = await getWorkflowCommands();
|
|
41
|
+
const agents = await getAgentCommands();
|
|
42
|
+
if (workflows.length > 0) {
|
|
43
|
+
info(chalk.bold("WORKFLOWS"));
|
|
44
|
+
for (const wf of workflows) {
|
|
45
|
+
const cmd = `/workflows:${wf.name}`;
|
|
46
|
+
const desc = wf.description.length > 40
|
|
47
|
+
? wf.description.slice(0, 37) + "..."
|
|
48
|
+
: wf.description;
|
|
49
|
+
info(` ${chalk.cyan(cmd.padEnd(34))}${desc}`);
|
|
50
|
+
}
|
|
51
|
+
console.log();
|
|
52
|
+
}
|
|
53
|
+
if (agents.length > 0) {
|
|
54
|
+
info(`${chalk.bold("AGENTS")} (run individually)${" ".repeat(26)}${chalk.dim("MODEL")}`);
|
|
55
|
+
for (const agent of agents) {
|
|
56
|
+
const cmd = `/agents:${agent.name}`;
|
|
57
|
+
const desc = agent.description.length > 17
|
|
58
|
+
? agent.description.slice(0, 14) + "..."
|
|
59
|
+
: agent.description;
|
|
60
|
+
info(` ${chalk.cyan(cmd.padEnd(34))}${desc.padEnd(17)}${chalk.dim(agent.model)}`);
|
|
61
|
+
}
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
info(chalk.dim(` This is the starter set. Browse more agents at registry.uluops.ai`));
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
package/dist/lib/file-ops.d.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Copy a file if its content has changed (hash comparison). Returns "copied" or "skipped".
|
|
3
3
|
*/
|
|
4
4
|
export declare function copyIfChanged(srcPath: string, destPath: string, dryRun: boolean): Promise<"copied" | "skipped">;
|
|
5
|
+
/**
|
|
6
|
+
* Write content to a file if it differs from the current content (hash comparison).
|
|
7
|
+
* Returns "copied" or "skipped".
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeIfChanged(destPath: string, content: string, dryRun: boolean): Promise<"copied" | "skipped">;
|
|
5
10
|
/**
|
|
6
11
|
* Remove files from a directory. Returns count of successfully removed files.
|
|
7
12
|
*/
|
|
@@ -14,6 +19,7 @@ export declare function syncAssets(opts: {
|
|
|
14
19
|
srcDir: string;
|
|
15
20
|
destDir: string;
|
|
16
21
|
dryRun: boolean;
|
|
22
|
+
extension?: string;
|
|
17
23
|
oldManifestFiles?: string[];
|
|
18
24
|
}): Promise<{
|
|
19
25
|
copied: number;
|
package/dist/lib/file-ops.js
CHANGED
|
@@ -21,6 +21,26 @@ export async function copyIfChanged(srcPath, destPath, dryRun) {
|
|
|
21
21
|
}
|
|
22
22
|
return "copied";
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Write content to a file if it differs from the current content (hash comparison).
|
|
26
|
+
* Returns "copied" or "skipped".
|
|
27
|
+
*/
|
|
28
|
+
export async function writeIfChanged(destPath, content, dryRun) {
|
|
29
|
+
const newHash = fileHash(content);
|
|
30
|
+
try {
|
|
31
|
+
const existing = await readFile(destPath, "utf-8");
|
|
32
|
+
if (newHash === fileHash(existing)) {
|
|
33
|
+
return "skipped";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// File doesn't exist yet
|
|
38
|
+
}
|
|
39
|
+
if (!dryRun) {
|
|
40
|
+
await writeFile(destPath, content);
|
|
41
|
+
}
|
|
42
|
+
return "copied";
|
|
43
|
+
}
|
|
24
44
|
/**
|
|
25
45
|
* Remove files from a directory. Returns count of successfully removed files.
|
|
26
46
|
*/
|
|
@@ -46,7 +66,8 @@ export async function syncAssets(opts) {
|
|
|
46
66
|
if (!opts.dryRun) {
|
|
47
67
|
await mkdir(opts.destDir, { recursive: true });
|
|
48
68
|
}
|
|
49
|
-
const
|
|
69
|
+
const ext = opts.extension ?? ".md";
|
|
70
|
+
const assetFiles = (await readdir(opts.srcDir)).filter((f) => f.endsWith(ext));
|
|
50
71
|
let copied = 0;
|
|
51
72
|
let skipped = 0;
|
|
52
73
|
const errors = [];
|
package/dist/lib/hash.d.ts
CHANGED
package/dist/lib/hash.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Shared health check timeout, configurable via ULUOPS_HEALTH_TIMEOUT env var. */
|
|
2
|
+
export function getHealthTimeout() {
|
|
3
|
+
const env = process.env["ULUOPS_HEALTH_TIMEOUT"];
|
|
4
|
+
if (env) {
|
|
5
|
+
const ms = Number(env);
|
|
6
|
+
if (Number.isFinite(ms) && ms > 0)
|
|
7
|
+
return ms;
|
|
8
|
+
}
|
|
9
|
+
return 10_000;
|
|
10
|
+
}
|
package/dist/lib/manifest.d.ts
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** Per-harness installation state. */
|
|
2
|
+
export interface HarnessManifest {
|
|
3
3
|
installedAt: string;
|
|
4
|
+
setupVersion: string;
|
|
4
5
|
mcpScope: "global" | "local";
|
|
5
6
|
mcpConfigPath: string;
|
|
6
7
|
defsScope: "global" | "local";
|
|
7
8
|
defsPath: string;
|
|
8
|
-
shellModified: boolean;
|
|
9
9
|
agents: string[];
|
|
10
10
|
commands: string[];
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
hooksInstalled: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Top-level manifest with per-harness entries. */
|
|
14
|
+
export interface Manifest {
|
|
15
|
+
version: string;
|
|
16
|
+
installedAt: string;
|
|
17
|
+
shellModified: boolean;
|
|
18
|
+
harnesses: Record<string, HarnessManifest>;
|
|
19
|
+
contentHash?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ManifestValidationResult {
|
|
22
|
+
valid: boolean;
|
|
23
|
+
errors: string[];
|
|
24
|
+
warnings: string[];
|
|
13
25
|
}
|
|
26
|
+
/** Validate a manifest against the current filesystem state. */
|
|
27
|
+
export declare function validateManifest(manifest: Manifest): Promise<ManifestValidationResult>;
|
|
28
|
+
/** Load the install manifest. Tries new location first, falls back to legacy, auto-migrates. */
|
|
14
29
|
export declare function loadManifest(): Promise<Manifest | null>;
|
|
30
|
+
/** Save the install manifest to ~/.uluops/manifest.json. Creates directory if needed. */
|
|
15
31
|
export declare function saveManifest(manifest: Manifest): Promise<void>;
|
|
32
|
+
/** Delete the install manifest file from disk. Tries both locations. */
|
|
16
33
|
export declare function deleteManifest(): Promise<void>;
|
package/dist/lib/manifest.js
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
import { readFile,
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { readFile, unlink, access, mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getManifestPath, getLegacyManifestPath, getUluopsDir } from "./paths.js";
|
|
4
|
+
import { fileHash } from "./hash.js";
|
|
5
|
+
import { atomicWrite } from "./atomic-write.js";
|
|
6
|
+
function isNewManifest(obj) {
|
|
7
|
+
if (typeof obj !== "object" || obj === null)
|
|
8
|
+
return false;
|
|
9
|
+
const m = obj;
|
|
10
|
+
if (typeof m["version"] !== "string" ||
|
|
11
|
+
typeof m["installedAt"] !== "string" ||
|
|
12
|
+
typeof m["shellModified"] !== "boolean" ||
|
|
13
|
+
typeof m["harnesses"] !== "object" ||
|
|
14
|
+
m["harnesses"] === null)
|
|
15
|
+
return false;
|
|
16
|
+
// Validate at least one harness entry has required fields
|
|
17
|
+
const harnesses = m["harnesses"];
|
|
18
|
+
for (const h of Object.values(harnesses)) {
|
|
19
|
+
if (typeof h !== "object" || h === null)
|
|
20
|
+
return false;
|
|
21
|
+
const hm = h;
|
|
22
|
+
if (typeof hm["mcpConfigPath"] !== "string" || typeof hm["defsPath"] !== "string")
|
|
23
|
+
return false;
|
|
24
|
+
if (!Array.isArray(hm["agents"]) || !Array.isArray(hm["commands"]))
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
function isLegacyManifest(obj) {
|
|
4
30
|
if (typeof obj !== "object" || obj === null)
|
|
5
31
|
return false;
|
|
6
32
|
const m = obj;
|
|
@@ -9,26 +35,135 @@ function isManifest(obj) {
|
|
|
9
35
|
typeof m["mcpConfigPath"] === "string" &&
|
|
10
36
|
typeof m["defsPath"] === "string" &&
|
|
11
37
|
Array.isArray(m["agents"]) &&
|
|
12
|
-
Array.isArray(m["commands"])
|
|
38
|
+
Array.isArray(m["commands"]) &&
|
|
39
|
+
!("harnesses" in m));
|
|
13
40
|
}
|
|
14
|
-
|
|
41
|
+
function migrateManifest(old) {
|
|
42
|
+
return {
|
|
43
|
+
version: old.version,
|
|
44
|
+
installedAt: old.installedAt,
|
|
45
|
+
shellModified: old.shellModified,
|
|
46
|
+
harnesses: {
|
|
47
|
+
"claude-code": {
|
|
48
|
+
installedAt: old.installedAt,
|
|
49
|
+
setupVersion: old.version,
|
|
50
|
+
mcpScope: old.mcpScope,
|
|
51
|
+
mcpConfigPath: old.mcpConfigPath,
|
|
52
|
+
defsScope: old.defsScope,
|
|
53
|
+
defsPath: old.defsPath,
|
|
54
|
+
agents: old.agents,
|
|
55
|
+
commands: old.commands,
|
|
56
|
+
hooksInstalled: old.metricsHookInstalled ?? false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Validate a manifest against the current filesystem state. */
|
|
62
|
+
export async function validateManifest(manifest) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
for (const [harnessName, hm] of Object.entries(manifest.harnesses)) {
|
|
66
|
+
const mcpExists = await pathExists(hm.mcpConfigPath);
|
|
67
|
+
if (!mcpExists) {
|
|
68
|
+
errors.push(`[${harnessName}] MCP config path does not exist: ${hm.mcpConfigPath}`);
|
|
69
|
+
}
|
|
70
|
+
const defsExists = await pathExists(hm.defsPath);
|
|
71
|
+
if (!defsExists) {
|
|
72
|
+
errors.push(`[${harnessName}] Defs path does not exist: ${hm.defsPath}`);
|
|
73
|
+
}
|
|
74
|
+
if (hm.agents.length > 0 && defsExists) {
|
|
75
|
+
const missing = await findMissingFiles(hm.defsPath, "agents", hm.agents);
|
|
76
|
+
if (missing.length > 0) {
|
|
77
|
+
warnings.push(`[${harnessName}] Agent files missing from disk: ${missing.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (hm.commands.length > 0 && defsExists) {
|
|
81
|
+
const missing = await findMissingFiles(hm.defsPath, "commands", hm.commands);
|
|
82
|
+
if (missing.length > 0) {
|
|
83
|
+
warnings.push(`[${harnessName}] Command files missing from disk: ${missing.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const manifestPath = getManifestPath();
|
|
15
88
|
try {
|
|
16
|
-
const raw = await readFile(
|
|
89
|
+
const raw = await readFile(manifestPath, "utf-8");
|
|
17
90
|
const parsed = JSON.parse(raw);
|
|
18
|
-
|
|
91
|
+
const { contentHash: storedHash, ...withoutHash } = parsed;
|
|
92
|
+
const canonical = JSON.stringify(withoutHash, null, 2) + "\n";
|
|
93
|
+
const currentHash = fileHash(canonical);
|
|
94
|
+
if (storedHash && storedHash !== currentHash) {
|
|
95
|
+
warnings.push("Manifest file has been modified since installation — content hash mismatch");
|
|
96
|
+
}
|
|
19
97
|
}
|
|
20
98
|
catch {
|
|
21
|
-
|
|
99
|
+
warnings.push("Cannot read manifest file to verify content hash");
|
|
22
100
|
}
|
|
101
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
23
102
|
}
|
|
24
|
-
|
|
25
|
-
|
|
103
|
+
async function pathExists(p) {
|
|
104
|
+
try {
|
|
105
|
+
await access(p);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
26
111
|
}
|
|
27
|
-
|
|
112
|
+
async function findMissingFiles(baseDir, subDir, files) {
|
|
113
|
+
const missing = [];
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
if (!(await pathExists(join(baseDir, subDir, file)))) {
|
|
116
|
+
missing.push(file);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return missing;
|
|
120
|
+
}
|
|
121
|
+
async function readManifestFile(path) {
|
|
28
122
|
try {
|
|
29
|
-
await
|
|
123
|
+
const raw = await readFile(path, "utf-8");
|
|
124
|
+
return JSON.parse(raw);
|
|
30
125
|
}
|
|
31
126
|
catch {
|
|
32
|
-
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Load the install manifest. Tries new location first, falls back to legacy, auto-migrates. */
|
|
131
|
+
export async function loadManifest() {
|
|
132
|
+
// Try new location first
|
|
133
|
+
const newData = await readManifestFile(getManifestPath());
|
|
134
|
+
if (newData && isNewManifest(newData))
|
|
135
|
+
return newData;
|
|
136
|
+
// Fall back to legacy location
|
|
137
|
+
const legacyData = await readManifestFile(getLegacyManifestPath());
|
|
138
|
+
if (legacyData && isLegacyManifest(legacyData)) {
|
|
139
|
+
return migrateManifest(legacyData);
|
|
140
|
+
}
|
|
141
|
+
// Also check if legacy location has new format (written by newer version but not yet moved)
|
|
142
|
+
if (legacyData && isNewManifest(legacyData))
|
|
143
|
+
return legacyData;
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
/** Save the install manifest to ~/.uluops/manifest.json. Creates directory if needed. */
|
|
147
|
+
export async function saveManifest(manifest) {
|
|
148
|
+
const dir = getUluopsDir();
|
|
149
|
+
await mkdir(dir, { recursive: true });
|
|
150
|
+
// Serialize without hash, compute hash of that content, embed it.
|
|
151
|
+
// Validation compares the stored hash against a re-hash of the file
|
|
152
|
+
// with contentHash stripped, so both sides agree on the input.
|
|
153
|
+
const { contentHash: _, ...withoutHash } = manifest;
|
|
154
|
+
const canonical = JSON.stringify(withoutHash, null, 2) + "\n";
|
|
155
|
+
const hash = fileHash(canonical);
|
|
156
|
+
const final = JSON.stringify({ ...withoutHash, contentHash: hash }, null, 2) + "\n";
|
|
157
|
+
await atomicWrite(getManifestPath(), final);
|
|
158
|
+
}
|
|
159
|
+
/** Delete the install manifest file from disk. Tries both locations. */
|
|
160
|
+
export async function deleteManifest() {
|
|
161
|
+
for (const path of [getManifestPath(), getLegacyManifestPath()]) {
|
|
162
|
+
try {
|
|
163
|
+
await unlink(path);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Already gone
|
|
167
|
+
}
|
|
33
168
|
}
|
|
34
169
|
}
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
export declare const PACKAGE_ROOT: string;
|
|
3
3
|
/** Assets directory containing pre-rendered .md files */
|
|
4
4
|
export declare const ASSETS_DIR: string;
|
|
5
|
+
export declare function setProjectRoot(path: string | null): void;
|
|
6
|
+
/** Walk upward from cwd to find the nearest directory containing .git or package.json. Falls back to cwd. */
|
|
7
|
+
export declare function findProjectRoot(): Promise<string>;
|
|
8
|
+
/** Return the Claude config home directory (~/.claude by default, or CLAUDE_HOME env override). */
|
|
5
9
|
export declare function getClaudeHome(): string;
|
|
10
|
+
/** Return the path to Claude's global config file (~/.claude.json by default, or CLAUDE_JSON_PATH env override). */
|
|
6
11
|
export declare function getClaudeJsonPath(): string;
|
|
7
|
-
|
|
12
|
+
/** Return the path to the project-local MCP config file (.mcp.json in project root). */
|
|
13
|
+
export declare function getLocalMcpPath(): Promise<string>;
|
|
14
|
+
/** Return the UluOps state directory (~/.uluops/). Harness-neutral. */
|
|
15
|
+
export declare function getUluopsDir(): string;
|
|
16
|
+
/** Return the path to the UluOps install manifest file (new location). */
|
|
8
17
|
export declare function getManifestPath(): string;
|
|
9
|
-
|
|
10
|
-
export declare function
|
|
18
|
+
/** Return the legacy manifest path for migration. */
|
|
19
|
+
export declare function getLegacyManifestPath(): string;
|
|
20
|
+
/** Return the backup directory for a harness's config files. */
|
|
21
|
+
export declare function getBackupDir(harnessName: string): string;
|
|
22
|
+
/** Detect the user's shell and return its name and profile path, or null if unsupported. */
|
|
11
23
|
export declare function getShellProfile(): {
|
|
12
24
|
shell: string;
|
|
13
25
|
path: string;
|