uv-suite 0.26.3 → 0.26.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/cli.js +260 -133
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync, spawn } = require('child_process');
4
- const path = require('path');
5
- const fs = require('fs');
3
+ const { execSync, spawn } = require("child_process");
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const crypto = require("crypto");
7
+ const readline = require("readline");
6
8
 
7
- const UV_SUITE_DIR = path.resolve(__dirname, '..');
9
+ const UV_SUITE_DIR = path.resolve(__dirname, "..");
8
10
  const args = process.argv.slice(2);
9
11
  const command = args[0];
10
- const pkg = require(path.join(UV_SUITE_DIR, 'package.json'));
12
+ const pkg = require(path.join(UV_SUITE_DIR, "package.json"));
11
13
 
12
- const PERSONAS = ['spike', 'sport', 'pro', 'professional', 'auto'];
13
- const TOOLS = ['claude', 'codex'];
14
+ const PERSONAS = ["spike", "sport", "pro", "professional", "auto"];
15
+ const TOOLS = ["claude", "codex"];
14
16
 
15
17
  function usage() {
16
18
  console.log(`
@@ -57,208 +59,333 @@ function info() {
57
59
  }
58
60
 
59
61
  function install() {
60
- const installScript = path.join(UV_SUITE_DIR, 'install.sh');
62
+ const installScript = path.join(UV_SUITE_DIR, "install.sh");
61
63
  if (!fs.existsSync(installScript)) {
62
- console.error('Error: install.sh not found at', installScript);
64
+ console.error("Error: install.sh not found at", installScript);
63
65
  process.exit(1);
64
66
  }
65
- const installArgs = args.slice(1).join(' ');
67
+ const installArgs = args.slice(1).join(" ");
66
68
  try {
67
- execSync(`bash "${installScript}" ${installArgs}`, { stdio: 'inherit' });
69
+ execSync(`bash "${installScript}" ${installArgs}`, { stdio: "inherit" });
68
70
  } catch (e) {
69
71
  process.exit(e.status || 1);
70
72
  }
71
73
  }
72
74
 
73
75
  function normPersona(p) {
74
- if (p === 'pro' || p === 'professional') return 'professional';
76
+ if (p === "pro" || p === "professional") return "professional";
75
77
  if (PERSONAS.includes(p)) return p;
76
78
  return null;
77
79
  }
78
80
 
79
81
  function personaLabel(p) {
80
82
  const labels = {
81
- spike: 'Spike — research & docs (Opus, max)',
82
- sport: 'Sport — lightweight (Sonnet, high)',
83
- professional: 'Professional — full rigor (all hooks, all guardrails)',
84
- auto: 'Auto — autonomous (max, everything approved)',
83
+ spike: "Spike — research & docs (Opus, max)",
84
+ sport: "Sport — lightweight (Sonnet, high)",
85
+ professional: "Professional — full rigor (all hooks, all guardrails)",
86
+ auto: "Auto — autonomous (max, everything approved)",
85
87
  };
86
88
  return labels[p] || p;
87
89
  }
88
90
 
89
- function ensureInstalled(persona) {
90
- const hooksDir = path.resolve('.claude/hooks');
91
- const personasDir = path.resolve('.claude/personas');
92
- const needsInstall = !fs.existsSync(personasDir) || !fs.existsSync(hooksDir);
93
-
94
- if (needsInstall) {
95
- console.log('UV Suite not installed in this project. Installing core files...');
96
- console.log('');
97
-
98
- // Fast install: copy essential files directly (no pip, no brew, no slow stuff)
99
- const srcDir = UV_SUITE_DIR;
100
- const targetDir = path.resolve('.claude');
101
-
102
- // Create directories
103
- for (const dir of ['agents', 'skills', 'hooks', 'rules', 'personas']) {
104
- fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
105
- }
91
+ // Sync package-owned files (hooks, skills, personas, agents, optional guardrails)
92
+ // from the installed npm package into the project's .claude/. Idempotent — runs
93
+ // every launch so users on older versions pick up new hooks and slash commands
94
+ // after `npm install -g uv-suite@latest` without needing `uvs install` again.
95
+ // settings.json is preserved if it exists (user customizations).
96
+ function syncPackageFiles(persona) {
97
+ const srcDir = UV_SUITE_DIR;
98
+ const targetDir = path.resolve(".claude");
99
+ const hooksDir = path.join(targetDir, "hooks");
100
+ const personasDir = path.join(targetDir, "personas");
101
+ const wasFreshInstall =
102
+ !fs.existsSync(personasDir) || !fs.existsSync(hooksDir);
103
+
104
+ for (const dir of ["agents", "skills", "hooks", "rules", "personas"]) {
105
+ fs.mkdirSync(path.join(targetDir, dir), { recursive: true });
106
+ }
106
107
 
107
- // Copy agents
108
- const agentsSrc = path.join(srcDir, 'agents', 'claude-code');
109
- if (fs.existsSync(agentsSrc)) {
110
- for (const f of fs.readdirSync(agentsSrc)) {
111
- fs.copyFileSync(path.join(agentsSrc, f), path.join(targetDir, 'agents', f));
112
- }
108
+ const agentsSrc = path.join(srcDir, "agents", "claude-code");
109
+ if (fs.existsSync(agentsSrc)) {
110
+ for (const f of fs.readdirSync(agentsSrc)) {
111
+ fs.copyFileSync(
112
+ path.join(agentsSrc, f),
113
+ path.join(targetDir, "agents", f),
114
+ );
113
115
  }
116
+ }
114
117
 
115
- // Copy hooks
116
- const hooksSrc = path.join(srcDir, 'hooks');
117
- if (fs.existsSync(hooksSrc)) {
118
- for (const f of fs.readdirSync(hooksSrc)) {
119
- const dest = path.join(targetDir, 'hooks', f);
120
- fs.copyFileSync(path.join(hooksSrc, f), dest);
121
- fs.chmodSync(dest, 0o755);
122
- }
118
+ const hooksSrc = path.join(srcDir, "hooks");
119
+ if (fs.existsSync(hooksSrc)) {
120
+ for (const f of fs.readdirSync(hooksSrc)) {
121
+ const dest = path.join(targetDir, "hooks", f);
122
+ fs.copyFileSync(path.join(hooksSrc, f), dest);
123
+ fs.chmodSync(dest, 0o755);
123
124
  }
125
+ }
124
126
 
125
- // Copy skills
126
- const skillsSrc = path.join(srcDir, 'skills');
127
- if (fs.existsSync(skillsSrc)) {
128
- for (const d of fs.readdirSync(skillsSrc)) {
129
- const skillFile = path.join(skillsSrc, d, 'SKILL.md');
130
- if (fs.existsSync(skillFile)) {
131
- const destDir = path.join(targetDir, 'skills', d);
132
- fs.mkdirSync(destDir, { recursive: true });
133
- fs.copyFileSync(skillFile, path.join(destDir, 'SKILL.md'));
134
- }
127
+ const skillsSrc = path.join(srcDir, "skills");
128
+ if (fs.existsSync(skillsSrc)) {
129
+ for (const d of fs.readdirSync(skillsSrc)) {
130
+ const skillFile = path.join(skillsSrc, d, "SKILL.md");
131
+ if (fs.existsSync(skillFile)) {
132
+ const destDir = path.join(targetDir, "skills", d);
133
+ fs.mkdirSync(destDir, { recursive: true });
134
+ fs.copyFileSync(skillFile, path.join(destDir, "SKILL.md"));
135
135
  }
136
136
  }
137
+ }
137
138
 
138
- // Copy guardrails (for professional and auto)
139
- if (persona === 'professional' || persona === 'auto') {
140
- const guardSrc = path.join(srcDir, 'guardrails');
141
- if (fs.existsSync(guardSrc)) {
142
- for (const f of fs.readdirSync(guardSrc)) {
143
- fs.copyFileSync(path.join(guardSrc, f), path.join(targetDir, 'rules', f));
144
- }
139
+ if (persona === "professional" || persona === "auto") {
140
+ const guardSrc = path.join(srcDir, "guardrails");
141
+ if (fs.existsSync(guardSrc)) {
142
+ for (const f of fs.readdirSync(guardSrc)) {
143
+ fs.copyFileSync(
144
+ path.join(guardSrc, f),
145
+ path.join(targetDir, "rules", f),
146
+ );
145
147
  }
146
148
  }
149
+ }
147
150
 
148
- // Copy personas
149
- const personasSrc = path.join(srcDir, 'personas');
150
- if (fs.existsSync(personasSrc)) {
151
- for (const f of fs.readdirSync(personasSrc)) {
152
- fs.copyFileSync(path.join(personasSrc, f), path.join(targetDir, 'personas', f));
153
- }
151
+ const personasSrc = path.join(srcDir, "personas");
152
+ if (fs.existsSync(personasSrc)) {
153
+ for (const f of fs.readdirSync(personasSrc)) {
154
+ fs.copyFileSync(
155
+ path.join(personasSrc, f),
156
+ path.join(targetDir, "personas", f),
157
+ );
154
158
  }
159
+ }
155
160
 
156
- // Set settings.json from persona
157
- const personaFile = path.join(targetDir, 'personas', `${persona}.json`);
158
- const settingsFile = path.join(targetDir, 'settings.json');
159
- if (fs.existsSync(personaFile) && !fs.existsSync(settingsFile)) {
160
- fs.copyFileSync(personaFile, settingsFile);
161
- }
161
+ // settings.json is user-owned. Only seed it on fresh install.
162
+ const personaFile = path.join(targetDir, "personas", `${persona}.json`);
163
+ const settingsFile = path.join(targetDir, "settings.json");
164
+ if (
165
+ wasFreshInstall &&
166
+ fs.existsSync(personaFile) &&
167
+ !fs.existsSync(settingsFile)
168
+ ) {
169
+ fs.copyFileSync(personaFile, settingsFile);
170
+ }
162
171
 
172
+ if (wasFreshInstall) {
173
+ console.log(
174
+ "UV Suite not installed in this project. Installing core files...",
175
+ );
163
176
  console.log(` Installed: agents, skills, hooks, guardrails, personas`);
164
- console.log('');
177
+ console.log("");
178
+ }
179
+ }
180
+
181
+ function prompt(rl, question) {
182
+ return new Promise((resolve) => rl.question(question, resolve));
183
+ }
184
+
185
+ function normalizeKind(s) {
186
+ const v = (s || "").toLowerCase().trim();
187
+ if (["l", "long", "long-running"].includes(v)) return "long-running";
188
+ if (["o", "outcome"].includes(v)) return "outcome";
189
+ return "";
190
+ }
191
+
192
+ function normalizePriority(s) {
193
+ const v = (s || "").toLowerCase().trim();
194
+ if (["l", "low"].includes(v)) return "low";
195
+ if (["m", "med", "medium"].includes(v)) return "med";
196
+ if (["h", "high"].includes(v)) return "high";
197
+ return "";
198
+ }
199
+
200
+ // Generate a UVS_SESSION_ID, prompt for metadata (name/kind/purpose/priority),
201
+ // write it to .uv-suite-state/sessions/<sid>.json, and return the id + name.
202
+ // Skipping (Enter) leaves a field empty; the session-label-nag.sh hook will
203
+ // remind the user to run /session-init mid-flight.
204
+ async function setupSession(persona) {
205
+ const projectDir = process.cwd();
206
+ const stateDir = path.join(projectDir, ".uv-suite-state");
207
+ const sessionsDir = path.join(stateDir, "sessions");
208
+ fs.mkdirSync(sessionsDir, { recursive: true });
209
+
210
+ const sid = crypto.randomUUID();
211
+ let name = "";
212
+ let kind = "";
213
+ let purpose = "";
214
+ let priority = "";
215
+
216
+ if (process.stdin.isTTY && !process.env.UVS_NO_PROMPT) {
217
+ const rl = readline.createInterface({
218
+ input: process.stdin,
219
+ output: process.stdout,
220
+ });
221
+ console.log("");
222
+ console.log("Label this session (Enter to skip — you'll be reminded):");
223
+ name = (await prompt(rl, " name: ")).trim();
224
+ const kindRaw = await prompt(rl, " kind [long/outcome]: ");
225
+ purpose = (await prompt(rl, " purpose: ")).trim();
226
+ const priorityRaw = await prompt(rl, " priority [low/med/high]: ");
227
+ rl.close();
228
+ kind = normalizeKind(kindRaw);
229
+ priority = normalizePriority(priorityRaw);
165
230
  }
231
+
232
+ const meta = {
233
+ uvs_session_id: sid,
234
+ name,
235
+ kind,
236
+ purpose,
237
+ priority,
238
+ persona,
239
+ cwd: projectDir,
240
+ started_at: Math.floor(Date.now() / 1000),
241
+ };
242
+ fs.writeFileSync(
243
+ path.join(sessionsDir, `${sid}.json`),
244
+ JSON.stringify(meta, null, 2),
245
+ );
246
+ fs.writeFileSync(path.join(stateDir, "current-session.txt"), sid);
247
+
248
+ return { sid, name };
249
+ }
250
+
251
+ // Backwards-compat shim — older code in this file still references this name.
252
+ function ensureInstalled(persona) {
253
+ syncPackageFiles(persona);
166
254
  }
167
255
 
168
- function launchClaude(persona, extra) {
169
- ensureInstalled(persona);
170
- const settings = path.resolve('.claude/personas', `${persona}.json`);
256
+ async function launchClaude(persona, extra) {
257
+ syncPackageFiles(persona);
258
+ const settings = path.resolve(".claude/personas", `${persona}.json`);
171
259
  if (!fs.existsSync(settings)) {
172
- console.error(`Error: installation failed. Run 'uvs install --persona ${persona}' manually.`);
260
+ console.error(
261
+ `Error: installation failed. Run 'uvs install --persona ${persona}' manually.`,
262
+ );
173
263
  process.exit(1);
174
264
  }
265
+ const { sid, name } = await setupSession(persona);
266
+ console.log("");
175
267
  console.log(`UV Suite | Claude Code | ${personaLabel(persona)}`);
176
- console.log('');
177
- const child = spawn('claude', ['--settings', settings, ...extra], { stdio: 'inherit' });
178
- child.on('exit', (code) => process.exit(code || 0));
268
+ console.log(`Session: ${sid.slice(0, 8)}${name ? " — " + name : ""}`);
269
+ console.log("");
270
+ const child = spawn("claude", ["--settings", settings, ...extra], {
271
+ stdio: "inherit",
272
+ env: { ...process.env, UVS_SESSION_ID: sid },
273
+ });
274
+ child.on("exit", (code) => process.exit(code || 0));
179
275
  }
180
276
 
181
- function launchCodex(persona, extra) {
277
+ async function launchCodex(persona, extra) {
182
278
  const approvalMap = {
183
- spike: ['--model', 'o3', '--approval-mode', 'suggest'],
184
- sport: ['--approval-mode', 'auto-edit'],
185
- professional: ['--approval-mode', 'suggest'],
186
- auto: ['--approval-mode', 'full-auto'],
279
+ spike: ["--model", "o3", "--approval-mode", "suggest"],
280
+ sport: ["--approval-mode", "auto-edit"],
281
+ professional: ["--approval-mode", "suggest"],
282
+ auto: ["--approval-mode", "full-auto"],
187
283
  };
188
- const codexArgs = approvalMap[persona] || ['--approval-mode', 'suggest'];
284
+ const codexArgs = approvalMap[persona] || ["--approval-mode", "suggest"];
285
+ const { sid, name } = await setupSession(persona);
286
+ console.log("");
189
287
  console.log(`UV Suite | Codex | ${personaLabel(persona)}`);
190
- console.log('');
191
- const child = spawn('codex', [...codexArgs, ...extra], { stdio: 'inherit' });
192
- child.on('exit', (code) => process.exit(code || 0));
288
+ console.log(`Session: ${sid.slice(0, 8)}${name ? " — " + name : ""}`);
289
+ console.log("");
290
+ const child = spawn("codex", [...codexArgs, ...extra], {
291
+ stdio: "inherit",
292
+ env: { ...process.env, UVS_SESSION_ID: sid },
293
+ });
294
+ child.on("exit", (code) => process.exit(code || 0));
193
295
  }
194
296
 
195
297
  function watch() {
196
- const serverScript = path.join(UV_SUITE_DIR, 'watchtower', 'server.js');
298
+ const serverScript = path.join(UV_SUITE_DIR, "watchtower", "server.js");
197
299
  if (!fs.existsSync(serverScript)) {
198
- console.error('Error: watchtower server not found at', serverScript);
300
+ console.error("Error: watchtower server not found at", serverScript);
199
301
  process.exit(1);
200
302
  }
201
303
 
202
- const bg = args.includes('--bg') || args.includes('--background');
203
- console.log('UV Suite Watchtower starting...');
204
- console.log('Dashboard: http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200));
205
- console.log('');
304
+ const bg = args.includes("--bg") || args.includes("--background");
305
+ console.log("UV Suite Watchtower starting...");
306
+ console.log(
307
+ "Dashboard: http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200),
308
+ );
309
+ console.log("");
206
310
 
207
311
  if (bg) {
208
- const child = spawn('node', [serverScript], {
209
- stdio: 'ignore',
312
+ const child = spawn("node", [serverScript], {
313
+ stdio: "ignore",
210
314
  detached: true,
211
315
  });
212
316
  child.unref();
213
317
  console.log(`Running in background (PID: ${child.pid})`);
214
- console.log('Stop with: kill ' + child.pid);
318
+ console.log("Stop with: kill " + child.pid);
215
319
 
216
320
  // Open browser
217
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
218
- spawn(opener, ['http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200)], { stdio: 'ignore' });
321
+ const opener =
322
+ process.platform === "darwin"
323
+ ? "open"
324
+ : process.platform === "win32"
325
+ ? "start"
326
+ : "xdg-open";
327
+ spawn(
328
+ opener,
329
+ ["http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200)],
330
+ { stdio: "ignore" },
331
+ );
219
332
  } else {
220
333
  // Foreground — open browser after a short delay
221
334
  setTimeout(() => {
222
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
223
- spawn(opener, ['http://localhost:' + (process.env.UVS_WATCHTOWER_PORT || 4200)], { stdio: 'ignore' });
335
+ const opener =
336
+ process.platform === "darwin"
337
+ ? "open"
338
+ : process.platform === "win32"
339
+ ? "start"
340
+ : "xdg-open";
341
+ spawn(
342
+ opener,
343
+ ["http://localhost:" + (process.env.UVS_WATCHTOWER_PORT || 4200)],
344
+ { stdio: "ignore" },
345
+ );
224
346
  }, 1000);
225
347
 
226
- const child = spawn('node', [serverScript], { stdio: 'inherit' });
227
- child.on('exit', (code) => process.exit(code || 0));
348
+ const child = spawn("node", [serverScript], { stdio: "inherit" });
349
+ child.on("exit", (code) => process.exit(code || 0));
228
350
  }
229
351
  }
230
352
 
231
353
  // --- Parse and route ---
232
354
 
233
- if (!command || command === '--help' || command === '-h') {
355
+ if (!command || command === "--help" || command === "-h") {
234
356
  usage();
235
357
  process.exit(0);
236
358
  }
237
359
 
238
- if (command === 'watch') {
239
- watch();
240
- } else if (command === 'install') {
241
- install();
242
- } else if (command === 'info') {
243
- info();
244
- } else if (TOOLS.includes(command)) {
245
- // uv claude pro, uv codex auto
246
- const persona = normPersona(args[1] || 'pro');
247
- if (!persona) {
248
- console.error(`Unknown persona: ${args[1]}`);
249
- console.error('Available: spike, sport, pro, auto');
360
+ (async () => {
361
+ if (command === "watch") {
362
+ watch();
363
+ } else if (command === "install") {
364
+ install();
365
+ } else if (command === "info") {
366
+ info();
367
+ } else if (TOOLS.includes(command)) {
368
+ // uvs claude pro, uvs codex auto
369
+ const persona = normPersona(args[1] || "pro");
370
+ if (!persona) {
371
+ console.error(`Unknown persona: ${args[1]}`);
372
+ console.error("Available: spike, sport, pro, auto");
373
+ process.exit(1);
374
+ }
375
+ const extra = args.slice(2);
376
+ if (command === "claude") await launchClaude(persona, extra);
377
+ else await launchCodex(persona, extra);
378
+ } else if (normPersona(command)) {
379
+ // uvs pro (shorthand for uvs claude pro)
380
+ const persona = normPersona(command);
381
+ const extra = args.slice(1);
382
+ await launchClaude(persona, extra);
383
+ } else {
384
+ console.error(`Unknown command: ${command}`);
385
+ usage();
250
386
  process.exit(1);
251
387
  }
252
- const extra = args.slice(2);
253
- if (command === 'claude') launchClaude(persona, extra);
254
- else launchCodex(persona, extra);
255
- } else if (normPersona(command)) {
256
- // uv pro (shorthand for uv claude pro)
257
- const persona = normPersona(command);
258
- const extra = args.slice(1);
259
- launchClaude(persona, extra);
260
- } else {
261
- console.error(`Unknown command: ${command}`);
262
- usage();
388
+ })().catch((err) => {
389
+ console.error(err);
263
390
  process.exit(1);
264
- }
391
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uv-suite",
3
- "version": "0.26.3",
3
+ "version": "0.26.4",
4
4
  "description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
5
5
  "author": "Utsav Anand",
6
6
  "license": "MIT",