up-cc 0.1.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.
Files changed (44) hide show
  1. package/agents/up-depurador.md +357 -0
  2. package/agents/up-executor.md +409 -0
  3. package/agents/up-pesquisador-projeto.md +358 -0
  4. package/agents/up-planejador.md +390 -0
  5. package/agents/up-roteirista.md +401 -0
  6. package/agents/up-sintetizador.md +232 -0
  7. package/agents/up-verificador.md +357 -0
  8. package/bin/install.js +709 -0
  9. package/bin/lib/core.cjs +270 -0
  10. package/bin/up-tools.cjs +1361 -0
  11. package/commands/adicionar-fase.md +33 -0
  12. package/commands/ajuda.md +131 -0
  13. package/commands/discutir-fase.md +35 -0
  14. package/commands/executar-fase.md +40 -0
  15. package/commands/novo-projeto.md +39 -0
  16. package/commands/pausar.md +33 -0
  17. package/commands/planejar-fase.md +43 -0
  18. package/commands/progresso.md +33 -0
  19. package/commands/rapido.md +40 -0
  20. package/commands/remover-fase.md +34 -0
  21. package/commands/retomar.md +35 -0
  22. package/commands/verificar-trabalho.md +35 -0
  23. package/package.json +32 -0
  24. package/references/checkpoints.md +358 -0
  25. package/references/git-integration.md +208 -0
  26. package/references/questioning.md +156 -0
  27. package/references/ui-brand.md +124 -0
  28. package/templates/config.json +6 -0
  29. package/templates/continue-here.md +78 -0
  30. package/templates/project.md +184 -0
  31. package/templates/requirements.md +129 -0
  32. package/templates/roadmap.md +131 -0
  33. package/templates/state.md +130 -0
  34. package/templates/summary.md +174 -0
  35. package/workflows/discutir-fase.md +324 -0
  36. package/workflows/executar-fase.md +277 -0
  37. package/workflows/executar-plano.md +192 -0
  38. package/workflows/novo-projeto.md +561 -0
  39. package/workflows/pausar.md +111 -0
  40. package/workflows/planejar-fase.md +208 -0
  41. package/workflows/progresso.md +226 -0
  42. package/workflows/rapido.md +209 -0
  43. package/workflows/retomar.md +231 -0
  44. package/workflows/verificar-trabalho.md +261 -0
