@uluops/setup 0.2.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 +178 -0
- package/assets/agents/api-contract-validator-agent.md +960 -0
- package/assets/agents/aristotle-analyst-agent.md +705 -0
- package/assets/agents/aristotle-explorer-agent.md +152 -0
- package/assets/agents/aristotle-forecaster-agent.md +666 -0
- package/assets/agents/aristotle-validator-agent.md +667 -0
- package/assets/agents/assumption-excavator-agent.md +1354 -0
- package/assets/agents/code-auditor-agent.md +1061 -0
- package/assets/agents/code-optimizer-agent.md +876 -0
- package/assets/agents/code-validator-agent.md +846 -0
- package/assets/agents/docs-validator-agent.md +490 -0
- package/assets/agents/frontend-validator-agent.md +844 -0
- package/assets/agents/mcp-validator-agent.md +827 -0
- package/assets/agents/pre-implementation-architect-agent.md +1036 -0
- package/assets/agents/prompt-engineer-agent.md +1158 -0
- package/assets/agents/prompt-pattern-analyzer-agent.md +907 -0
- package/assets/agents/prompt-quality-validator-agent.md +1018 -0
- package/assets/agents/public-interface-validator-agent.md +951 -0
- package/assets/agents/release-readiness-agent.md +482 -0
- package/assets/agents/security-analyst-agent.md +1093 -0
- package/assets/agents/test-architect-agent.md +861 -0
- package/assets/agents/type-safety-validator-agent.md +932 -0
- package/assets/agents/workflow-synthesis-agent.md +836 -0
- package/assets/commands/agents/api-contract.md +135 -0
- package/assets/commands/agents/architect.md +135 -0
- package/assets/commands/agents/aristotle-analyst.md +115 -0
- package/assets/commands/agents/aristotle-explorer.md +92 -0
- package/assets/commands/agents/aristotle-forecaster.md +114 -0
- package/assets/commands/agents/aristotle-validator.md +114 -0
- package/assets/commands/agents/assumption-excavator.md +114 -0
- package/assets/commands/agents/audit.md +136 -0
- package/assets/commands/agents/docs-validate.md +133 -0
- package/assets/commands/agents/frontend.md +135 -0
- package/assets/commands/agents/mcp-validate.md +136 -0
- package/assets/commands/agents/optimize.md +133 -0
- package/assets/commands/agents/pattern-analyzer.md +126 -0
- package/assets/commands/agents/prompt-quality.md +134 -0
- package/assets/commands/agents/prompt-validate.md +135 -0
- package/assets/commands/agents/public-interface.md +134 -0
- package/assets/commands/agents/release.md +135 -0
- package/assets/commands/agents/security.md +137 -0
- package/assets/commands/agents/test-review.md +136 -0
- package/assets/commands/agents/type-safety.md +135 -0
- package/assets/commands/agents/validate.md +134 -0
- package/assets/commands/agents/workflow-synthesis.md +101 -0
- package/assets/commands/workflows/aristotle.md +543 -0
- package/assets/commands/workflows/post-implementation.md +577 -0
- package/assets/commands/workflows/pre-implementation.md +670 -0
- package/assets/commands/workflows/prompt-audit.md +754 -0
- package/assets/commands/workflows/ship.md +721 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +436 -0
- package/dist/lib/config-merger.d.ts +26 -0
- package/dist/lib/config-merger.js +63 -0
- package/dist/lib/file-ops.d.ts +23 -0
- package/dist/lib/file-ops.js +86 -0
- package/dist/lib/hash.d.ts +1 -0
- package/dist/lib/hash.js +4 -0
- package/dist/lib/manifest.d.ts +16 -0
- package/dist/lib/manifest.js +34 -0
- package/dist/lib/paths.d.ts +14 -0
- package/dist/lib/paths.js +49 -0
- package/dist/lib/settings-merger.d.ts +43 -0
- package/dist/lib/settings-merger.js +91 -0
- package/dist/steps/agents.d.ts +8 -0
- package/dist/steps/agents.js +14 -0
- package/dist/steps/auth.d.ts +12 -0
- package/dist/steps/auth.js +80 -0
- package/dist/steps/commands.d.ts +9 -0
- package/dist/steps/commands.js +69 -0
- package/dist/steps/detect.d.ts +9 -0
- package/dist/steps/detect.js +30 -0
- package/dist/steps/mcp.d.ts +6 -0
- package/dist/steps/mcp.js +40 -0
- package/dist/steps/metrics.d.ts +22 -0
- package/dist/steps/metrics.js +176 -0
- package/dist/steps/shell.d.ts +2 -0
- package/dist/steps/shell.js +48 -0
- package/dist/steps/signup.d.ts +13 -0
- package/dist/steps/signup.js +92 -0
- package/dist/steps/verify.d.ts +10 -0
- package/dist/steps/verify.js +184 -0
- package/dist/test/auth.test.d.ts +1 -0
- package/dist/test/auth.test.js +43 -0
- package/dist/test/config-io.test.d.ts +1 -0
- package/dist/test/config-io.test.js +56 -0
- package/dist/test/config-merger.test.d.ts +1 -0
- package/dist/test/config-merger.test.js +94 -0
- package/dist/test/detect.test.d.ts +1 -0
- package/dist/test/detect.test.js +25 -0
- package/dist/test/file-ops.test.d.ts +1 -0
- package/dist/test/file-ops.test.js +100 -0
- package/dist/test/hash.test.d.ts +1 -0
- package/dist/test/hash.test.js +14 -0
- package/dist/test/manifest.test.d.ts +1 -0
- package/dist/test/manifest.test.js +78 -0
- package/dist/test/paths.test.d.ts +1 -0
- package/dist/test/paths.test.js +30 -0
- package/dist/test/settings-merger.test.d.ts +1 -0
- package/dist/test/settings-merger.test.js +167 -0
- package/dist/test/shell-profile.test.d.ts +1 -0
- package/dist/test/shell-profile.test.js +40 -0
- package/dist/test/shell.test.d.ts +1 -0
- package/dist/test/shell.test.js +71 -0
- package/dist/test/signup.test.d.ts +1 -0
- package/dist/test/signup.test.js +83 -0
- package/package.json +36 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { detect } from "./steps/detect.js";
|
|
8
|
+
import { resolveApiKey } from "./steps/auth.js";
|
|
9
|
+
import { signup } from "./steps/signup.js";
|
|
10
|
+
import { installMcp, uninstallMcp } from "./steps/mcp.js";
|
|
11
|
+
import { installAgents, uninstallAgents } from "./steps/agents.js";
|
|
12
|
+
import { installCommands, uninstallCommands } from "./steps/commands.js";
|
|
13
|
+
import { writeShellExport, removeShellExport } from "./steps/shell.js";
|
|
14
|
+
import { verify } from "./steps/verify.js";
|
|
15
|
+
import { installMetrics, uninstallMetrics } from "./steps/metrics.js";
|
|
16
|
+
import { loadManifest, saveManifest, deleteManifest, } from "./lib/manifest.js";
|
|
17
|
+
import { getClaudeHome, getAgentsDir, ASSETS_DIR } from "./lib/paths.js";
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
async function getVersion() {
|
|
20
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
21
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
22
|
+
return pkg.version;
|
|
23
|
+
}
|
|
24
|
+
const ok = (msg) => console.log(` ${chalk.green("✓")} ${msg}`);
|
|
25
|
+
const warn = (msg) => console.log(` ${chalk.yellow("⚠")} ${msg}`);
|
|
26
|
+
const fail = (msg) => console.log(` ${chalk.red("✗")} ${msg}`);
|
|
27
|
+
const info = (msg) => console.log(` ${msg}`);
|
|
28
|
+
async function runSetup(opts) {
|
|
29
|
+
const version = await getVersion();
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")}`);
|
|
32
|
+
console.log(` ${chalk.dim("operating intelligence as infrastructure")}`);
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(` Setup v${version}`);
|
|
35
|
+
console.log();
|
|
36
|
+
if (opts.dryRun) {
|
|
37
|
+
info(chalk.dim("(dry run — no changes will be made)\n"));
|
|
38
|
+
}
|
|
39
|
+
// Detect environment
|
|
40
|
+
const env = await detect();
|
|
41
|
+
// Resolve API key — via signup or existing key
|
|
42
|
+
let apiKey;
|
|
43
|
+
let email = null;
|
|
44
|
+
try {
|
|
45
|
+
if (opts.signup) {
|
|
46
|
+
info("Create your UluOps account\n");
|
|
47
|
+
const auth = await signup();
|
|
48
|
+
apiKey = auth.apiKey;
|
|
49
|
+
email = auth.email;
|
|
50
|
+
ok(`Account created (${email})`);
|
|
51
|
+
ok(`API key generated`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const auth = await resolveApiKey({
|
|
55
|
+
apiKeyFlag: opts.apiKey,
|
|
56
|
+
skipValidation: opts.skipValidation,
|
|
57
|
+
interactive: !opts.yes && !opts.apiKey && !process.env["ULUOPS_API_KEY"],
|
|
58
|
+
});
|
|
59
|
+
apiKey = auth.apiKey;
|
|
60
|
+
email = auth.email;
|
|
61
|
+
if (email) {
|
|
62
|
+
ok(`Key validated (${email})`);
|
|
63
|
+
}
|
|
64
|
+
else if (opts.skipValidation) {
|
|
65
|
+
ok("Key accepted (validation skipped)");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
ok("Key validated");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
// Load existing manifest for update detection
|
|
78
|
+
const existingManifest = await loadManifest();
|
|
79
|
+
// Show update info if re-running with newer version
|
|
80
|
+
if (existingManifest && existingManifest.version !== version) {
|
|
81
|
+
info(`Updating ${chalk.dim(existingManifest.version)} → ${chalk.green(version)}`);
|
|
82
|
+
console.log();
|
|
83
|
+
}
|
|
84
|
+
else if (existingManifest) {
|
|
85
|
+
info(chalk.dim(`Already at v${version} — checking for changes`));
|
|
86
|
+
console.log();
|
|
87
|
+
}
|
|
88
|
+
// Check for conflicts on first install
|
|
89
|
+
if (!existingManifest && !opts.yes && !opts.dryRun) {
|
|
90
|
+
await checkConflicts(opts.localDefs);
|
|
91
|
+
}
|
|
92
|
+
// MCP config
|
|
93
|
+
const mcpResult = await installMcp(apiKey, opts.scope, opts.dryRun);
|
|
94
|
+
ok(`MCP config → ${mcpResult.configPath} (2 servers)`);
|
|
95
|
+
// Agents
|
|
96
|
+
const agentsResult = await installAgents(opts.localDefs, opts.dryRun, existingManifest?.agents);
|
|
97
|
+
const agentParts = [];
|
|
98
|
+
if (agentsResult.copied > 0)
|
|
99
|
+
agentParts.push(`${agentsResult.copied} copied`);
|
|
100
|
+
if (agentsResult.skipped > 0)
|
|
101
|
+
agentParts.push(`${agentsResult.skipped} unchanged`);
|
|
102
|
+
if (agentsResult.removed > 0)
|
|
103
|
+
agentParts.push(`${agentsResult.removed} removed`);
|
|
104
|
+
ok(`${agentsResult.files.length} agents → ${opts.localDefs ? "./uluops/agents/" : "~/.claude/agents/"}${agentParts.length ? ` (${agentParts.join(", ")})` : ""}`);
|
|
105
|
+
// Commands
|
|
106
|
+
const commandsResult = await installCommands(opts.localDefs, opts.dryRun, existingManifest?.commands);
|
|
107
|
+
const totalCommands = commandsResult.agentCommands + commandsResult.workflowCommands;
|
|
108
|
+
const cmdSkipped = commandsResult.skipped;
|
|
109
|
+
const cmdTotal = commandsResult.files.length;
|
|
110
|
+
const cmdParts = [];
|
|
111
|
+
if (totalCommands > 0)
|
|
112
|
+
cmdParts.push(`${totalCommands} copied`);
|
|
113
|
+
if (cmdSkipped > 0)
|
|
114
|
+
cmdParts.push(`${cmdSkipped} unchanged`);
|
|
115
|
+
if (commandsResult.removed > 0)
|
|
116
|
+
cmdParts.push(`${commandsResult.removed} removed`);
|
|
117
|
+
ok(`${cmdTotal} commands → ${opts.localDefs ? "./uluops/commands/" : "~/.claude/commands/"}${cmdParts.length ? ` (${cmdParts.join(", ")})` : ""}`);
|
|
118
|
+
// Agent metrics (SubagentStop hook for auto-capture)
|
|
119
|
+
const metricsResult = await installMetrics(opts.dryRun);
|
|
120
|
+
if (metricsResult.hookConfigured) {
|
|
121
|
+
const parts = [];
|
|
122
|
+
if (metricsResult.toolFilesCopied > 0)
|
|
123
|
+
parts.push(`${metricsResult.toolFilesCopied} files`);
|
|
124
|
+
parts.push("hook configured");
|
|
125
|
+
ok(`Agent metrics → ~/.claude/tools/agent-metrics/ (${parts.join(", ")})`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
warn("Agent metrics hook not configured (tool files not found)");
|
|
129
|
+
}
|
|
130
|
+
// Health check
|
|
131
|
+
if (!opts.skipValidation && !opts.dryRun) {
|
|
132
|
+
try {
|
|
133
|
+
const [trackerOk, registryOk] = await Promise.all([
|
|
134
|
+
checkEndpoint("https://api.uluops.ai/api/v1/health"),
|
|
135
|
+
checkEndpoint("https://api.uluops.ai/api/v1/registry/health"),
|
|
136
|
+
]);
|
|
137
|
+
if (trackerOk && registryOk) {
|
|
138
|
+
ok("Health check passed — both APIs reachable");
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
warn("Some APIs unreachable (MCP tools may have limited functionality)");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
warn("Health check skipped (network issue)");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Shell export
|
|
149
|
+
let shellModified = false;
|
|
150
|
+
if (opts.shell && env.shellProfile) {
|
|
151
|
+
await writeShellExport(env.shellProfile, apiKey, opts.dryRun);
|
|
152
|
+
ok(`ULUOPS_API_KEY added to ${env.shellProfile}`);
|
|
153
|
+
shellModified = true;
|
|
154
|
+
}
|
|
155
|
+
else if (opts.shell) {
|
|
156
|
+
warn("--shell requested but no supported shell detected ($SHELL). Skipping.");
|
|
157
|
+
}
|
|
158
|
+
// Save manifest
|
|
159
|
+
if (!opts.dryRun) {
|
|
160
|
+
await saveManifest({
|
|
161
|
+
version,
|
|
162
|
+
installedAt: new Date().toISOString(),
|
|
163
|
+
mcpScope: opts.scope,
|
|
164
|
+
mcpConfigPath: mcpResult.configPath,
|
|
165
|
+
defsScope: opts.localDefs ? "local" : "global",
|
|
166
|
+
defsPath: opts.localDefs
|
|
167
|
+
? join(process.cwd(), "uluops")
|
|
168
|
+
: getClaudeHome(),
|
|
169
|
+
shellModified,
|
|
170
|
+
agents: agentsResult.files,
|
|
171
|
+
commands: commandsResult.files,
|
|
172
|
+
metricsHookInstalled: metricsResult.hookConfigured,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
printSetupSummary({
|
|
176
|
+
agentCount: agentsResult.files.length,
|
|
177
|
+
commandCount: cmdTotal,
|
|
178
|
+
apiKey,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// MCP tool count across both servers. Update when server toolsets change.
|
|
182
|
+
const TOOL_COUNT = 73;
|
|
183
|
+
const AGENT_LIST = [
|
|
184
|
+
["/agents:validate", "Code quality", "sonnet"],
|
|
185
|
+
["/agents:type-safety", "TypeScript", "sonnet"],
|
|
186
|
+
["/agents:test-review", "Test quality", "sonnet"],
|
|
187
|
+
["/agents:optimize", "Performance", "sonnet"],
|
|
188
|
+
["/agents:frontend", "React/a11y", "sonnet"],
|
|
189
|
+
["/agents:mcp-validate", "MCP compliance", "sonnet"],
|
|
190
|
+
["/agents:architect", "Design review", "sonnet"],
|
|
191
|
+
["/agents:audit", "Runtime bugs", "opus"],
|
|
192
|
+
["/agents:security", "OWASP", "sonnet"],
|
|
193
|
+
["/agents:api-contract", "API alignment", "sonnet"],
|
|
194
|
+
["/agents:release", "Publish ready", "sonnet"],
|
|
195
|
+
["/agents:public-interface", "README/exports", "sonnet"],
|
|
196
|
+
["/agents:docs-validate", "Documentation", "sonnet"],
|
|
197
|
+
["/agents:prompt-validate", "Prompt review", "sonnet"],
|
|
198
|
+
["/agents:prompt-quality", "Prompt quality", "sonnet"],
|
|
199
|
+
["/agents:pattern-analyzer", "Patterns", "sonnet"],
|
|
200
|
+
["/agents:aristotle-explorer", "Categories", "opus"],
|
|
201
|
+
["/agents:aristotle-analyst", "Four causes", "opus"],
|
|
202
|
+
["/agents:aristotle-validator", "Teleology", "opus"],
|
|
203
|
+
["/agents:aristotle-forecaster", "Potentiality", "opus"],
|
|
204
|
+
["/agents:assumption-excavator", "Assumptions", "sonnet"],
|
|
205
|
+
["/agents:workflow-synthesis", "Cross-agent synthesis", "opus"],
|
|
206
|
+
];
|
|
207
|
+
function printSetupSummary(opts) {
|
|
208
|
+
console.log();
|
|
209
|
+
console.log(` ${chalk.dim("━".repeat(46))}`);
|
|
210
|
+
console.log();
|
|
211
|
+
console.log(` ${chalk.bold("Setup complete!")} ${TOOL_COUNT} MCP tools · ${opts.agentCount} agents · ${opts.commandCount} slash commands · metrics`);
|
|
212
|
+
console.log();
|
|
213
|
+
printAgentList();
|
|
214
|
+
info("For SDK/CLI usage, add to your shell profile:");
|
|
215
|
+
info(` ${chalk.cyan(`export ULUOPS_API_KEY="${opts.apiKey}"`)}`);
|
|
216
|
+
console.log();
|
|
217
|
+
info(`Run again to update: ${chalk.cyan("npx @uluops/setup")}`);
|
|
218
|
+
console.log();
|
|
219
|
+
// Restart warning — last and prominent
|
|
220
|
+
console.log(` ${chalk.dim("━".repeat(46))}`);
|
|
221
|
+
console.log();
|
|
222
|
+
console.log(` ${chalk.yellow.bold("Restart Claude Code to load agents.")}`);
|
|
223
|
+
console.log();
|
|
224
|
+
info("After restart, verify with:");
|
|
225
|
+
info(` ${chalk.cyan("/agents:validate --help")}`);
|
|
226
|
+
console.log();
|
|
227
|
+
info("Then try:");
|
|
228
|
+
info(` ${chalk.cyan("/workflows:post-implementation .")}`);
|
|
229
|
+
console.log();
|
|
230
|
+
}
|
|
231
|
+
function printAgentList() {
|
|
232
|
+
info(chalk.bold("WORKFLOWS"));
|
|
233
|
+
info(` ${chalk.cyan("/workflows:pre-implementation")} Design review before coding`);
|
|
234
|
+
info(` ${chalk.cyan("/workflows:post-implementation")} Iterative validation loop`);
|
|
235
|
+
info(` ${chalk.cyan("/workflows:ship")} Final gate before shipping`);
|
|
236
|
+
info(` ${chalk.cyan("/workflows:prompt-audit")} Audit agent prompts`);
|
|
237
|
+
console.log();
|
|
238
|
+
info(` ${chalk.cyan("/workflows:aristotle")} Four-cause teleological analysis`);
|
|
239
|
+
console.log();
|
|
240
|
+
info(`${chalk.bold("AGENTS")} (run individually)${" ".repeat(26)}${chalk.dim("MODEL")}`);
|
|
241
|
+
for (const [cmd, desc, model] of AGENT_LIST) {
|
|
242
|
+
info(` ${chalk.cyan(cmd.padEnd(34))}${desc.padEnd(17)}${chalk.dim(model)}`);
|
|
243
|
+
}
|
|
244
|
+
console.log();
|
|
245
|
+
info(chalk.dim(` This is the starter set. Browse 135+ agents at registry.uluops.ai`));
|
|
246
|
+
console.log();
|
|
247
|
+
}
|
|
248
|
+
async function runUninstall(opts) {
|
|
249
|
+
const version = await getVersion();
|
|
250
|
+
console.log();
|
|
251
|
+
console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")} ${chalk.red("Uninstall")} v${version}`);
|
|
252
|
+
console.log();
|
|
253
|
+
if (opts.dryRun) {
|
|
254
|
+
info(chalk.dim("(dry run — no changes will be made)\n"));
|
|
255
|
+
}
|
|
256
|
+
const manifest = await loadManifest();
|
|
257
|
+
if (!manifest) {
|
|
258
|
+
warn("No manifest found — nothing to uninstall.");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Remove agents
|
|
262
|
+
if (!opts.dryRun) {
|
|
263
|
+
const removed = await uninstallAgents(manifest.agents, manifest.defsPath);
|
|
264
|
+
ok(`Removed ${removed} agent(s)`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
ok(`Would remove ${manifest.agents.length} agent(s)`);
|
|
268
|
+
}
|
|
269
|
+
// Remove commands
|
|
270
|
+
if (!opts.dryRun) {
|
|
271
|
+
const removed = await uninstallCommands(manifest.commands, manifest.defsPath);
|
|
272
|
+
ok(`Removed ${removed} command(s)`);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
ok(`Would remove ${manifest.commands.length} command(s)`);
|
|
276
|
+
}
|
|
277
|
+
// Remove MCP config
|
|
278
|
+
if (!opts.dryRun) {
|
|
279
|
+
await uninstallMcp(manifest.mcpConfigPath);
|
|
280
|
+
ok(`Removed MCP servers from ${manifest.mcpConfigPath}`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
ok(`Would remove MCP servers from ${manifest.mcpConfigPath}`);
|
|
284
|
+
}
|
|
285
|
+
// Remove agent metrics hook and tool files
|
|
286
|
+
if (manifest.metricsHookInstalled) {
|
|
287
|
+
if (!opts.dryRun) {
|
|
288
|
+
await uninstallMetrics(false);
|
|
289
|
+
ok("Removed agent-metrics hook and tool files");
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
ok("Would remove agent-metrics hook and tool files");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Remove shell export
|
|
296
|
+
if (manifest.shellModified) {
|
|
297
|
+
const { getShellProfile } = await import("./lib/paths.js");
|
|
298
|
+
const profile = getShellProfile();
|
|
299
|
+
if (profile && !opts.dryRun) {
|
|
300
|
+
await removeShellExport(profile.path);
|
|
301
|
+
ok(`Removed export from ${profile.path}`);
|
|
302
|
+
}
|
|
303
|
+
else if (profile) {
|
|
304
|
+
ok(`Would remove export from ${profile.path}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Delete manifest
|
|
308
|
+
if (!opts.dryRun) {
|
|
309
|
+
await deleteManifest();
|
|
310
|
+
ok("Manifest deleted");
|
|
311
|
+
}
|
|
312
|
+
console.log();
|
|
313
|
+
info("UluOps has been removed. Restart Claude Code to complete.");
|
|
314
|
+
console.log();
|
|
315
|
+
}
|
|
316
|
+
async function runVerify() {
|
|
317
|
+
const version = await getVersion();
|
|
318
|
+
console.log();
|
|
319
|
+
console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")} Installation Check v${version}`);
|
|
320
|
+
console.log();
|
|
321
|
+
const result = await verify();
|
|
322
|
+
for (const check of result.checks) {
|
|
323
|
+
if (check.passed) {
|
|
324
|
+
ok(check.label);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
fail(`${check.label}${check.detail ? ` — ${check.detail}` : ""}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
console.log();
|
|
331
|
+
if (result.ok) {
|
|
332
|
+
info(chalk.green("All checks passed."));
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
info(chalk.red("Some checks failed. Run npx @uluops/setup to fix."));
|
|
336
|
+
}
|
|
337
|
+
console.log();
|
|
338
|
+
process.exit(result.ok ? 0 : 1);
|
|
339
|
+
}
|
|
340
|
+
async function checkConflicts(localDefs) {
|
|
341
|
+
const destDir = getAgentsDir(localDefs);
|
|
342
|
+
const srcDir = join(ASSETS_DIR, "agents");
|
|
343
|
+
let existingFiles;
|
|
344
|
+
let assetFiles;
|
|
345
|
+
try {
|
|
346
|
+
existingFiles = await readdir(destDir);
|
|
347
|
+
assetFiles = await readdir(srcDir);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return; // Directory doesn't exist yet
|
|
351
|
+
}
|
|
352
|
+
const conflicts = assetFiles.filter((f) => existingFiles.includes(f));
|
|
353
|
+
if (conflicts.length === 0)
|
|
354
|
+
return;
|
|
355
|
+
warn(`Found ${conflicts.length} existing agents that match UluOps definitions:`);
|
|
356
|
+
for (const f of conflicts.slice(0, 5)) {
|
|
357
|
+
info(` ${f}`);
|
|
358
|
+
}
|
|
359
|
+
if (conflicts.length > 5) {
|
|
360
|
+
info(` ... and ${conflicts.length - 5} more`);
|
|
361
|
+
}
|
|
362
|
+
console.log();
|
|
363
|
+
info("These will be overwritten.");
|
|
364
|
+
console.log();
|
|
365
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
366
|
+
const proceed = await confirm({ message: "Continue?", default: true });
|
|
367
|
+
if (!proceed) {
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function checkEndpoint(url) {
|
|
372
|
+
try {
|
|
373
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
374
|
+
return res.ok;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function main() {
|
|
381
|
+
const version = await getVersion();
|
|
382
|
+
const program = new Command()
|
|
383
|
+
.name("uluops-setup")
|
|
384
|
+
.description("Zero-friction installer for UluOps + Claude Code")
|
|
385
|
+
.version(version)
|
|
386
|
+
.option("--api-key <key>", "API key (skip prompt)")
|
|
387
|
+
.option("--signup", "Create a new account (email + password, no browser)")
|
|
388
|
+
.option("--scope <mode>", 'MCP config scope: "global" or "local"', "global")
|
|
389
|
+
.option("--local-defs", "Save agents/commands locally instead of ~/.claude/", false)
|
|
390
|
+
.option("--shell", "Write API key export to shell profile", false)
|
|
391
|
+
.option("--skip-validation", "Accept API key without verifying", false)
|
|
392
|
+
.option("--list", "Show available agents and workflows without installing")
|
|
393
|
+
.option("--verify", "Check existing installation health")
|
|
394
|
+
.option("--uninstall", "Remove all UluOps artifacts")
|
|
395
|
+
.option("--dry-run", "Show what would happen", false)
|
|
396
|
+
.option("-y, --yes", "Skip confirmations", false);
|
|
397
|
+
program.parse();
|
|
398
|
+
const opts = program.opts();
|
|
399
|
+
if (opts.list) {
|
|
400
|
+
console.log();
|
|
401
|
+
console.log(` ${chalk.dim("⟨u⟩")} ${chalk.cyan.bold("ulu")}${chalk.bold("·ops")} v${version} — available agents and workflows`);
|
|
402
|
+
console.log();
|
|
403
|
+
printAgentList();
|
|
404
|
+
info(`Install with: ${chalk.cyan("npx @uluops/setup")}`);
|
|
405
|
+
console.log();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (opts.verify) {
|
|
409
|
+
await runVerify();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (opts.uninstall) {
|
|
413
|
+
await runUninstall({ dryRun: opts.dryRun });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (opts.scope && opts.scope !== "local" && opts.scope !== "global") {
|
|
417
|
+
console.error(chalk.red(`\n Invalid --scope "${opts.scope}". Expected "global" or "local".\n`));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
const scope = opts.scope === "local" ? "local" : "global";
|
|
421
|
+
await runSetup({
|
|
422
|
+
apiKey: opts.apiKey,
|
|
423
|
+
signup: opts.signup ?? false,
|
|
424
|
+
scope,
|
|
425
|
+
localDefs: opts.localDefs,
|
|
426
|
+
shell: opts.shell,
|
|
427
|
+
skipValidation: opts.skipValidation,
|
|
428
|
+
dryRun: opts.dryRun,
|
|
429
|
+
yes: opts.yes,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
main().catch((err) => {
|
|
433
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
434
|
+
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
435
|
+
process.exit(1);
|
|
436
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface McpServerConfig {
|
|
2
|
+
command: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
env: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
interface ClaudeConfig {
|
|
7
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read an existing config file, or return empty object if it doesn't exist.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readConfig(path: string): Promise<ClaudeConfig>;
|
|
14
|
+
/**
|
|
15
|
+
* Merge UluOps MCP server entries into a config, preserving all other keys.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mergeUluopsMcp(config: ClaudeConfig, apiKey: string): ClaudeConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Remove UluOps MCP server entries from a config.
|
|
20
|
+
*/
|
|
21
|
+
export declare function removeUluopsMcp(config: ClaudeConfig): ClaudeConfig;
|
|
22
|
+
/**
|
|
23
|
+
* Write config back to file, preserving formatting.
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeConfig(path: string, config: ClaudeConfig): Promise<void>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
/**
|
|
3
|
+
* Read an existing config file, or return empty object if it doesn't exist.
|
|
4
|
+
*/
|
|
5
|
+
export async function readConfig(path) {
|
|
6
|
+
try {
|
|
7
|
+
const raw = await readFile(path, "utf-8");
|
|
8
|
+
return JSON.parse(raw);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Merge UluOps MCP server entries into a config, preserving all other keys.
|
|
16
|
+
*/
|
|
17
|
+
export function mergeUluopsMcp(config, apiKey) {
|
|
18
|
+
const existing = config.mcpServers ?? {};
|
|
19
|
+
return {
|
|
20
|
+
...config,
|
|
21
|
+
mcpServers: {
|
|
22
|
+
...existing,
|
|
23
|
+
"uluops-tracker": {
|
|
24
|
+
command: "npx",
|
|
25
|
+
args: ["-y", "uluops-tracker-mcp-client"],
|
|
26
|
+
env: {
|
|
27
|
+
ULUOPS_TRACKER_API_URL: "https://api.uluops.ai/api/v1",
|
|
28
|
+
ULUOPS_TRACKER_API_KEY: apiKey,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
"uluops-registry": {
|
|
32
|
+
command: "npx",
|
|
33
|
+
args: ["-y", "uluops-registry-mcp-client"],
|
|
34
|
+
env: {
|
|
35
|
+
ULUOPS_REGISTRY_URL: "https://api.uluops.ai/api/v1/registry",
|
|
36
|
+
ULUOPS_API_KEY: apiKey,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Remove UluOps MCP server entries from a config.
|
|
44
|
+
*/
|
|
45
|
+
export function removeUluopsMcp(config) {
|
|
46
|
+
const servers = { ...config.mcpServers };
|
|
47
|
+
delete servers["uluops-tracker"];
|
|
48
|
+
delete servers["uluops-registry"];
|
|
49
|
+
const result = { ...config };
|
|
50
|
+
if (Object.keys(servers).length === 0) {
|
|
51
|
+
delete result.mcpServers;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
result.mcpServers = servers;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Write config back to file, preserving formatting.
|
|
60
|
+
*/
|
|
61
|
+
export async function writeConfig(path, config) {
|
|
62
|
+
await writeFile(path, JSON.stringify(config, null, 2) + "\n");
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy a file if its content has changed (hash comparison). Returns "copied" or "skipped".
|
|
3
|
+
*/
|
|
4
|
+
export declare function copyIfChanged(srcPath: string, destPath: string, dryRun: boolean): Promise<"copied" | "skipped">;
|
|
5
|
+
/**
|
|
6
|
+
* Remove files from a directory. Returns count of successfully removed files.
|
|
7
|
+
*/
|
|
8
|
+
export declare function unlinkFiles(dir: string, files: string[]): Promise<number>;
|
|
9
|
+
/**
|
|
10
|
+
* Ensure a directory exists, then copy matching .md files using hash comparison.
|
|
11
|
+
* Returns list of copied files, skipped count, and removed count (for old manifest entries).
|
|
12
|
+
*/
|
|
13
|
+
export declare function syncAssets(opts: {
|
|
14
|
+
srcDir: string;
|
|
15
|
+
destDir: string;
|
|
16
|
+
dryRun: boolean;
|
|
17
|
+
oldManifestFiles?: string[];
|
|
18
|
+
}): Promise<{
|
|
19
|
+
copied: number;
|
|
20
|
+
skipped: number;
|
|
21
|
+
removed: number;
|
|
22
|
+
files: string[];
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileHash } from "./hash.js";
|
|
4
|
+
/**
|
|
5
|
+
* Copy a file if its content has changed (hash comparison). Returns "copied" or "skipped".
|
|
6
|
+
*/
|
|
7
|
+
export async function copyIfChanged(srcPath, destPath, dryRun) {
|
|
8
|
+
const srcContent = await readFile(srcPath, "utf-8");
|
|
9
|
+
const srcHash = fileHash(srcContent);
|
|
10
|
+
try {
|
|
11
|
+
const destContent = await readFile(destPath, "utf-8");
|
|
12
|
+
if (srcHash === fileHash(destContent)) {
|
|
13
|
+
return "skipped";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// File doesn't exist yet
|
|
18
|
+
}
|
|
19
|
+
if (!dryRun) {
|
|
20
|
+
await writeFile(destPath, srcContent);
|
|
21
|
+
}
|
|
22
|
+
return "copied";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Remove files from a directory. Returns count of successfully removed files.
|
|
26
|
+
*/
|
|
27
|
+
export async function unlinkFiles(dir, files) {
|
|
28
|
+
let removed = 0;
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
try {
|
|
31
|
+
await unlink(join(dir, file));
|
|
32
|
+
removed++;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Already gone
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return removed;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Ensure a directory exists, then copy matching .md files using hash comparison.
|
|
42
|
+
* Returns list of copied files, skipped count, and removed count (for old manifest entries).
|
|
43
|
+
*/
|
|
44
|
+
export async function syncAssets(opts) {
|
|
45
|
+
const { readdir } = await import("node:fs/promises");
|
|
46
|
+
if (!opts.dryRun) {
|
|
47
|
+
await mkdir(opts.destDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
const assetFiles = (await readdir(opts.srcDir)).filter((f) => f.endsWith(".md"));
|
|
50
|
+
let copied = 0;
|
|
51
|
+
let skipped = 0;
|
|
52
|
+
const errors = [];
|
|
53
|
+
for (const file of assetFiles) {
|
|
54
|
+
try {
|
|
55
|
+
const result = await copyIfChanged(join(opts.srcDir, file), join(opts.destDir, file), opts.dryRun);
|
|
56
|
+
if (result === "copied")
|
|
57
|
+
copied++;
|
|
58
|
+
else
|
|
59
|
+
skipped++;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
errors.push(`${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Remove files that were in the old manifest but no longer in the package
|
|
66
|
+
let removed = 0;
|
|
67
|
+
if (opts.oldManifestFiles) {
|
|
68
|
+
for (const oldFile of opts.oldManifestFiles) {
|
|
69
|
+
if (!assetFiles.includes(oldFile)) {
|
|
70
|
+
if (!opts.dryRun) {
|
|
71
|
+
try {
|
|
72
|
+
await unlink(join(opts.destDir, oldFile));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Already gone
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
removed++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (errors.length > 0) {
|
|
83
|
+
throw new Error(`Failed to copy ${errors.length} file(s):\n ${errors.join("\n ")}`);
|
|
84
|
+
}
|
|
85
|
+
return { copied, skipped, removed, files: assetFiles };
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fileHash(content: string): string;
|
package/dist/lib/hash.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Manifest {
|
|
2
|
+
version: string;
|
|
3
|
+
installedAt: string;
|
|
4
|
+
mcpScope: "global" | "local";
|
|
5
|
+
mcpConfigPath: string;
|
|
6
|
+
defsScope: "global" | "local";
|
|
7
|
+
defsPath: string;
|
|
8
|
+
shellModified: boolean;
|
|
9
|
+
agents: string[];
|
|
10
|
+
commands: string[];
|
|
11
|
+
/** Whether agent-metrics hook is configured */
|
|
12
|
+
metricsHookInstalled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function loadManifest(): Promise<Manifest | null>;
|
|
15
|
+
export declare function saveManifest(manifest: Manifest): Promise<void>;
|
|
16
|
+
export declare function deleteManifest(): Promise<void>;
|