package/bin/install.js ADDED
@@ -0,0 +1,709 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * UP Installer — installs UP system into CLI tool config directories
5
+ *
6
+ * Supports: Claude Code, Gemini CLI, OpenCode, Codex
7
+ *
8
+ * Usage:
9
+ * node install.js # Interactive install
10
+ * node install.js --claude --global # Install for Claude Code globally
11
+ * node install.js --gemini --global # Install for Gemini globally
12
+ * node install.js --all --global # Install for all runtimes
13
+ * node install.js --uninstall # Remove UP files
14
+ *
15
+ * What gets installed:
16
+ * up/ → <config>/up/ (CLI, workflows, templates, references)
17
+ * agents/up-* → <config>/agents/ (UP agents)
18
+ * commands/up/ → <config>/commands/up/ (UP slash commands)
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const os = require('os');
24
+ const readline = require('readline');
25
+
26
+ // Colors
27
+ const cyan = '\x1b[36m';
28
+ const green = '\x1b[32m';
29
+ const yellow = '\x1b[33m';
30
+ const red = '\x1b[31m';
31
+ const dim = '\x1b[2m';
32
+ const bold = '\x1b[1m';
33
+ const reset = '\x1b[0m';
34
+
35
+ // Version from package.json
36
+ const pkg = require('../package.json');
37
+ const VERSION = pkg.version;
38
+
39
+ // Parse args
40
+ const args = process.argv.slice(2);
41
+ const hasGlobal = args.includes('--global') || args.includes('-g');
42
+ const hasLocal = args.includes('--local') || args.includes('-l');
43
+ const hasClaude = args.includes('--claude');
44
+ const hasGemini = args.includes('--gemini');
45
+ const hasOpencode = args.includes('--opencode');
46
+ const hasAll = args.includes('--all');
47
+ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
48
+ const hasHelp = args.includes('--help') || args.includes('-h');
49
+
50
+ // Runtime selection
51
+ let selectedRuntimes = [];
52
+ if (hasAll) {
53
+ selectedRuntimes = ['claude', 'gemini', 'opencode'];
54
+ } else {
55
+ if (hasClaude) selectedRuntimes.push('claude');
56
+ if (hasGemini) selectedRuntimes.push('gemini');
57
+ if (hasOpencode) selectedRuntimes.push('opencode');
58
+ }
59
+
60
+ const banner = '\n' +
61
+ cyan + ' ██╗ ██╗██████╗\n' +
62
+ ' ██║ ██║██╔══██╗\n' +
63
+ ' ██║ ██║██████╔╝\n' +
64
+ ' ██║ ██║██╔═══╝\n' +
65
+ ' ╚██████╔╝██║\n' +
66
+ ' ╚═════╝ ╚═╝' + reset + '\n\n' +
67
+ ' UP ' + dim + 'v' + VERSION + reset + '\n' +
68
+ ' Simplified spec-driven development for Claude Code, Gemini and OpenCode.\n';
69
+
70
+ console.log(banner);
71
+
72
+ if (hasHelp) {
73
+ console.log(` ${yellow}Usage:${reset} node install.js [options]\n`);
74
+ console.log(` ${yellow}Options:${reset}`);
75
+ console.log(` ${cyan}-g, --global${reset} Install globally (to config directory)`);
76
+ console.log(` ${cyan}-l, --local${reset} Install locally (to current directory)`);
77
+ console.log(` ${cyan}--claude${reset} Install for Claude Code`);
78
+ console.log(` ${cyan}--gemini${reset} Install for Gemini CLI`);
79
+ console.log(` ${cyan}--opencode${reset} Install for OpenCode`);
80
+ console.log(` ${cyan}--all${reset} Install for all runtimes`);
81
+ console.log(` ${cyan}-u, --uninstall${reset} Remove all UP files`);
82
+ console.log(` ${cyan}-h, --help${reset} Show this help\n`);
83
+ console.log(` ${yellow}Examples:${reset}`);
84
+ console.log(` ${dim}# Interactive install${reset}`);
85
+ console.log(` node install.js\n`);
86
+ console.log(` ${dim}# Install for Claude Code globally${reset}`);
87
+ console.log(` node install.js --claude --global\n`);
88
+ console.log(` ${dim}# Install for Gemini globally${reset}`);
89
+ console.log(` node install.js --gemini --global\n`);
90
+ console.log(` ${dim}# Install for all runtimes${reset}`);
91
+ console.log(` node install.js --all --global\n`);
92
+ process.exit(0);
93
+ }
94
+
95
+ // Determine source directory (package root = one level up from bin/)
96
+ const scriptDir = __dirname;
97
+ const packageRoot = path.resolve(scriptDir, '..');
98
+
99
+ // ── Runtime Helpers ──
100
+
101
+ function getDirName(runtime) {
102
+ if (runtime === 'opencode') return '.opencode';
103
+ if (runtime === 'gemini') return '.gemini';
104
+ return '.claude';
105
+ }
106
+
107
+ function getRuntimeLabel(runtime) {
108
+ if (runtime === 'opencode') return 'OpenCode';
109
+ if (runtime === 'gemini') return 'Gemini';
110
+ return 'Claude Code';
111
+ }
112
+
113
+ function getGlobalDir(runtime) {
114
+ if (runtime === 'opencode') {
115
+ if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
116
+ if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, 'opencode');
117
+ return path.join(os.homedir(), '.config', 'opencode');
118
+ }
119
+ if (runtime === 'gemini') {
120
+ if (process.env.GEMINI_CONFIG_DIR) return process.env.GEMINI_CONFIG_DIR;
121
+ return path.join(os.homedir(), '.gemini');
122
+ }
123
+ // Claude Code
124
+ if (process.env.CLAUDE_CONFIG_DIR) return process.env.CLAUDE_CONFIG_DIR;
125
+ return path.join(os.homedir(), '.claude');
126
+ }
127
+
128
+ function getTargetDir(runtime, isGlobal) {
129
+ if (isGlobal) return getGlobalDir(runtime);
130
+ return path.join(process.cwd(), getDirName(runtime));
131
+ }
132
+
133
+ /**
134
+ * Convert absolute path to $HOME-relative form for portable path replacement
135
+ */
136
+ function toHomePrefix(pathPrefix) {
137
+ const home = os.homedir().replace(/\\/g, '/');
138
+ const normalized = pathPrefix.replace(/\\/g, '/');
139
+ if (normalized.startsWith(home)) {
140
+ return '$HOME' + normalized.slice(home.length);
141
+ }
142
+ return normalized;
143
+ }
144
+
145
+ // ── Tool Name Conversion ──
146
+
147
+ // Claude Code → Gemini CLI tool name mapping
148
+ const claudeToGeminiTools = {
149
+ Read: 'read_file',
150
+ Write: 'write_file',
151
+ Edit: 'replace',
152
+ Bash: 'run_shell_command',
153
+ Glob: 'glob',
154
+ Grep: 'search_file_content',
155
+ WebSearch: 'google_web_search',
156
+ WebFetch: 'web_fetch',
157
+ TodoWrite: 'write_todos',
158
+ AskUserQuestion: 'ask_user',
159
+ };
160
+
161
+ // Claude Code → OpenCode tool name mapping
162
+ const claudeToOpencodeTools = {
163
+ AskUserQuestion: 'question',
164
+ SlashCommand: 'skill',
165
+ TodoWrite: 'todowrite',
166
+ WebFetch: 'webfetch',
167
+ WebSearch: 'websearch',
168
+ };
169
+
170
+ function convertGeminiToolName(claudeTool) {
171
+ if (claudeTool.startsWith('mcp__')) return null; // Auto-discovered in Gemini
172
+ if (claudeToGeminiTools[claudeTool]) return claudeToGeminiTools[claudeTool];
173
+ return claudeTool.toLowerCase();
174
+ }
175
+
176
+ function convertOpencodeToolName(claudeTool) {
177
+ if (claudeToOpencodeTools[claudeTool]) return claudeToOpencodeTools[claudeTool];
178
+ if (claudeTool.startsWith('mcp__')) return claudeTool;
179
+ return claudeTool.toLowerCase();
180
+ }
181
+
182
+ // ── Frontmatter Conversion ──
183
+
184
+ function extractFrontmatterAndBody(content) {
185
+ if (!content.startsWith('---')) return { frontmatter: null, body: content };
186
+ const endIndex = content.indexOf('---', 3);
187
+ if (endIndex === -1) return { frontmatter: null, body: content };
188
+ return {
189
+ frontmatter: content.substring(3, endIndex).trim(),
190
+ body: content.substring(endIndex + 3),
191
+ };
192
+ }
193
+
194
+ function extractFrontmatterField(frontmatter, fieldName) {
195
+ const regex = new RegExp(`^${fieldName}:\\s*(.+)$`, 'm');
196
+ const match = frontmatter.match(regex);
197
+ if (!match) return null;
198
+ return match[1].trim().replace(/^['"]|['"]$/g, '');
199
+ }
200
+
201
+ /**
202
+ * Convert Claude Code agent .md to Gemini CLI format
203
+ * - tools: must be a YAML array with Gemini tool names
204
+ * - color: must be removed (causes validation error)
205
+ * - ${VAR} patterns escaped to $VAR (Gemini template engine conflict)
206
+ */
207
+ function convertAgentToGemini(content) {
208
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
209
+ if (!frontmatter) return content;
210
+
211
+ const lines = frontmatter.split('\n');
212
+ const newLines = [];
213
+ const tools = [];
214
+ let inTools = false;
215
+
216
+ for (const line of lines) {
217
+ const trimmed = line.trim();
218
+
219
+ if (trimmed.startsWith('tools:')) {
220
+ const toolsValue = trimmed.substring(6).trim();
221
+ if (toolsValue) {
222
+ const parsed = toolsValue.split(',').map(t => t.trim()).filter(t => t);
223
+ for (const t of parsed) {
224
+ const mapped = convertGeminiToolName(t);
225
+ if (mapped) tools.push(mapped);
226
+ }
227
+ } else {
228
+ inTools = true;
229
+ }
230
+ continue;
231
+ }
232
+
233
+ if (trimmed.startsWith('color:')) continue; // Not supported by Gemini
234
+
235
+ if (inTools) {
236
+ if (trimmed.startsWith('- ')) {
237
+ const mapped = convertGeminiToolName(trimmed.substring(2).trim());
238
+ if (mapped) tools.push(mapped);
239
+ continue;
240
+ } else if (trimmed && !trimmed.startsWith('-')) {
241
+ inTools = false;
242
+ }
243
+ }
244
+
245
+ if (!inTools) newLines.push(line);
246
+ }
247
+
248
+ if (tools.length > 0) {
249
+ newLines.push('tools:');
250
+ for (const tool of tools) {
251
+ newLines.push(` - ${tool}`);
252
+ }
253
+ }
254
+
255
+ // Escape ${VAR} to $VAR (Gemini templateString() conflicts)
256
+ const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1');
257
+ // Strip <sub> tags (terminals can't render subscript)
258
+ const cleanBody = escapedBody.replace(/<sub>(.*?)<\/sub>/g, '*($1)*');
259
+
260
+ return `---\n${newLines.join('\n').trim()}\n---${cleanBody}`;
261
+ }
262
+
263
+ /**
264
+ * Convert Claude Code agent .md to OpenCode format
265
+ * - tools: must be object with tool: true pairs
266
+ * - color: must be hex
267
+ * - name: removed (OpenCode uses filename)
268
+ */
269
+ const colorNameToHex = {
270
+ cyan: '#00FFFF', red: '#FF0000', green: '#00FF00', blue: '#0000FF',
271
+ yellow: '#FFFF00', magenta: '#FF00FF', orange: '#FFA500', purple: '#800080',
272
+ };
273
+
274
+ function convertAgentToOpencode(content) {
275
+ // Replace tool references in content
276
+ let converted = content;
277
+ converted = converted.replace(/\bAskUserQuestion\b/g, 'question');
278
+ converted = converted.replace(/\bSlashCommand\b/g, 'skill');
279
+ converted = converted.replace(/\bTodoWrite\b/g, 'todowrite');
280
+ converted = converted.replace(/\/up:/g, '/up-'); // OpenCode flat command structure
281
+
282
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
283
+ if (!frontmatter) return converted;
284
+
285
+ const lines = frontmatter.split('\n');
286
+ const newLines = [];
287
+ const tools = [];
288
+ let inTools = false;
289
+
290
+ for (const line of lines) {
291
+ const trimmed = line.trim();
292
+
293
+ if (trimmed.startsWith('name:')) continue; // OpenCode uses filename
294
+ if (trimmed.startsWith('tools:')) {
295
+ const toolsValue = trimmed.substring(6).trim();
296
+ if (toolsValue) {
297
+ tools.push(...toolsValue.split(',').map(t => t.trim()).filter(t => t));
298
+ } else {
299
+ inTools = true;
300
+ }
301
+ continue;
302
+ }
303
+ if (trimmed.startsWith('color:')) {
304
+ const colorValue = trimmed.substring(6).trim().toLowerCase();
305
+ const hex = colorNameToHex[colorValue];
306
+ if (hex) newLines.push(`color: "${hex}"`);
307
+ continue;
308
+ }
309
+ if (inTools) {
310
+ if (trimmed.startsWith('- ')) {
311
+ tools.push(trimmed.substring(2).trim());
312
+ continue;
313
+ } else if (trimmed && !trimmed.startsWith('-')) {
314
+ inTools = false;
315
+ }
316
+ }
317
+ if (!inTools) newLines.push(line);
318
+ }
319
+
320
+ if (tools.length > 0) {
321
+ newLines.push('tools:');
322
+ for (const tool of tools) {
323
+ newLines.push(` ${convertOpencodeToolName(tool)}: true`);
324
+ }
325
+ }
326
+
327
+ return `---\n${newLines.join('\n').trim()}\n---${body}`;
328
+ }
329
+
330
+ /**
331
+ * Convert Claude Code command .md to Gemini TOML format
332
+ */
333
+ function convertCommandToGeminiToml(content) {
334
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
335
+ let description = '';
336
+ if (frontmatter) {
337
+ description = extractFrontmatterField(frontmatter, 'description') || '';
338
+ }
339
+ let toml = '';
340
+ if (description) toml += `description = ${JSON.stringify(description)}\n`;
341
+ toml += `prompt = ${JSON.stringify(body.trim())}\n`;
342
+ return toml;
343
+ }
344
+
345
+ // ── File Copy ──
346
+
347
+ /**
348
+ * Build path prefix string for $HOME-relative replacement
349
+ */
350
+ function buildPathPrefix(targetDir, isGlobal, runtime) {
351
+ if (isGlobal) return `${targetDir.replace(/\\/g, '/')}/`;
352
+ return `./${getDirName(runtime)}/`;
353
+ }
354
+
355
+ /**
356
+ * Replace path references in content for target runtime
357
+ */
358
+ function replacePaths(content, pathPrefix, runtime) {
359
+ const homePrefix = toHomePrefix(pathPrefix);
360
+ content = content.replace(/\$HOME\/\.claude\//g, homePrefix);
361
+ content = content.replace(/~\/\.claude\//g, homePrefix);
362
+
363
+ if (runtime === 'opencode') {
364
+ content = content.replace(/\/up:/g, '/up-');
365
+ content = content.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"');
366
+ }
367
+
368
+ return content;
369
+ }
370
+
371
+ /**
372
+ * Recursively copy directory with path replacement and optional runtime conversion
373
+ */
374
+ function copyDirWithReplace(src, dest, pathPrefix, runtime, isCommand = false) {
375
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true });
376
+ fs.mkdirSync(dest, { recursive: true });
377
+
378
+ const entries = fs.readdirSync(src, { withFileTypes: true });
379
+ for (const entry of entries) {
380
+ const srcPath = path.join(src, entry.name);
381
+ const destPath = path.join(dest, entry.name);
382
+
383
+ if (entry.isDirectory()) {
384
+ copyDirWithReplace(srcPath, destPath, pathPrefix, runtime, isCommand);
385
+ } else if (entry.name.endsWith('.md') || entry.name.endsWith('.cjs') || entry.name.endsWith('.js') || entry.name.endsWith('.json')) {
386
+ let content = fs.readFileSync(srcPath, 'utf8');
387
+ content = replacePaths(content, pathPrefix, runtime);
388
+
389
+ // Convert commands to TOML for Gemini
390
+ if (runtime === 'gemini' && isCommand && entry.name.endsWith('.md')) {
391
+ const toml = convertCommandToGeminiToml(content);
392
+ fs.writeFileSync(destPath.replace(/\.md$/, '.toml'), toml);
393
+ } else {
394
+ fs.writeFileSync(destPath, content);
395
+ }
396
+ } else {
397
+ fs.copyFileSync(srcPath, destPath);
398
+ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Copy commands flattened for OpenCode (commands/up/help.md → command/up-help.md)
404
+ */
405
+ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
406
+ if (!fs.existsSync(srcDir)) return;
407
+ fs.mkdirSync(destDir, { recursive: true });
408
+
409
+ // Remove old up-*.md files
410
+ if (fs.existsSync(destDir)) {
411
+ for (const file of fs.readdirSync(destDir)) {
412
+ if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
413
+ fs.unlinkSync(path.join(destDir, file));
414
+ }
415
+ }
416
+ }
417
+
418
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
419
+ for (const entry of entries) {
420
+ const srcPath = path.join(srcDir, entry.name);
421
+ if (entry.isDirectory()) {
422
+ copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
423
+ } else if (entry.name.endsWith('.md')) {
424
+ const baseName = entry.name.replace('.md', '');
425
+ const destPath = path.join(destDir, `${prefix}-${baseName}.md`);
426
+ let content = fs.readFileSync(srcPath, 'utf8');
427
+ content = replacePaths(content, pathPrefix, runtime);
428
+ content = convertAgentToOpencode(content);
429
+ fs.writeFileSync(destPath, content);
430
+ }
431
+ }
432
+ }
433
+
434
+ // ── Utility ──
435
+
436
+ function rmDir(dirPath) {
437
+ if (fs.existsSync(dirPath)) fs.rmSync(dirPath, { recursive: true, force: true });
438
+ }
439
+
440
+ function countFiles(dirPath) {
441
+ if (!fs.existsSync(dirPath)) return 0;
442
+ let count = 0;
443
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
444
+ count += entry.isDirectory() ? countFiles(path.join(dirPath, entry.name)) : 1;
445
+ }
446
+ return count;
447
+ }
448
+
449
+ // ── UNINSTALL ──
450
+
451
+ function uninstall(targetDir, runtime) {
452
+ const label = getRuntimeLabel(runtime);
453
+ console.log(` ${yellow}Uninstalling UP from ${label} at ${targetDir}${reset}\n`);
454
+
455
+ if (!fs.existsSync(targetDir)) {
456
+ console.log(` ${dim}Directory does not exist. Nothing to uninstall.${reset}\n`);
457
+ return;
458
+ }
459
+
460
+ let removed = 0;
461
+
462
+ // Remove up/ directory
463
+ const upDir = path.join(targetDir, 'up');
464
+ if (fs.existsSync(upDir)) {
465
+ const count = countFiles(upDir);
466
+ rmDir(upDir);
467
+ console.log(` ${green}✓${reset} Removed up/ (${count} files)`);
468
+ removed += count;
469
+ }
470
+
471
+ // Remove UP agents
472
+ const agentsDir = path.join(targetDir, 'agents');
473
+ if (fs.existsSync(agentsDir)) {
474
+ for (const file of fs.readdirSync(agentsDir)) {
475
+ if (file.startsWith('up-') && file.endsWith('.md')) {
476
+ fs.unlinkSync(path.join(agentsDir, file));
477
+ removed++;
478
+ }
479
+ }
480
+ if (removed > 0) console.log(` ${green}✓${reset} Removed UP agents`);
481
+ }
482
+
483
+ // Remove UP commands (runtime-specific structure)
484
+ if (runtime === 'opencode') {
485
+ const commandDir = path.join(targetDir, 'command');
486
+ if (fs.existsSync(commandDir)) {
487
+ let cmdCount = 0;
488
+ for (const file of fs.readdirSync(commandDir)) {
489
+ if (file.startsWith('up-') && file.endsWith('.md')) {
490
+ fs.unlinkSync(path.join(commandDir, file));
491
+ cmdCount++;
492
+ }
493
+ }
494
+ if (cmdCount > 0) {
495
+ console.log(` ${green}✓${reset} Removed ${cmdCount} commands from command/`);
496
+ removed += cmdCount;
497
+ }
498
+ }
499
+ } else {
500
+ // Claude & Gemini: nested commands/up/ or commands/up/*.toml
501
+ const commandsDir = path.join(targetDir, 'commands', 'up');
502
+ if (fs.existsSync(commandsDir)) {
503
+ const count = countFiles(commandsDir);
504
+ rmDir(commandsDir);
505
+ console.log(` ${green}✓${reset} Removed commands/up/ (${count} files)`);
506
+ removed += count;
507
+ }
508
+ }
509
+
510
+ if (removed === 0) {
511
+ console.log(` ${dim}Nothing to remove — UP is not installed here.${reset}`);
512
+ } else {
513
+ console.log(`\n ${green}Done!${reset} Removed ${removed} files from ${label}.\n`);
514
+ }
515
+ }
516
+
517
+ // ── INSTALL ──
518
+
519
+ function install(isGlobal, runtime) {
520
+ const targetDir = getTargetDir(runtime, isGlobal);
521
+ const pathPrefix = buildPathPrefix(targetDir, isGlobal, runtime);
522
+ const label = getRuntimeLabel(runtime);
523
+ const locationLabel = isGlobal ? targetDir.replace(os.homedir(), '~') : targetDir.replace(process.cwd(), '.');
524
+ const failures = [];
525
+
526
+ console.log(` Installing for ${cyan}${label}${reset} to ${cyan}${locationLabel}${reset}\n`);
527
+
528
+ // 1. Copy up/ (the entire package becomes config/up/)
529
+ const upSrc = packageRoot;
530
+ const upDest = path.join(targetDir, 'up');
531
+ copyDirWithReplace(upSrc, upDest, pathPrefix, runtime);
532
+ const upCount = countFiles(upDest);
533
+ if (upCount > 0) {
534
+ console.log(` ${green}✓${reset} Installed up/ (${upCount} files)`);
535
+ } else {
536
+ failures.push('up/');
537
+ }
538
+
539
+ // 2. Copy commands (runtime-specific structure)
540
+ const cmdsSrc = path.join(packageRoot, 'commands');
541
+ if (fs.existsSync(cmdsSrc)) {
542
+ if (runtime === 'opencode') {
543
+ // OpenCode: flat command/up-*.md
544
+ const commandDir = path.join(targetDir, 'command');
545
+ fs.mkdirSync(commandDir, { recursive: true });
546
+ copyFlattenedCommands(cmdsSrc, commandDir, 'up', pathPrefix, runtime);
547
+ const count = fs.readdirSync(commandDir).filter(f => f.startsWith('up-')).length;
548
+ if (count > 0) {
549
+ console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
550
+ } else {
551
+ failures.push('commands');
552
+ }
553
+ } else {
554
+ // Claude & Gemini: nested commands/up/
555
+ const cmdsDest = path.join(targetDir, 'commands', 'up');
556
+ copyDirWithReplace(cmdsSrc, cmdsDest, pathPrefix, runtime, true);
557
+ const cmdCount = countFiles(cmdsDest);
558
+ if (cmdCount > 0) {
559
+ console.log(` ${green}✓${reset} Installed ${cmdCount} commands`);
560
+ } else {
561
+ failures.push('commands');
562
+ }
563
+ }
564
+ }
565
+
566
+ // 3. Copy agents (with runtime conversion)
567
+ const agentsSrc = path.join(packageRoot, 'agents');
568
+ if (fs.existsSync(agentsSrc)) {
569
+ const agentsDest = path.join(targetDir, 'agents');
570
+ fs.mkdirSync(agentsDest, { recursive: true });
571
+
572
+ // Remove old UP agents
573
+ for (const file of fs.readdirSync(agentsDest)) {
574
+ if (file.startsWith('up-') && file.endsWith('.md')) {
575
+ fs.unlinkSync(path.join(agentsDest, file));
576
+ }
577
+ }
578
+
579
+ // Copy UP agents with runtime conversion
580
+ let agentCount = 0;
581
+ for (const file of fs.readdirSync(agentsSrc)) {
582
+ if (file.endsWith('.md')) {
583
+ let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8');
584
+ content = replacePaths(content, pathPrefix, runtime);
585
+
586
+ if (runtime === 'gemini') {
587
+ content = convertAgentToGemini(content);
588
+ } else if (runtime === 'opencode') {
589
+ content = convertAgentToOpencode(content);
590
+ }
591
+
592
+ fs.writeFileSync(path.join(agentsDest, file), content);
593
+ agentCount++;
594
+ }
595
+ }
596
+ if (agentCount > 0) {
597
+ console.log(` ${green}✓${reset} Installed ${agentCount} agents`);
598
+ } else {
599
+ failures.push('agents');
600
+ }
601
+ }
602
+
603
+ // 4. Write VERSION file
604
+ const versionDest = path.join(upDest, 'VERSION');
605
+ fs.writeFileSync(versionDest, VERSION);
606
+ console.log(` ${green}✓${reset} Wrote VERSION (${VERSION})`);
607
+
608
+ // 5. Write package.json for CommonJS mode (prevents ESM conflicts)
609
+ if (runtime !== 'opencode') {
610
+ const pkgDest = path.join(targetDir, 'package.json');
611
+ if (!fs.existsSync(pkgDest)) {
612
+ fs.writeFileSync(pkgDest, '{"type":"commonjs"}\n');
613
+ console.log(` ${green}✓${reset} Wrote package.json (CommonJS mode)`);
614
+ }
615
+ }
616
+
617
+ // Summary
618
+ if (failures.length > 0) {
619
+ console.log(`\n ${red}Failed:${reset} ${failures.join(', ')}`);
620
+ process.exit(1);
621
+ }
622
+
623
+ const command = runtime === 'opencode' ? '/up-ajuda' : '/up:ajuda';
624
+ console.log(`\n ${green}Done!${reset} Run ${cyan}${command}${reset} in ${label} to get started.\n`);
625
+ }
626
+
627
+ // ── INTERACTIVE MODE ──
628
+
629
+ function promptRuntime(callback) {
630
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
631
+ let answered = false;
632
+ rl.on('close', () => {
633
+ if (!answered) { answered = true; process.exit(0); }
634
+ });
635
+
636
+ console.log(` ${yellow}Which runtime(s)?${reset}\n`);
637
+ console.log(` ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}`);
638
+ console.log(` ${cyan}2${reset}) Gemini ${dim}(~/.gemini)${reset}`);
639
+ console.log(` ${cyan}3${reset}) OpenCode ${dim}(~/.config/opencode)${reset}`);
640
+ console.log(` ${cyan}4${reset}) All\n`);
641
+
642
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
643
+ answered = true;
644
+ rl.close();
645
+ const choice = answer.trim() || '1';
646
+ if (choice === '4') callback(['claude', 'gemini', 'opencode']);
647
+ else if (choice === '3') callback(['opencode']);
648
+ else if (choice === '2') callback(['gemini']);
649
+ else callback(['claude']);
650
+ });
651
+ }
652
+
653
+ function promptLocation(runtimes) {
654
+ if (!process.stdin.isTTY) {
655
+ console.log(` ${yellow}Non-interactive terminal, defaulting to global install${reset}\n`);
656
+ for (const r of runtimes) install(true, r);
657
+ return;
658
+ }
659
+
660
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
661
+ let answered = false;
662
+ rl.on('close', () => {
663
+ if (!answered) { answered = true; process.exit(0); }
664
+ });
665
+
666
+ const globalPaths = runtimes.map(r => getGlobalDir(r).replace(os.homedir(), '~')).join(', ');
667
+ const localPaths = runtimes.map(r => `./${getDirName(r)}`).join(', ');
668
+
669
+ console.log(` ${yellow}Where to install?${reset}\n`);
670
+ console.log(` ${cyan}1${reset}) Global ${dim}(${globalPaths})${reset}`);
671
+ console.log(` ${cyan}2${reset}) Local ${dim}(${localPaths})${reset}\n`);
672
+
673
+ rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
674
+ answered = true;
675
+ rl.close();
676
+ const isGlobal = answer.trim() !== '2';
677
+ for (const r of runtimes) install(isGlobal, r);
678
+ });
679
+ }
680
+
681
+ // ── MAIN ──
682
+
683
+ if (hasGlobal && hasLocal) {
684
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
685
+ process.exit(1);
686
+ } else if (hasUninstall) {
687
+ if (!hasGlobal && !hasLocal) {
688
+ console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
689
+ process.exit(1);
690
+ }
691
+ const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
692
+ for (const r of runtimes) uninstall(getTargetDir(r, hasGlobal), r);
693
+ } else if (selectedRuntimes.length > 0) {
694
+ if (!hasGlobal && !hasLocal) {
695
+ promptLocation(selectedRuntimes);
696
+ } else {
697
+ for (const r of selectedRuntimes) install(hasGlobal, r);
698
+ }
699
+ } else if (hasGlobal || hasLocal) {
700
+ install(hasGlobal, 'claude');
701
+ } else {
702
+ // Fully interactive
703
+ if (!process.stdin.isTTY) {
704
+ console.log(` ${yellow}Non-interactive terminal, defaulting to Claude Code global${reset}\n`);
705
+ install(true, 'claude');
706
+ } else {
707
+ promptRuntime((runtimes) => promptLocation(runtimes));
708
+ }
709
+ }