oxe-cc 0.3.2 → 0.3.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.
package/bin/oxe-cc.js CHANGED
@@ -1,16 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * OXE — install workflows into a target project; bootstrap `.oxe/`; doctor.
4
- * Usage:
5
- * npx oxe-cc [options] [target-dir]
6
- * npx oxe-cc doctor [options] [target-dir]
7
- * npx oxe-cc init-oxe [options] [target-dir]
3
+ * OXE — CLI em pt-BR: instala workflows no projeto, bootstrap `.oxe/`, doctor, uninstall, update.
4
+ * Uso: npx oxe-cc, doctor, status, init-oxe, uninstall, update (ver --help).
8
5
  */
9
6
 
10
7
  const fs = require('fs');
11
8
  const path = require('path');
9
+ const os = require('os');
10
+ const readline = require('readline');
11
+ const readlinePromises = require('readline/promises');
12
+ const { spawnSync } = require('child_process');
12
13
 
13
14
  const PKG_ROOT = path.join(__dirname, '..');
15
+ const oxeManifest = require(path.join(__dirname, 'lib', 'oxe-manifest.cjs'));
16
+ const oxeHealth = require(path.join(__dirname, 'lib', 'oxe-project-health.cjs'));
17
+
18
+ /** GSD-style merge markers for ~/.copilot/copilot-instructions.md */
19
+ const OXE_INST_BEGIN = '<!-- oxe-cc:install-begin -->';
20
+ const OXE_INST_END = '<!-- oxe-cc:install-end -->';
14
21
 
15
22
  const cyan = '\x1b[36m';
16
23
  const green = '\x1b[32m';
@@ -20,6 +27,9 @@ const red = '\x1b[31m';
20
27
  const bold = '\x1b[1m';
21
28
  const reset = '\x1b[0m';
22
29
 
30
+ /** @type {string} */
31
+ const RULE = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
32
+
23
33
  /** Plain banner if banner.txt is missing (keep in sync with bin/banner.txt style). */
24
34
  const DEFAULT_BANNER = ` .============================================.
25
35
  | OXE · spec-driven workflow CLI |
@@ -34,6 +44,18 @@ function useAnsiColors() {
34
44
  return process.stdout.isTTY === true;
35
45
  }
36
46
 
47
+ /** Section header (GSD-inspired). */
48
+ function printSection(title) {
49
+ const c = useAnsiColors();
50
+ if (!c) {
51
+ console.log(`\n${title}\n${'─'.repeat(50)}\n`);
52
+ return;
53
+ }
54
+ console.log(`\n${dim}${RULE}${reset}`);
55
+ console.log(` ${cyan}${bold}${title}${reset}`);
56
+ console.log(`${dim}${RULE}${reset}\n`);
57
+ }
58
+
37
59
  /** Print branded header; skip with OXE_NO_BANNER=1. Not used for --version (scripts). */
38
60
  function printBanner() {
39
61
  if (process.env.OXE_NO_BANNER === '1' || process.env.OXE_NO_BANNER === 'true') return;
@@ -49,6 +71,7 @@ function printBanner() {
49
71
  }
50
72
  }
51
73
  const text = raw.replace(/\{version\}/g, ver).replace(/\r\n/g, '\n').trimEnd();
74
+ if (color) console.log(`${dim}${RULE}${reset}\n`);
52
75
  if (!color) {
53
76
  console.log(text + '\n');
54
77
  return;
@@ -58,10 +81,152 @@ function printBanner() {
58
81
  if (line.includes(`v${ver}`)) console.log(`${dim}${line}${reset}`);
59
82
  else console.log(`${cyan}${bold}${line}${reset}`);
60
83
  }
61
- console.log('');
84
+ if (color) console.log(`\n${dim}${RULE}${reset}\n`);
85
+ else console.log('');
86
+ }
87
+
88
+ /**
89
+ * Caminho amigável: prefere ~/ quando estiver sob o HOME.
90
+ * @param {string} absPath
91
+ */
92
+ function displayPathForUser(absPath) {
93
+ const home = os.homedir();
94
+ const rel = path.relative(home, absPath);
95
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return absPath;
96
+ const normalized = rel.split(path.sep).join('/');
97
+ return '~/' + normalized;
98
+ }
99
+
100
+ /**
101
+ * Rodapé estilo GSD: o que foi afetado + próximos comandos (pt-BR).
102
+ * @param {boolean} c
103
+ * @param {{ bullets: string[], nextSteps: { desc: string, cmd: string }[], dryRun?: boolean }} block
104
+ */
105
+ function printSummaryAndNextSteps(c, { bullets, nextSteps, dryRun = false }) {
106
+ const title = dryRun ? 'Simulação (dry-run)' : 'Resumo do que foi feito';
107
+ console.log(`\n ${c ? dim : ''}${RULE}${reset}`);
108
+ console.log(` ${c ? cyan : ''}${title}${reset}`);
109
+ for (const b of bullets) {
110
+ console.log(` ${c ? green : ''}•${c ? reset : ''} ${b}`);
111
+ }
112
+ if (nextSteps.length) {
113
+ console.log(`\n ${c ? yellow : ''}Próximos passos sugeridos${c ? reset : ''}`);
114
+ let n = 1;
115
+ for (const s of nextSteps) {
116
+ console.log(` ${c ? dim : ''}${n}.${c ? reset : ''} ${s.desc}`);
117
+ console.log(` ${c ? cyan : ''}${s.cmd}${reset}`);
118
+ n += 1;
119
+ }
120
+ }
121
+ console.log(` ${c ? dim : ''}${RULE}${reset}\n`);
122
+ }
123
+
124
+ /**
125
+ * @param {InstallOpts} opts
126
+ * @param {boolean} fullLayout
127
+ * @param {string} cursorBase
128
+ * @param {string} copilotRoot
129
+ * @param {string} claudeBase
130
+ */
131
+ function buildInstallSummary(opts, fullLayout, cursorBase, copilotRoot, claudeBase) {
132
+ const bullets = [];
133
+ const prefix = opts.dryRun ? '[simulação] ' : '';
134
+
135
+ if (opts.oxeOnly) {
136
+ bullets.push(`${prefix}Repositório: .oxe/workflows/ e .oxe/templates/`);
137
+ } else if (fullLayout) {
138
+ bullets.push(`${prefix}Repositório: pastas oxe/ e .oxe/ (workflows e templates)`);
139
+ if (opts.commands) bullets.push(`${prefix}Repositório: commands/oxe/ (comandos estilo Claude)`);
140
+ if (opts.agents) bullets.push(`${prefix}Repositório: AGENTS.md na raiz`);
141
+ if (opts.vscode) bullets.push(`${prefix}Repositório: .vscode/settings.json (chat.promptFiles)`);
142
+ } else {
143
+ bullets.push(`${prefix}Repositório: .oxe/workflows/ e .oxe/templates/ (layout mínimo, sem oxe/ na raiz)`);
144
+ }
145
+
146
+ if (!opts.noInitOxe) {
147
+ bullets.push(`${prefix}Bootstrap .oxe/: STATE.md, config.json e pasta codebase/`);
148
+ }
149
+
150
+ if (opts.cursor) {
151
+ bullets.push(
152
+ `${prefix}Cursor: comandos em ${displayPathForUser(path.join(cursorBase, 'commands'))} e regras em ${displayPathForUser(path.join(cursorBase, 'rules'))}`
153
+ );
154
+ }
155
+ if (opts.copilot) {
156
+ bullets.push(
157
+ `${prefix}Copilot (VS Code): trecho OXE em ${displayPathForUser(path.join(copilotRoot, 'copilot-instructions.md'))} e prompts em ${displayPathForUser(path.join(copilotRoot, 'prompts'))}`
158
+ );
159
+ }
160
+ if (opts.copilotCli) {
161
+ bullets.push(
162
+ `${prefix}CLI: mesmos comandos em ${displayPathForUser(path.join(claudeBase, 'commands'))} e em ${displayPathForUser(path.join(copilotRoot, 'commands'))}`
163
+ );
164
+ }
165
+
166
+ const nextSteps = [];
167
+ nextSteps.push({
168
+ desc: 'Validar workflows e pasta .oxe (rode na raiz do projeto):',
169
+ cmd: 'npx oxe-cc doctor',
170
+ });
171
+ nextSteps.push({
172
+ desc: 'Resumo rápido: coerência .oxe/ e um único próximo passo:',
173
+ cmd: 'npx oxe-cc status',
174
+ });
175
+
176
+ const agentHint = [];
177
+ if (opts.cursor) agentHint.push('Cursor');
178
+ if (opts.copilot) agentHint.push('Copilot no VS Code');
179
+ if (opts.copilotCli) agentHint.push('Copilot CLI / Claude');
180
+ if (agentHint.length) {
181
+ nextSteps.push({
182
+ desc: `Mapear o código no agente (${agentHint.join(', ')}):`,
183
+ cmd: '/oxe-scan',
184
+ });
185
+ } else if (opts.oxeOnly) {
186
+ nextSteps.push({
187
+ desc: 'Para ativar Cursor ou Copilot neste repo, instale de novo sem --oxe-only:',
188
+ cmd: 'npx oxe-cc@latest',
189
+ });
190
+ } else {
191
+ nextSteps.push({
192
+ desc: 'Primeiro passo do fluxo no seu editor:',
193
+ cmd: '/oxe-scan',
194
+ });
195
+ }
196
+
197
+ nextSteps.push({
198
+ desc: 'Atualizar OXE depois (mesmo repositório):',
199
+ cmd: 'npx oxe-cc@latest --force ou npx oxe-cc update',
200
+ });
201
+
202
+ return { bullets, nextSteps, dryRun: opts.dryRun };
203
+ }
204
+
205
+ /**
206
+ * @param {UninstallOpts} u
207
+ */
208
+ function buildUninstallFooter(u) {
209
+ const bullets = [];
210
+ const p = u.dryRun ? '[simulação] ' : '';
211
+ const rm = u.dryRun ? 'Seriam removidos' : 'Removidos';
212
+ if (u.cursor) bullets.push(`${p}${rm} artefatos OXE em ~/.cursor (comandos e regras).`);
213
+ if (u.copilot) bullets.push(`${p}${rm} prompts oxe-* e bloco OXE em copilot-instructions (se existissem).`);
214
+ if (u.copilotCli) bullets.push(`${p}${rm} comandos oxe-* em ~/.claude/commands e ~/.copilot/commands.`);
215
+ if (!u.noProject) {
216
+ bullets.push(
217
+ `${p}${u.dryRun ? 'Seriam removidas' : 'Removidas'} no repositório: .oxe/workflows, .oxe/templates, oxe/ e commands/oxe (o que existir).`
218
+ );
219
+ } else {
220
+ bullets.push(`${p}Pastas do repositório não ${u.dryRun ? 'seriam alteradas' : 'foram alteradas'} (--ide-only).`);
221
+ }
222
+ const nextSteps = [
223
+ { desc: 'Instalar OXE de novo neste projeto:', cmd: 'npx oxe-cc@latest' },
224
+ { desc: 'Conferir o estado após reinstalar:', cmd: 'npx oxe-cc doctor' },
225
+ ];
226
+ return { bullets, nextSteps, dryRun: u.dryRun };
62
227
  }
63
228
 
64
- /** @typedef {{ help: boolean, version: boolean, cursor: boolean, copilot: boolean, vscode: boolean, commands: boolean, agents: boolean, force: boolean, dryRun: boolean, dir: string, all: boolean, noInitOxe: boolean, oxeOnly: boolean, parseError: boolean, unknownFlag: string }} InstallOpts */
229
+ /** @typedef {{ help: boolean, version: boolean, cursor: boolean, copilot: boolean, copilotCli: boolean, vscode: boolean, commands: boolean, agents: boolean, force: boolean, dryRun: boolean, dir: string, all: boolean, noInitOxe: boolean, oxeOnly: boolean, globalCli: boolean, noGlobalCli: boolean, installAssetsGlobal: boolean, explicitScope: boolean, integrationsUnset: boolean, explicitConfigDir: string | null, parseError: boolean, unknownFlag: string, conflictFlags: string }} InstallOpts */
65
230
 
66
231
  /**
67
232
  * @param {string[]} argv
@@ -74,6 +239,7 @@ function parseInstallArgs(argv) {
74
239
  version: false,
75
240
  cursor: false,
76
241
  copilot: false,
242
+ copilotCli: false,
77
243
  vscode: false,
78
244
  commands: true,
79
245
  agents: true,
@@ -83,16 +249,32 @@ function parseInstallArgs(argv) {
83
249
  all: false,
84
250
  noInitOxe: false,
85
251
  oxeOnly: false,
252
+ globalCli: false,
253
+ noGlobalCli: false,
254
+ installAssetsGlobal: false,
255
+ explicitScope: false,
256
+ integrationsUnset: false,
257
+ explicitConfigDir: null,
86
258
  parseError: false,
87
259
  unknownFlag: '',
260
+ conflictFlags: '',
88
261
  restPositional: [],
89
262
  };
90
263
  for (let i = 0; i < argv.length; i++) {
91
264
  const a = argv[i];
92
265
  if (a === '-h' || a === '--help') out.help = true;
93
266
  else if (a === '-v' || a === '--version') out.version = true;
94
- else if (a === '--cursor') out.cursor = true;
267
+ else if ((a === '--config-dir' || a === '-c') && argv[i + 1]) {
268
+ out.explicitConfigDir = path.resolve(expandTilde(argv[++i]));
269
+ } else if (a === '--global') {
270
+ out.installAssetsGlobal = true;
271
+ out.explicitScope = true;
272
+ } else if (a === '--local') {
273
+ out.installAssetsGlobal = false;
274
+ out.explicitScope = true;
275
+ } else if (a === '--cursor') out.cursor = true;
95
276
  else if (a === '--copilot') out.copilot = true;
277
+ else if (a === '--copilot-cli') out.copilotCli = true;
96
278
  else if (a === '--vscode') out.vscode = true;
97
279
  else if (a === '--no-commands') out.commands = false;
98
280
  else if (a === '--no-agents') out.agents = false;
@@ -101,6 +283,8 @@ function parseInstallArgs(argv) {
101
283
  else if (a === '--all' || a === '-a') out.all = true;
102
284
  else if (a === '--no-init-oxe') out.noInitOxe = true;
103
285
  else if (a === '--oxe-only') out.oxeOnly = true;
286
+ else if (a === '--global-cli' || a === '-g') out.globalCli = true;
287
+ else if (a === '--no-global-cli' || a === '-l') out.noGlobalCli = true;
104
288
  else if (a === '--dir' && argv[i + 1]) {
105
289
  out.dir = path.resolve(argv[++i]);
106
290
  } else if (!a.startsWith('-')) out.restPositional.push(a);
@@ -110,15 +294,35 @@ function parseInstallArgs(argv) {
110
294
  break;
111
295
  }
112
296
  }
297
+ if (out.globalCli && out.noGlobalCli) {
298
+ out.conflictFlags = 'Não use --global-cli (-g) e --no-global-cli (-l) ao mesmo tempo';
299
+ }
300
+ if (!out.conflictFlags && argv.includes('--global') && argv.includes('--local')) {
301
+ out.conflictFlags = 'Não use --global e --local ao mesmo tempo';
302
+ }
303
+ if (!out.conflictFlags && out.explicitConfigDir) {
304
+ const ideCount = [out.cursor, out.copilot, out.copilotCli].filter(Boolean).length;
305
+ if (out.oxeOnly || ideCount !== 1) {
306
+ out.conflictFlags =
307
+ '--config-dir exige exatamente um entre --cursor, --copilot e --copilot-cli (e não combina com --oxe-only)';
308
+ }
309
+ }
113
310
  if (out.oxeOnly) {
114
311
  out.cursor = false;
115
312
  out.copilot = false;
313
+ out.copilotCli = false;
116
314
  out.vscode = false;
117
315
  out.commands = false;
118
316
  out.agents = false;
119
- } else if (out.all || (!out.cursor && !out.copilot)) {
317
+ out.integrationsUnset = false;
318
+ } else if (out.all) {
120
319
  out.cursor = true;
121
320
  out.copilot = true;
321
+ out.integrationsUnset = false;
322
+ } else if (!out.cursor && !out.copilot && !out.copilotCli && !out.vscode) {
323
+ out.integrationsUnset = true;
324
+ } else {
325
+ out.integrationsUnset = false;
122
326
  }
123
327
  if (out.restPositional.length) out.dir = path.resolve(out.restPositional[0]);
124
328
  return out;
@@ -134,6 +338,16 @@ function readPkgVersion() {
134
338
  }
135
339
  }
136
340
 
341
+ function readPkgName() {
342
+ try {
343
+ const p = path.join(PKG_ROOT, 'package.json');
344
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
345
+ return typeof j.name === 'string' ? j.name : 'oxe-cc';
346
+ } catch {
347
+ return 'oxe-cc';
348
+ }
349
+ }
350
+
137
351
  function readMinNode() {
138
352
  try {
139
353
  const p = path.join(PKG_ROOT, 'package.json');
@@ -147,6 +361,228 @@ function readMinNode() {
147
361
  }
148
362
  }
149
363
 
364
+ /** @param {string} filePath */
365
+ function expandTilde(filePath) {
366
+ if (filePath && typeof filePath === 'string' && filePath.startsWith('~/')) {
367
+ return path.join(os.homedir(), filePath.slice(2));
368
+ }
369
+ return filePath;
370
+ }
371
+
372
+ /** GSD-style: Windows-native Node on WSL breaks paths — abort with guidance. */
373
+ function assertNotWslWindowsNode() {
374
+ if (process.platform !== 'win32') return;
375
+ let isWsl = false;
376
+ try {
377
+ if (process.env.WSL_DISTRO_NAME) isWsl = true;
378
+ else if (fs.existsSync('/proc/version')) {
379
+ const pv = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
380
+ if (pv.includes('microsoft') || pv.includes('wsl')) isWsl = true;
381
+ }
382
+ } catch {
383
+ /* ignore */
384
+ }
385
+ if (!isWsl) return;
386
+ console.error(`
387
+ ${yellow}Node.js do Windows detectado dentro do WSL.${reset}
388
+
389
+ Isso quebra caminhos (HOME / instalação). Instale o Node nativo no WSL e execute o comando de novo.
390
+ `);
391
+ process.exit(1);
392
+ }
393
+
394
+ /** @param {InstallOpts} opts */
395
+ function cursorUserDir(opts) {
396
+ if (opts.explicitConfigDir && opts.cursor) return path.resolve(expandTilde(opts.explicitConfigDir));
397
+ if (process.env.CURSOR_CONFIG_DIR) return expandTilde(process.env.CURSOR_CONFIG_DIR);
398
+ return path.join(os.homedir(), '.cursor');
399
+ }
400
+
401
+ /** @param {InstallOpts} opts */
402
+ function copilotUserDir(opts) {
403
+ if (opts.explicitConfigDir && opts.copilot) return path.resolve(expandTilde(opts.explicitConfigDir));
404
+ if (process.env.COPILOT_CONFIG_DIR) return expandTilde(process.env.COPILOT_CONFIG_DIR);
405
+ return path.join(os.homedir(), '.copilot');
406
+ }
407
+
408
+ /** @param {InstallOpts} opts */
409
+ function claudeUserDir(opts) {
410
+ if (opts.explicitConfigDir && opts.copilotCli) return path.resolve(expandTilde(opts.explicitConfigDir));
411
+ if (process.env.CLAUDE_CONFIG_DIR) return expandTilde(process.env.CLAUDE_CONFIG_DIR);
412
+ return path.join(os.homedir(), '.claude');
413
+ }
414
+
415
+ /** Layout “clássico”: pasta `oxe/` na raiz do repo. Caso contrário: só `.oxe/` (workflows em `.oxe/workflows`). */
416
+ function useFullRepoLayout(opts) {
417
+ return opts.installAssetsGlobal === true;
418
+ }
419
+
420
+ /** @param {string} content */
421
+ function adjustWorkflowPathsForNestedLayout(content) {
422
+ return content
423
+ .replace(/\boxe\/workflows\//g, '.oxe/workflows/')
424
+ .replace(/\boxe\/templates\//g, '.oxe/templates/');
425
+ }
426
+
427
+ function isTextAssetForPathRewrite(fileName) {
428
+ return (
429
+ fileName.endsWith('.md') ||
430
+ fileName.endsWith('.mdc') ||
431
+ fileName.endsWith('.prompt.md')
432
+ );
433
+ }
434
+
435
+ function escapeForRegExp(s) {
436
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
437
+ }
438
+
439
+ /**
440
+ * GSD-style merge into copilot-instructions.md (user-level).
441
+ * @param {string} srcPath
442
+ * @param {string} destPath
443
+ * @param {{ dryRun: boolean, force: boolean }} opts
444
+ * @param {boolean} idePathRewrite
445
+ */
446
+ function installMergedCopilotInstructions(srcPath, destPath, opts, idePathRewrite) {
447
+ let body = fs.readFileSync(srcPath, 'utf8');
448
+ if (idePathRewrite) body = adjustWorkflowPathsForNestedLayout(body);
449
+ const block = `${OXE_INST_BEGIN}\n${body.trim()}\n${OXE_INST_END}\n`;
450
+ if (opts.dryRun) {
451
+ console.log(`${dim}fusão${reset} copilot-instructions.md → ${destPath}`);
452
+ return;
453
+ }
454
+ if (!fs.existsSync(destPath)) {
455
+ ensureDir(path.dirname(destPath));
456
+ fs.writeFileSync(destPath, block, 'utf8');
457
+ return;
458
+ }
459
+ const existing = fs.readFileSync(destPath, 'utf8');
460
+ if (!opts.force) {
461
+ if (existing.includes(OXE_INST_BEGIN)) {
462
+ console.log(`${dim}omitido${reset} ${destPath} (bloco OXE já existe — use --force para atualizar)`);
463
+ } else {
464
+ console.log(`${dim}omitido${reset} ${destPath} (arquivo existe — use --force para acrescentar o bloco OXE)`);
465
+ }
466
+ return;
467
+ }
468
+ ensureDir(path.dirname(destPath));
469
+ let merged;
470
+ if (existing.includes(OXE_INST_BEGIN)) {
471
+ const re = new RegExp(`${escapeForRegExp(OXE_INST_BEGIN)}[\\s\\S]*?${escapeForRegExp(OXE_INST_END)}`, 'm');
472
+ merged = existing.replace(re, block.trim());
473
+ } else {
474
+ merged = `${existing.trimEnd()}\n\n${block}`;
475
+ }
476
+ fs.writeFileSync(destPath, merged, 'utf8');
477
+ }
478
+
479
+ function canInstallPrompt() {
480
+ return (
481
+ process.stdin.isTTY === true &&
482
+ process.stdout.isTTY === true &&
483
+ process.env.OXE_NO_PROMPT !== '1' &&
484
+ process.env.OXE_NO_PROMPT !== 'true'
485
+ );
486
+ }
487
+
488
+ /** @returns {Promise<{ cursor: boolean, copilot: boolean, copilotCli: boolean, vscode: boolean, commands: boolean, agents: boolean }>} */
489
+ async function promptIntegrationProfile() {
490
+ const rl = readlinePromises.createInterface({ input: process.stdin, output: process.stdout });
491
+ const c = useAnsiColors();
492
+ try {
493
+ console.log(` ${c ? yellow : ''}Onde você quer integrar o OXE?${c ? reset : ''}
494
+ ${c ? cyan : ''}1${c ? reset : ''}) ${c ? dim : ''}Cursor + GitHub Copilot${c ? reset : ''} ${c ? dim : ''}(recomendado)${c ? reset : ''}
495
+ ${c ? cyan : ''}2${c ? reset : ''}) ${c ? dim : ''}Só Cursor${c ? reset : ''}
496
+ ${c ? cyan : ''}3${c ? reset : ''}) ${c ? dim : ''}Só Copilot${c ? reset : ''} ${c ? dim : ''}(VS Code)${c ? reset : ''}
497
+ ${c ? cyan : ''}4${c ? reset : ''}) ${c ? dim : ''}Cursor + Copilot + comandos na CLI${c ? reset : ''} ${c ? dim : ''}(~/.claude e ~/.copilot)${c ? reset : ''}
498
+ ${c ? cyan : ''}5${c ? reset : ''}) ${c ? dim : ''}Só o núcleo${c ? reset : ''} ${c ? dim : ''}(apenas .oxe/ com workflows, sem IDE)${c ? reset : ''}
499
+ `);
500
+ const answer = await rl.question(` ${c ? cyan : ''}Escolha${c ? reset : ''} ${c ? dim : ''}[1]${c ? reset : ''}: `);
501
+ const choice = (answer || '1').trim();
502
+ if (choice === '5') {
503
+ return { cursor: false, copilot: false, copilotCli: false, vscode: false, commands: false, agents: false };
504
+ }
505
+ if (choice === '2') {
506
+ return { cursor: true, copilot: false, copilotCli: false, vscode: false, commands: true, agents: true };
507
+ }
508
+ if (choice === '3') {
509
+ return { cursor: false, copilot: true, copilotCli: false, vscode: false, commands: true, agents: true };
510
+ }
511
+ if (choice === '4') {
512
+ return { cursor: true, copilot: true, copilotCli: true, vscode: false, commands: true, agents: true };
513
+ }
514
+ return { cursor: true, copilot: true, copilotCli: false, vscode: false, commands: true, agents: true };
515
+ } finally {
516
+ rl.close();
517
+ }
518
+ }
519
+
520
+ /** @param {InstallOpts} opts */
521
+ async function promptInstallScope(opts) {
522
+ const hasIde = opts.cursor || opts.copilot || opts.copilotCli;
523
+ if (!hasIde) return;
524
+ const rl = readlinePromises.createInterface({ input: process.stdin, output: process.stdout });
525
+ const c = useAnsiColors();
526
+ try {
527
+ console.log(` ${c ? yellow : ''}Como organizar o OXE no repositório?${c ? reset : ''}
528
+ ${c ? dim : ''}Cursor, Copilot e CLI usam sempre a pasta do seu usuário (${c ? cyan : ''}~/.cursor${c ? dim : ''}, ${c ? cyan : ''}~/.copilot${c ? dim : ''}, ${c ? cyan : ''}~/.claude${c ? dim : ''}).${c ? reset : ''}
529
+
530
+ ${c ? cyan : ''}1${c ? reset : ''}) ${c ? dim : ''}Clássico${c ? reset : ''} — ${c ? dim : ''}pasta ${c ? cyan : ''}oxe/${c ? dim : ''} na raiz + ${c ? cyan : ''}.oxe/${c ? dim : ''} (e, se aplicável, ${c ? cyan : ''}commands/oxe${c ? dim : ''}, ${c ? cyan : ''}AGENTS.md${c ? dim : ''})${c ? reset : ''}
531
+ ${c ? cyan : ''}2${c ? reset : ''}) ${c ? dim : ''}Só ${c ? cyan : ''}.oxe/${c ? reset : ''} ${c ? dim : ''}— workflows em ${c ? cyan : ''}.oxe/workflows/${c ? dim : ''}; sem ${c ? cyan : ''}oxe/${c ? dim : ''} na raiz${c ? reset : ''}
532
+ `);
533
+ const answer = await rl.question(` ${c ? cyan : ''}Escolha${c ? reset : ''} ${c ? dim : ''}[1]${c ? reset : ''}: `);
534
+ const choice = (answer || '1').trim();
535
+ opts.installAssetsGlobal = choice !== '2';
536
+ } finally {
537
+ rl.close();
538
+ }
539
+ }
540
+
541
+ /** @param {InstallOpts} opts */
542
+ async function resolveInteractiveInstall(opts) {
543
+ if (opts.dryRun) {
544
+ if (opts.integrationsUnset) {
545
+ opts.cursor = true;
546
+ opts.copilot = true;
547
+ opts.integrationsUnset = false;
548
+ }
549
+ if (!opts.explicitScope && (opts.cursor || opts.copilot || opts.copilotCli)) {
550
+ opts.installAssetsGlobal = false;
551
+ }
552
+ return;
553
+ }
554
+
555
+ const can = canInstallPrompt();
556
+
557
+ if (opts.integrationsUnset) {
558
+ if (can) {
559
+ const p = await promptIntegrationProfile();
560
+ Object.assign(opts, p);
561
+ opts.integrationsUnset = false;
562
+ } else {
563
+ opts.cursor = true;
564
+ opts.copilot = true;
565
+ opts.integrationsUnset = false;
566
+ const c = useAnsiColors();
567
+ console.log(
568
+ `\n ${c ? yellow : ''}Terminal não interativo${c ? reset : ''} — layout mínimo: só ${c ? cyan : ''}.oxe/${c ? reset : ''}; integrações em ~/.cursor e ~/.copilot. Para ${c ? cyan : ''}oxe/${c ? reset : ''} na raiz use ${c ? cyan : ''}--global${c ? reset : ''}. Outras flags: ${c ? cyan : ''}--cursor${c ? reset : ''}, ${c ? cyan : ''}--copilot${c ? reset : ''}, ${c ? cyan : ''}--oxe-only${c ? reset : ''}, ${c ? cyan : ''}OXE_NO_PROMPT=1${c ? reset : ''}.\n`
569
+ );
570
+ }
571
+ }
572
+
573
+ const hasIde = opts.cursor || opts.copilot || opts.copilotCli;
574
+ if (hasIde && !opts.explicitScope) {
575
+ if (can) await promptInstallScope(opts);
576
+ else {
577
+ opts.installAssetsGlobal = false;
578
+ const c = useAnsiColors();
579
+ console.log(
580
+ `\n ${c ? yellow : ''}Terminal não interativo${c ? reset : ''} — layout do repo: só ${c ? cyan : ''}.oxe/${c ? reset : ''} (opção 2). Use ${c ? cyan : ''}--global${c ? reset : ''} para também criar ${c ? cyan : ''}oxe/${c ? reset : ''} na raiz.\n`
581
+ );
582
+ }
583
+ }
584
+ }
585
+
150
586
  function ensureDir(p) {
151
587
  fs.mkdirSync(p, { recursive: true });
152
588
  }
@@ -161,24 +597,58 @@ function copyFile(src, dest, opts) {
161
597
  fs.copyFileSync(src, dest);
162
598
  }
163
599
 
164
- /** @param {string} srcDir @param {string} destDir @param {{ dryRun: boolean, force: boolean }} opts */
165
- function copyDir(srcDir, destDir, opts) {
600
+ /**
601
+ * @param {string} src
602
+ * @param {string} dest
603
+ * @param {{ dryRun: boolean, force: boolean }} opts
604
+ * @param {boolean} pathRewriteNested
605
+ */
606
+ function copyFileMaybeRewrite(src, dest, opts, pathRewriteNested) {
607
+ if (opts.dryRun) {
608
+ console.log(`${dim}file${reset} ${src} → ${dest}`);
609
+ return;
610
+ }
611
+ if (fs.existsSync(dest) && !opts.force) {
612
+ console.log(`${dim}omitido${reset} ${dest} (já existe)`);
613
+ return;
614
+ }
615
+ ensureDir(path.dirname(dest));
616
+ if (pathRewriteNested && isTextAssetForPathRewrite(path.basename(src))) {
617
+ const t = adjustWorkflowPathsForNestedLayout(fs.readFileSync(src, 'utf8'));
618
+ fs.writeFileSync(dest, t, 'utf8');
619
+ } else {
620
+ fs.copyFileSync(src, dest);
621
+ }
622
+ }
623
+
624
+ /**
625
+ * @param {string} srcDir
626
+ * @param {string} destDir
627
+ * @param {{ dryRun: boolean, force: boolean }} opts
628
+ * @param {boolean} [pathRewriteNested]
629
+ */
630
+ function copyDir(srcDir, destDir, opts, pathRewriteNested = false) {
166
631
  if (!fs.existsSync(srcDir)) return;
167
632
  ensureDir(destDir);
168
633
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
169
634
  for (const e of entries) {
170
635
  const s = path.join(srcDir, e.name);
171
636
  const d = path.join(destDir, e.name);
172
- if (e.isDirectory()) copyDir(s, d, opts);
637
+ if (e.isDirectory()) copyDir(s, d, opts, pathRewriteNested);
173
638
  else {
174
639
  if (fs.existsSync(d) && !opts.force) {
175
- console.log(`${dim}skip${reset} ${d} (exists, use --force)`);
640
+ console.log(`${dim}omitido${reset} ${d} ( existe — use --force para substituir)`);
176
641
  continue;
177
642
  }
178
643
  if (opts.dryRun) console.log(`${dim}file${reset} ${s} → ${d}`);
179
644
  else {
180
645
  ensureDir(path.dirname(d));
181
- fs.copyFileSync(s, d);
646
+ if (pathRewriteNested && isTextAssetForPathRewrite(e.name)) {
647
+ const t = adjustWorkflowPathsForNestedLayout(fs.readFileSync(s, 'utf8'));
648
+ fs.writeFileSync(d, t, 'utf8');
649
+ } else {
650
+ fs.copyFileSync(s, d);
651
+ }
182
652
  }
183
653
  }
184
654
  }
@@ -198,7 +668,7 @@ function bootstrapOxe(target, opts) {
198
668
  const configDest = path.join(oxeDir, 'config.json');
199
669
 
200
670
  if (!fs.existsSync(stateSrc)) {
201
- console.error(`${yellow}warn:${reset} template missing: ${stateSrc}`);
671
+ console.error(`${yellow}aviso:${reset} modelo ausente: ${stateSrc}`);
202
672
  return;
203
673
  }
204
674
 
@@ -213,7 +683,7 @@ function bootstrapOxe(target, opts) {
213
683
  copyFile(stateSrc, stateDest, { dryRun: false });
214
684
  console.log(`${green}init${reset} ${stateDest}`);
215
685
  } else {
216
- console.log(`${dim}skip${reset} ${stateDest} (exists, use --force to replace)`);
686
+ console.log(`${dim}omitido${reset} ${stateDest} ( existe — use --force para substituir)`);
217
687
  }
218
688
 
219
689
  if (fs.existsSync(configSrc)) {
@@ -221,28 +691,133 @@ function bootstrapOxe(target, opts) {
221
691
  copyFile(configSrc, configDest, { dryRun: false });
222
692
  console.log(`${green}init${reset} ${configDest}`);
223
693
  } else {
224
- console.log(`${dim}skip${reset} ${configDest} (exists, use --force to replace)`);
694
+ console.log(`${dim}omitido${reset} ${configDest} ( existe — use --force para substituir)`);
225
695
  }
226
696
  }
227
697
  }
228
698
 
699
+ /** @param {string} targetProject */
700
+ function resolveWorkflowsDir(targetProject) {
701
+ const nested = path.join(targetProject, '.oxe', 'workflows');
702
+ const root = path.join(targetProject, 'oxe', 'workflows');
703
+ if (fs.existsSync(nested)) return nested;
704
+ if (fs.existsSync(root)) return root;
705
+ return null;
706
+ }
707
+
708
+ /**
709
+ * Doctor / status: config estendida, fase STATE, scan antigo, SUMMARY, SPEC/PLAN.
710
+ * @param {string} target
711
+ * @param {boolean} c
712
+ */
713
+ function printOxeHealthDiagnostics(target, c) {
714
+ const r = oxeHealth.buildHealthReport(target);
715
+ const { config } = oxeHealth.loadOxeConfigMerged(target);
716
+
717
+ console.log(`\n ${c ? cyan : ''}▸ Coerência .oxe/ e config${reset}`);
718
+
719
+ if (r.configParseError) {
720
+ console.log(` ${red}FALHA${reset} config.json: ${r.configParseError}`);
721
+ return;
722
+ }
723
+
724
+ for (const err of r.typeErrors) {
725
+ console.log(` ${yellow}AVISO${reset} ${err}`);
726
+ }
727
+ if (r.unknownConfigKeys.length) {
728
+ console.log(
729
+ ` ${yellow}AVISO${reset} Chaves desconhecidas em .oxe/config.json (não usadas pelo oxe-cc): ${r.unknownConfigKeys.join(', ')}`
730
+ );
731
+ }
732
+
733
+ if (r.phase) {
734
+ console.log(` ${c ? dim : ''}Fase (STATE.md):${c ? reset : ''} ${r.phase}`);
735
+ }
736
+
737
+ if (config.scan_max_age_days > 0 && r.scanDate && r.stale.stale) {
738
+ console.log(
739
+ ` ${yellow}AVISO${reset} Último scan há ~${r.stale.days} dia(s) (limite: ${config.scan_max_age_days}) — considere ${cyan}/oxe-scan${reset}`
740
+ );
741
+ } else if (config.scan_max_age_days > 0 && !r.scanDate && fs.existsSync(path.join(target, '.oxe', 'STATE.md'))) {
742
+ console.log(
743
+ ` ${dim}Obs.:${reset} Preencha **Data:** em STATE.md (secção Último scan) para o aviso de scan antigo, ou use scan_max_age_days: 0`
744
+ );
745
+ }
746
+
747
+ if (Array.isArray(config.scan_focus_globs) && config.scan_focus_globs.length) {
748
+ console.log(` ${c ? dim : ''}Scan (foco em .oxe/config):${c ? reset : ''} ${config.scan_focus_globs.join(', ')}`);
749
+ }
750
+ if (Array.isArray(config.scan_ignore_globs) && config.scan_ignore_globs.length) {
751
+ console.log(` ${c ? dim : ''}Scan (ignorar):${c ? reset : ''} ${config.scan_ignore_globs.join(', ')}`);
752
+ }
753
+
754
+ for (const w of r.phaseWarn) {
755
+ console.log(` ${yellow}AVISO${reset} ${w}`);
756
+ }
757
+ if (r.summaryGapWarn) {
758
+ console.log(` ${yellow}AVISO${reset} ${r.summaryGapWarn}`);
759
+ }
760
+ for (const w of r.specWarn) {
761
+ console.log(` ${yellow}AVISO${reset} ${w}`);
762
+ }
763
+ for (const w of r.planWarn) {
764
+ console.log(` ${yellow}AVISO${reset} ${w}`);
765
+ }
766
+ }
767
+
768
+ /**
769
+ * @param {string} target
770
+ */
771
+ function runStatus(target) {
772
+ printSection('OXE ▸ status');
773
+ const c = useAnsiColors();
774
+ console.log(` ${c ? green : ''}Projeto:${c ? reset : ''} ${c ? cyan : ''}${target}${c ? reset : ''}`);
775
+
776
+ const wfTgt = resolveWorkflowsDir(target);
777
+ if (!wfTgt) {
778
+ console.log(` ${yellow}AVISO${reset} Workflows OXE não encontrados — ${cyan}npx oxe-cc@latest${reset}`);
779
+ }
780
+
781
+ printOxeHealthDiagnostics(target, c);
782
+
783
+ const { config } = oxeHealth.loadOxeConfigMerged(target);
784
+ const next = oxeHealth.suggestNextStep(target, { discuss_before_plan: config.discuss_before_plan });
785
+
786
+ console.log(`\n ${c ? yellow : ''}Próximo passo sugerido (único)${reset}`);
787
+ console.log(` ${c ? dim : ''}Passo:${c ? reset : ''} ${c ? green : ''}${next.step}${reset}`);
788
+ console.log(` ${c ? dim : ''}No Cursor:${c ? reset : ''} ${c ? cyan : ''}${next.cursorCmd}${reset}`);
789
+ console.log(` ${c ? dim : ''}Motivo:${c ? reset : ''} ${next.reason}`);
790
+
791
+ printSummaryAndNextSteps(c, {
792
+ bullets: [`Artefatos em jogo: ${next.artifacts.join(', ')}`],
793
+ nextSteps: [
794
+ { desc: 'Diagnóstico completo (inclui pacote de workflows):', cmd: 'npx oxe-cc doctor' },
795
+ { desc: 'Ação sugerida no agente:', cmd: next.cursorCmd },
796
+ ],
797
+ dryRun: false,
798
+ });
799
+ console.log(` ${c ? green : ''}✓${c ? reset : ''} status concluído.\n`);
800
+ }
801
+
229
802
  /** @param {string} target */
230
803
  function runDoctor(target) {
804
+ printSection('OXE ▸ doctor');
231
805
  const v = process.versions.node;
232
806
  const major = parseInt(v.split('.')[0], 10);
233
807
  const minNode = readMinNode();
234
- console.log(`${cyan}oxe-cc doctor${reset} ${target}`);
235
- console.log(`Node.js ${v} (require >= ${minNode})`);
808
+ const c = useAnsiColors();
809
+ console.log(` ${c ? green : ''}Projeto:${c ? reset : ''} ${c ? cyan : ''}${target}${c ? reset : ''}`);
810
+ console.log(` Node.js ${v} (mínimo exigido pelo pacote: ${minNode})`);
236
811
  if (major < minNode) {
237
- console.log(`${red}FAIL${reset} Node.js version below package engines`);
812
+ console.log(`${red}FALHA${reset} Versão do Node abaixo do exigido em engines do pacote`);
238
813
  process.exit(1);
239
814
  }
240
815
  console.log(`${green}OK${reset} Node.js`);
241
816
 
242
817
  const wfPkg = path.join(PKG_ROOT, 'oxe', 'workflows');
243
- const wfTgt = path.join(target, 'oxe', 'workflows');
818
+ const wfTgt = resolveWorkflowsDir(target);
244
819
  if (!fs.existsSync(wfPkg)) {
245
- console.log(`${red}FAIL${reset} package workflows missing: ${wfPkg}`);
820
+ console.log(`${red}FALHA${reset} Workflows do pacote npm ausentes: ${wfPkg}`);
246
821
  process.exit(1);
247
822
  }
248
823
  const expected = fs
@@ -250,8 +825,10 @@ function runDoctor(target) {
250
825
  .filter((f) => f.endsWith('.md'))
251
826
  .sort();
252
827
 
253
- if (!fs.existsSync(wfTgt)) {
254
- console.log(`${yellow}WARN${reset} Target has no oxe/workflows/ — run ${cyan}oxe-cc${reset} to install.`);
828
+ if (!wfTgt) {
829
+ console.log(
830
+ `${yellow}AVISO${reset} Não há oxe/workflows/ nem .oxe/workflows/ neste projeto — rode ${cyan}npx oxe-cc@latest${reset} para instalar.`
831
+ );
255
832
  process.exit(1);
256
833
  }
257
834
 
@@ -263,27 +840,36 @@ function runDoctor(target) {
263
840
  const extra = actual.filter((f) => !expected.includes(f));
264
841
 
265
842
  if (missing.length) {
266
- console.log(`${red}FAIL${reset} Missing workflows vs package: ${missing.join(', ')}`);
843
+ console.log(`${red}FALHA${reset} Faltam workflows em relação ao pacote: ${missing.join(', ')}`);
267
844
  process.exit(1);
268
845
  }
269
- if (extra.length) console.log(`${dim}Note:${reset} Extra workflows in target (ok for forks): ${extra.join(', ')}`);
270
- console.log(`${green}OK${reset} oxe/workflows has all ${expected.length} package files`);
846
+ if (extra.length) {
847
+ console.log(
848
+ `${dim}Obs.:${reset} Há workflows extras no projeto (ok em forks): ${extra.join(', ')}`
849
+ );
850
+ }
851
+ const wfLabel = wfTgt.includes(`${path.sep}.oxe${path.sep}`) ? '.oxe/workflows' : 'oxe/workflows';
852
+ console.log(`${green}OK${reset} ${wfLabel} contém os ${expected.length} arquivos esperados do pacote`);
271
853
 
272
854
  const oxeState = path.join(target, '.oxe', 'STATE.md');
273
- if (fs.existsSync(oxeState)) console.log(`${green}OK${reset} .oxe/STATE.md present`);
274
- else console.log(`${dim}Note:${reset} .oxe/STATE.md absent — run ${cyan}oxe-cc init-oxe${reset} or install without ${cyan}--no-init-oxe${reset}`);
855
+ if (fs.existsSync(oxeState)) console.log(`${green}OK${reset} .oxe/STATE.md encontrado`);
856
+ else {
857
+ console.log(
858
+ `${dim}Obs.:${reset} .oxe/STATE.md ausente — rode ${cyan}oxe-cc init-oxe${reset} ou instale sem ${cyan}--no-init-oxe${reset}`
859
+ );
860
+ }
275
861
 
276
862
  const cfgPath = path.join(target, '.oxe', 'config.json');
277
863
  if (fs.existsSync(cfgPath)) {
278
864
  try {
279
865
  JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
280
- console.log(`${green}OK${reset} .oxe/config.json (valid JSON)`);
866
+ console.log(`${green}OK${reset} .oxe/config.json (JSON válido)`);
281
867
  } catch (e) {
282
- console.log(`${red}FAIL${reset} .oxe/config.json invalid JSON: ${e.message}`);
868
+ console.log(`${red}FALHA${reset} .oxe/config.json com JSON inválido: ${e.message}`);
283
869
  process.exit(1);
284
870
  }
285
871
  } else {
286
- console.log(`${dim}Note:${reset} .oxe/config.json absent (optionalsee oxe/templates/CONFIG.md)`);
872
+ console.log(`${dim}Obs.:${reset} .oxe/config.json ausente (opcionalver oxe/templates/CONFIG.md)`);
287
873
  }
288
874
 
289
875
  const cbDir = path.join(target, '.oxe', 'codebase');
@@ -300,134 +886,744 @@ function runDoctor(target) {
300
886
  const missingMaps = expectedMaps.filter((f) => !fs.existsSync(path.join(cbDir, f)));
301
887
  if (missingMaps.length) {
302
888
  console.log(
303
- `${yellow}Note:${reset} scan incompletemissing under .oxe/codebase/: ${missingMaps.join(', ')} (run ${cyan}/oxe-scan${reset})`
889
+ `${yellow}Obs.:${reset} Mapa do codebase incompleto faltam em .oxe/codebase/: ${missingMaps.join(', ')} (rode ${cyan}/oxe-scan${reset})`
890
+ );
891
+ } else {
892
+ console.log(`${green}OK${reset} .oxe/codebase/ com os ${expectedMaps.length} mapas esperados`);
893
+ }
894
+ }
895
+
896
+ printOxeHealthDiagnostics(target, c);
897
+
898
+ console.log(`\n ${green}Diagnóstico OK — nenhum bloqueio crítico encontrado.${reset}`);
899
+ printSummaryAndNextSteps(c, {
900
+ bullets: [
901
+ `Projeto em ${target}`,
902
+ `Workflows conferidos em ${wfLabel}`,
903
+ 'Node.js e (quando existir) config.json validados',
904
+ ],
905
+ nextSteps: [
906
+ { desc: 'Mapear ou atualizar o codebase no agente:', cmd: '/oxe-scan' },
907
+ { desc: 'Ver ajuda e ordem dos passos OXE:', cmd: '/oxe-help' },
908
+ { desc: 'Reinstalar ou atualizar arquivos do OXE:', cmd: 'npx oxe-cc@latest --force' },
909
+ ],
910
+ dryRun: false,
911
+ });
912
+ }
913
+
914
+ /**
915
+ * npm install -g oxe-cc@version (same version as this running CLI).
916
+ * @returns {boolean}
917
+ */
918
+ function installGlobalCliPackage() {
919
+ const name = readPkgName();
920
+ const ver = readPkgVersion();
921
+ const spec = `${name}@${ver}`;
922
+ const c = useAnsiColors();
923
+ const dimOrEmpty = c ? dim : '';
924
+ const resetOrEmpty = c ? reset : '';
925
+ console.log(`\n ${dimOrEmpty}npm install -g ${spec}${resetOrEmpty}\n`);
926
+ const r = spawnSync('npm', ['install', '-g', spec], {
927
+ stdio: 'inherit',
928
+ shell: true,
929
+ env: process.env,
930
+ });
931
+ if (r.status === 0) {
932
+ console.log(
933
+ `\n ${c ? green : ''}✓${c ? reset : ''} ${c ? cyan : ''}oxe-cc${c ? reset : ''} disponível globalmente (execute ${c ? cyan : ''}oxe-cc --help${c ? reset : ''} em qualquer pasta).\n`
934
+ );
935
+ return true;
936
+ }
937
+ console.log(
938
+ `\n ${c ? yellow : ''}⚠${c ? reset : ''} npm install -g falhou. Tente manualmente: ${c ? cyan : ''}npm install -g ${spec}${c ? reset : ''}\n`
939
+ );
940
+ return false;
941
+ }
942
+
943
+ /**
944
+ * After copying OXE into the project: optionally install the CLI globally (like GSD’s “where to install” choice).
945
+ * @param {InstallOpts} opts
946
+ * @returns {Promise<void>}
947
+ */
948
+ function maybePromptGlobalCli(opts) {
949
+ if (opts.oxeOnly) return Promise.resolve();
950
+ if (opts.dryRun) {
951
+ if (useAnsiColors()) console.log(`${dim} (dry-run — pergunta do CLI global ignorada neste modo)${reset}`);
952
+ return Promise.resolve();
953
+ }
954
+ if (opts.globalCli) {
955
+ installGlobalCliPackage();
956
+ return Promise.resolve();
957
+ }
958
+ if (opts.noGlobalCli) return Promise.resolve();
959
+ if (process.env.OXE_NO_PROMPT === '1' || process.env.OXE_NO_PROMPT === 'true') return Promise.resolve();
960
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
961
+ const c = useAnsiColors();
962
+ if (c) {
963
+ console.log(
964
+ `\n ${yellow}Terminal não interativo${reset} — sem pergunta de CLI global. Use ${cyan}npx oxe-cc@latest${reset} ou ${cyan}--global-cli${reset}.\n`
304
965
  );
305
966
  } else {
306
- console.log(`${green}OK${reset} .oxe/codebase/ has all ${expectedMaps.length} map files`);
967
+ console.log(
968
+ '\nTerminal não interativo — pergunta do CLI global ignorada. Use npx oxe-cc@latest ou --global-cli.\n'
969
+ );
307
970
  }
971
+ return Promise.resolve();
308
972
  }
309
973
 
310
- console.log(`\n${green}Doctor finished.${reset}`);
974
+ const c = useAnsiColors();
975
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
976
+
977
+ console.log(
978
+ ` ${c ? yellow : ''}Instalar o comando oxe-cc globalmente?${c ? reset : ''}
979
+ (Os arquivos OXE já foram copiados para o projeto.)
980
+
981
+ ${c ? cyan : ''}1${c ? reset : ''}) ${c ? dim : ''}Não — uso ${c ? reset : ''}${c ? cyan : ''}npx oxe-cc@latest${c ? reset : ''}${c ? dim : ''} para atualizar (recomendado em CI)${c ? reset : ''}
982
+ ${c ? cyan : ''}2${c ? reset : ''}) ${c ? dim : ''}Sim — ${c ? reset : ''}${c ? cyan : ''}npm install -g ${readPkgName()}@${readPkgVersion()}${c ? reset : ''}${c ? dim : ''} (${c ? reset : ''}${c ? cyan : ''}oxe-cc${c ? reset : ''}${c ? dim : ''} no PATH)${c ? reset : ''}
983
+ `
984
+ );
985
+
986
+ return new Promise((resolve) => {
987
+ rl.question(` ${c ? cyan : ''}Escolha${c ? reset : ''} ${c ? dim : ''}[1]${c ? reset : ''}: `, (answer) => {
988
+ rl.close();
989
+ const choice = (answer || '1').trim();
990
+ if (choice === '2') installGlobalCliPackage();
991
+ else {
992
+ console.log(
993
+ `\n ${c ? green : ''}✓${c ? reset : ''} Para atualizar workflows: ${c ? cyan : ''}npx oxe-cc@latest --force${c ? reset : ''} ou ${c ? cyan : ''}npx oxe-cc update${c ? reset : ''} na raiz do projeto.\n`
994
+ );
995
+ }
996
+ resolve();
997
+ });
998
+ });
311
999
  }
312
1000
 
313
1001
  function usage() {
314
1002
  console.log(`
315
- ${cyan}oxe-cc${reset} — install OXE workflows (Cursor + GitHub Copilot) into a project
316
-
317
- ${green}Usage:${reset}
318
- npx oxe-cc@latest [options] [target-dir]
319
- npx oxe-cc@latest --dir /path/to/project
320
- npx oxe-cc doctor [options] [target-dir]
321
- npx oxe-cc init-oxe [options] [target-dir]
322
-
323
- ${green}Install options:${reset}
324
- --cursor Install .cursor/commands and .cursor/rules (default with --all)
325
- --copilot Install .github/copilot-instructions.md and .github/prompts
326
- --vscode Also copy .vscode/settings.json (chat.promptFiles)
327
- --all, -a Cursor + Copilot (default when neither --cursor nor --copilot)
328
- --no-commands Skip commands/oxe (Claude-style frontmatter)
329
- --no-agents Skip AGENTS.md
330
- --no-init-oxe Do not create .oxe/STATE.md + .oxe/codebase/ after install
331
- --oxe-only Only copy oxe/ (skip Cursor, Copilot, commands, AGENTS.md)
332
- --force, -f Overwrite existing files
333
- --dry-run Print actions without writing
334
- --dir <path> Target directory (default: cwd)
1003
+ ${cyan}oxe-cc${reset} — instala workflows OXE (Cursor + GitHub Copilot) no projeto
1004
+
1005
+ ${green}Uso:${reset}
1006
+ npx oxe-cc@latest [opções] [pasta-do-projeto]
1007
+ npx oxe-cc@latest --dir /caminho/do/projeto
1008
+ npx oxe-cc doctor [opções] [pasta-do-projeto]
1009
+ npx oxe-cc status [opções] [pasta-do-projeto]
1010
+ npx oxe-cc init-oxe [opções] [pasta-do-projeto]
1011
+ npx oxe-cc uninstall [opções] [pasta-do-projeto]
1012
+ npx oxe-cc update [opções] [argumentos extras…]
1013
+
1014
+ ${green}uninstall${reset} (remove OXE da pasta do usuário + pastas de workflows no repo)
1015
+ --cursor / --copilot / --copilot-cli só essa integração (omissão = todas)
1016
+ --ide-only não apagar .oxe/workflows, oxe/, etc. no projeto
1017
+ --config-dir <caminho> com exatamente uma flag IDE acima (estilo GSD)
1018
+ --dry-run
1019
+ --dir <pasta> raiz do projeto (padrão: diretório atual)
1020
+
1021
+ ${green}update${reset} (executa npx oxe-cc@latest --force na pasta do projeto)
1022
+ --dir <pasta> pasta em que o npx roda (padrão: atual)
1023
+ --dry-run mostra o comando sem executar
1024
+ [argumentos extras…] repassados ao oxe-cc (ex.: --cursor --global)
1025
+
1026
+ ${green}Opções da instalação:${reset}
1027
+ --cursor Copia comandos e regras para ~/.cursor (padrão com --all)
1028
+ --copilot Mescla instruções + prompts em ~/.copilot (não fica .github/ no repo)
1029
+ --copilot-cli Copia comandos para ~/.claude/commands e ~/.copilot/commands (CLI — experimental)
1030
+ --vscode Também copia .vscode/settings.json (chat.promptFiles)
1031
+ --all, -a Cursor + Copilot (padrão se não passar --cursor nem --copilot)
1032
+ --no-commands Não copia commands/oxe
1033
+ --no-agents Não copia AGENTS.md
1034
+ --no-init-oxe Não cria .oxe/STATE.md + .oxe/codebase/ após copiar workflows
1035
+ --oxe-only Só .oxe/workflows e templates (sem Cursor, Copilot, commands, AGENTS.md)
1036
+ --global Layout clássico: oxe/ na raiz + .oxe/; IDE em ~/.cursor, ~/.copilot, ~/.claude
1037
+ --local Layout mínimo (padrão): só .oxe/ com .oxe/workflows; IDE nas pastas do usuário
1038
+ --global-cli, -g Depois da cópia: npm install -g oxe-cc@<versão> (sem pergunta)
1039
+ --no-global-cli, -l Não pergunta pelo CLI global (recomendado em CI)
1040
+ --force, -f Sobrescreve arquivos existentes
1041
+ --dry-run Lista ações sem gravar
1042
+ --config-dir, -c <pasta> Só com exatamente um de --cursor, --copilot, --copilot-cli
1043
+ --dir <pasta> Pasta de destino (padrão: diretório atual)
335
1044
  -h, --help
336
1045
  -v, --version
337
1046
 
338
- ${green}Upgrade (project already has OXE):${reset}
339
- npx oxe-cc@latest --force # repo root refresh oxe/, .cursor/, .github/, …
340
- npm install -g oxe-cc@latest && oxe-cc --force # global CLI install
341
- npx clear-npx-cache # if npx keeps an old tarball (npm 7+)
1047
+ ${green}status${reset} (coerência .oxe/ + um próximo passo sugerido; não exige pacote de workflows completo)
1048
+ --dir <pasta> raiz do projeto (padrão: diretório atual)
1049
+
1050
+ ${green}Atualizar (projeto tem OXE):${reset}
1051
+ npx oxe-cc update
1052
+ npx oxe-cc@latest --force
1053
+ npm install -g oxe-cc@latest && oxe-cc --force
1054
+ npx clear-npx-cache # se o npx ficar preso em tarball antigo (npm 7+)
342
1055
 
343
- ${green}Examples:${reset}
1056
+ ${green}Exemplos:${reset}
344
1057
  npx oxe-cc@latest
345
- npx oxe-cc@latest ./my-app
1058
+ npx oxe-cc@latest ./meu-app
346
1059
  npx oxe-cc@latest --cursor --dry-run
1060
+ npx oxe-cc@latest --copilot --copilot-cli
347
1061
  npx oxe-cc doctor
348
- npx oxe-cc init-oxe --dir ./my-app
1062
+ npx oxe-cc init-oxe --dir ./meu-app
1063
+ npx oxe-cc uninstall --dir .
349
1064
  `);
350
1065
  }
351
1066
 
352
1067
  function runInstall(opts) {
353
1068
  const target = opts.dir;
354
1069
  if (!opts.dryRun && !fs.existsSync(target)) {
355
- console.error(`${yellow}Target directory does not exist: ${target}${reset}`);
1070
+ console.error(`${yellow}Diretório não encontrado: ${target}${reset}`);
356
1071
  process.exit(1);
357
1072
  }
358
1073
 
359
- console.log(`${cyan}OXE${reset} install → ${green}${target}${reset}`);
360
- if (opts.dryRun) console.log(`${yellow}(dry-run)${reset}`);
1074
+ assertNotWslWindowsNode();
1075
+ const home = os.homedir();
1076
+ const prevManifest = oxeManifest.loadFileManifest(home);
1077
+ oxeManifest.backupModifiedFromManifest(home, prevManifest, opts, { yellow, cyan, dim, reset });
1078
+
1079
+ printSection('OXE ▸ Instalação no projeto');
1080
+ const c = useAnsiColors();
1081
+ const fullLayout = useFullRepoLayout(opts);
1082
+ const idePathRewrite = !fullLayout;
1083
+
1084
+ console.log(` ${c ? green : ''}Destino:${c ? reset : ''} ${c ? cyan : ''}${target}${c ? reset : ''}`);
1085
+ if (opts.dryRun) console.log(` ${c ? yellow : ''}(dry-run)${c ? reset : ''}`);
1086
+
1087
+ if (fullLayout) {
1088
+ console.log(
1089
+ ` ${c ? dim : ''}Layout repo:${c ? reset : ''} ${c ? yellow : ''}oxe/${c ? reset : ''} na raiz + ${c ? yellow : ''}.oxe/${c ? reset : ''}`
1090
+ );
1091
+ } else {
1092
+ console.log(
1093
+ ` ${c ? dim : ''}Layout repo:${c ? reset : ''} ${c ? yellow : ''}só .oxe/${c ? reset : ''} ${c ? dim : ''}(${c ? cyan : ''}.oxe/workflows${c ? dim : ''})${c ? reset : ''}`
1094
+ );
1095
+ }
1096
+ const ideAny = opts.cursor || opts.copilot || opts.copilotCli;
1097
+ if (ideAny) {
1098
+ console.log(
1099
+ ` ${c ? dim : ''}Integrações IDE:${c ? reset : ''} ${c ? yellow : ''}~/.cursor${c ? reset : ''}, ${c ? yellow : ''}~/.copilot${c ? reset : ''}, ${c ? yellow : ''}~/.claude${c ? reset : ''} ${c ? dim : ''}(conforme opções)${c ? reset : ''}`
1100
+ );
1101
+ }
361
1102
 
362
1103
  const copyOpts = { dryRun: opts.dryRun, force: opts.force };
363
1104
 
364
- copyDir(path.join(PKG_ROOT, 'oxe'), path.join(target, 'oxe'), copyOpts);
1105
+ if (fullLayout) {
1106
+ copyDir(path.join(PKG_ROOT, 'oxe'), path.join(target, 'oxe'), copyOpts, false);
1107
+ } else {
1108
+ const nested = path.join(target, '.oxe');
1109
+ copyDir(path.join(PKG_ROOT, 'oxe', 'workflows'), path.join(nested, 'workflows'), copyOpts, true);
1110
+ copyDir(path.join(PKG_ROOT, 'oxe', 'templates'), path.join(nested, 'templates'), copyOpts, true);
1111
+ }
365
1112
 
1113
+ const cursorBase = cursorUserDir(opts);
366
1114
  if (opts.cursor) {
367
1115
  const cCmd = path.join(PKG_ROOT, '.cursor', 'commands');
368
1116
  const cRules = path.join(PKG_ROOT, '.cursor', 'rules');
369
- if (fs.existsSync(cCmd)) copyDir(cCmd, path.join(target, '.cursor', 'commands'), copyOpts);
370
- if (fs.existsSync(cRules)) copyDir(cRules, path.join(target, '.cursor', 'rules'), copyOpts);
1117
+ if (fs.existsSync(cCmd)) copyDir(cCmd, path.join(cursorBase, 'commands'), copyOpts, idePathRewrite);
1118
+ if (fs.existsSync(cRules)) copyDir(cRules, path.join(cursorBase, 'rules'), copyOpts, idePathRewrite);
371
1119
  }
372
1120
 
1121
+ if (opts.copilotCli) {
1122
+ const cCmd = path.join(PKG_ROOT, '.cursor', 'commands');
1123
+ const clDest = path.join(claudeUserDir(opts), 'commands');
1124
+ const cpCmdDest = path.join(copilotUserDir(opts), 'commands');
1125
+ if (fs.existsSync(cCmd)) {
1126
+ console.log(
1127
+ ` ${c ? green : ''}cli${c ? reset : ''} ${c ? dim : ''}comandos:${c ? reset : ''} ${c ? cyan : ''}${clDest}${c ? reset : ''} ${c ? dim : ''}+${c ? reset : ''} ${c ? cyan : ''}${cpCmdDest}${c ? reset : ''} ${c ? dim : ''}(~/.claude + ~/.copilot)${c ? reset : ''}`
1128
+ );
1129
+ copyDir(cCmd, clDest, copyOpts, idePathRewrite);
1130
+ copyDir(cCmd, cpCmdDest, copyOpts, idePathRewrite);
1131
+ } else {
1132
+ console.warn(`${yellow}aviso:${reset} pasta ausente ${cCmd} — ignorando --copilot-cli`);
1133
+ }
1134
+ }
1135
+
1136
+ const copilotRoot = copilotUserDir(opts);
373
1137
  if (opts.copilot) {
374
1138
  const gh = path.join(PKG_ROOT, '.github');
375
1139
  const inst = path.join(gh, 'copilot-instructions.md');
376
1140
  const prompts = path.join(gh, 'prompts');
377
1141
  if (fs.existsSync(inst)) {
378
- const dest = path.join(target, '.github', 'copilot-instructions.md');
379
- if (opts.dryRun) console.log(`${dim}file${reset} ${inst} ${dest}`);
380
- else {
381
- if (fs.existsSync(dest) && !opts.force) console.log(`${dim}skip${reset} ${dest} (exists)`);
382
- else copyFile(inst, dest, copyOpts);
383
- }
1142
+ const dest = path.join(copilotRoot, 'copilot-instructions.md');
1143
+ installMergedCopilotInstructions(inst, dest, copyOpts, idePathRewrite);
1144
+ }
1145
+ if (fs.existsSync(prompts)) {
1146
+ copyDir(prompts, path.join(copilotRoot, 'prompts'), copyOpts, idePathRewrite);
384
1147
  }
385
- if (fs.existsSync(prompts)) copyDir(prompts, path.join(target, '.github', 'prompts'), copyOpts);
386
1148
  }
387
1149
 
388
- if (opts.vscode) {
1150
+ if (opts.vscode && fullLayout) {
389
1151
  const vs = path.join(PKG_ROOT, '.vscode', 'settings.json');
390
1152
  if (fs.existsSync(vs)) {
391
1153
  const dest = path.join(target, '.vscode', 'settings.json');
392
1154
  if (opts.dryRun) console.log(`${dim}file${reset} ${vs} → ${dest}`);
393
1155
  else {
394
- if (fs.existsSync(dest) && !opts.force) console.log(`${dim}skip${reset} ${dest} (exists)`);
1156
+ if (fs.existsSync(dest) && !opts.force) console.log(`${dim}omitido${reset} ${dest} (já existe)`);
395
1157
  else copyFile(vs, dest, copyOpts);
396
1158
  }
397
1159
  }
398
1160
  }
399
1161
 
400
- if (opts.commands) {
1162
+ if (opts.commands && fullLayout) {
401
1163
  const cmdSrc = path.join(PKG_ROOT, 'commands', 'oxe');
402
1164
  const cmdDest = path.join(target, 'commands', 'oxe');
403
- if (fs.existsSync(cmdSrc)) copyDir(cmdSrc, cmdDest, copyOpts);
1165
+ if (fs.existsSync(cmdSrc)) copyDir(cmdSrc, cmdDest, copyOpts, idePathRewrite);
404
1166
  }
405
1167
 
406
- if (opts.agents) {
1168
+ if (opts.agents && fullLayout) {
407
1169
  const agents = path.join(PKG_ROOT, 'AGENTS.md');
408
1170
  if (fs.existsSync(agents)) {
409
1171
  const dest = path.join(target, 'AGENTS.md');
410
1172
  if (opts.dryRun) console.log(`${dim}file${reset} ${agents} → ${dest}`);
411
- else if (fs.existsSync(dest) && !opts.force) console.log(`${dim}skip${reset} ${dest} (exists)`);
412
- else copyFile(agents, dest, copyOpts);
1173
+ else if (fs.existsSync(dest) && !opts.force) console.log(`${dim}omitido${reset} ${dest} (já existe)`);
1174
+ else copyFileMaybeRewrite(agents, dest, copyOpts, idePathRewrite);
413
1175
  }
414
1176
  }
415
1177
 
416
1178
  if (!opts.noInitOxe) bootstrapOxe(target, { dryRun: opts.dryRun, force: opts.force });
417
1179
 
418
- console.log(
419
- `\n${green}Done.${reset} Open the project in Cursor (${cyan}/oxe-scan${reset}) or VS Code + Copilot (prompt ${cyan}/oxe-scan${reset}).`
1180
+ if (!opts.dryRun && (opts.cursor || opts.copilot || opts.copilotCli)) {
1181
+ const nextFiles = {};
1182
+ const addTracked = (root, nameFilter) => {
1183
+ if (!fs.existsSync(root)) return;
1184
+ const files = oxeManifest.collectFilesRecursive(root, nameFilter);
1185
+ for (const f of files) {
1186
+ try {
1187
+ nextFiles[f] = oxeManifest.sha256File(f);
1188
+ } catch {
1189
+ /* skip */
1190
+ }
1191
+ }
1192
+ };
1193
+ if (opts.cursor) {
1194
+ addTracked(path.join(cursorBase, 'commands'), (n) => n.startsWith('oxe-') && n.endsWith('.md'));
1195
+ addTracked(path.join(cursorBase, 'rules'), (n) => n.includes('oxe') && (n.endsWith('.mdc') || n.endsWith('.md')));
1196
+ }
1197
+ if (opts.copilot) {
1198
+ const instP = path.join(copilotRoot, 'copilot-instructions.md');
1199
+ if (fs.existsSync(instP)) {
1200
+ try {
1201
+ nextFiles[instP] = oxeManifest.sha256File(instP);
1202
+ } catch {
1203
+ /* skip */
1204
+ }
1205
+ }
1206
+ addTracked(path.join(copilotRoot, 'prompts'), (n) => n.startsWith('oxe-'));
1207
+ }
1208
+ if (opts.copilotCli) {
1209
+ addTracked(path.join(claudeUserDir(opts), 'commands'), (n) => n.startsWith('oxe-') && n.endsWith('.md'));
1210
+ addTracked(path.join(copilotRoot, 'commands'), (n) => n.startsWith('oxe-') && n.endsWith('.md'));
1211
+ }
1212
+ const mergedManifest = { ...prevManifest, ...nextFiles };
1213
+ oxeManifest.writeFileManifest(home, mergedManifest, readPkgVersion());
1214
+ }
1215
+
1216
+ printSummaryAndNextSteps(
1217
+ c,
1218
+ buildInstallSummary(opts, fullLayout, cursorBase, copilotRoot, claudeUserDir(opts))
1219
+ );
1220
+ console.log(` ${c ? green : ''}✓${c ? reset : ''} Instalação concluída com sucesso.\n`);
1221
+ }
1222
+
1223
+ /** @typedef {{ help: boolean, dryRun: boolean, cursor: boolean, copilot: boolean, copilotCli: boolean, ideExplicit: boolean, noProject: boolean, dir: string, explicitConfigDir: string | null, parseError: boolean, unknownFlag: string, conflictFlags: string }} UninstallOpts */
1224
+
1225
+ /**
1226
+ * @param {string[]} argv
1227
+ * @returns {UninstallOpts}
1228
+ */
1229
+ function parseUninstallArgs(argv) {
1230
+ /** @type {UninstallOpts} */
1231
+ const out = {
1232
+ help: false,
1233
+ dryRun: false,
1234
+ cursor: false,
1235
+ copilot: false,
1236
+ copilotCli: false,
1237
+ ideExplicit: false,
1238
+ noProject: false,
1239
+ dir: process.cwd(),
1240
+ explicitConfigDir: null,
1241
+ parseError: false,
1242
+ unknownFlag: '',
1243
+ conflictFlags: '',
1244
+ };
1245
+ const rest = [];
1246
+ for (let i = 0; i < argv.length; i++) {
1247
+ const a = argv[i];
1248
+ if (a === '-h' || a === '--help') out.help = true;
1249
+ else if (a === '--dry-run') out.dryRun = true;
1250
+ else if ((a === '--config-dir' || a === '-c') && argv[i + 1]) {
1251
+ out.explicitConfigDir = path.resolve(expandTilde(argv[++i]));
1252
+ } else if (a === '--cursor') {
1253
+ out.cursor = true;
1254
+ out.ideExplicit = true;
1255
+ } else if (a === '--copilot') {
1256
+ out.copilot = true;
1257
+ out.ideExplicit = true;
1258
+ } else if (a === '--copilot-cli') {
1259
+ out.copilotCli = true;
1260
+ out.ideExplicit = true;
1261
+ } else if (a === '--ide-only') out.noProject = true;
1262
+ else if (a === '--dir' && argv[i + 1]) out.dir = path.resolve(argv[++i]);
1263
+ else if (!a.startsWith('-')) rest.push(path.resolve(a));
1264
+ else {
1265
+ out.parseError = true;
1266
+ out.unknownFlag = a;
1267
+ break;
1268
+ }
1269
+ }
1270
+ if (rest.length) out.dir = rest[0];
1271
+ if (!out.ideExplicit) {
1272
+ out.cursor = true;
1273
+ out.copilot = true;
1274
+ out.copilotCli = true;
1275
+ }
1276
+ if (!out.conflictFlags && out.explicitConfigDir) {
1277
+ const n = [out.cursor, out.copilot, out.copilotCli].filter(Boolean).length;
1278
+ if (n !== 1) {
1279
+ out.conflictFlags =
1280
+ '--config-dir exige exatamente um entre --cursor, --copilot e --copilot-cli';
1281
+ }
1282
+ }
1283
+ return out;
1284
+ }
1285
+
1286
+ /**
1287
+ * @param {string} destPath
1288
+ * @param {{ dryRun: boolean }} opts
1289
+ */
1290
+ function stripOxeFromCopilotInstructions(destPath, opts) {
1291
+ if (!fs.existsSync(destPath)) return;
1292
+ const existing = fs.readFileSync(destPath, 'utf8');
1293
+ if (!existing.includes(OXE_INST_BEGIN)) return;
1294
+ if (opts.dryRun) {
1295
+ console.log(`${dim}strip${reset} bloco OXE em ${destPath}`);
1296
+ return;
1297
+ }
1298
+ const re = new RegExp(
1299
+ `\\n?${escapeForRegExp(OXE_INST_BEGIN)}[\\s\\S]*?${escapeForRegExp(OXE_INST_END)}\\n?`,
1300
+ 'm'
420
1301
  );
1302
+ const merged = existing.replace(re, '\n').replace(/\n{3,}/g, '\n\n').trimEnd();
1303
+ fs.writeFileSync(destPath, merged ? `${merged}\n` : '\n', 'utf8');
1304
+ }
1305
+
1306
+ /**
1307
+ * @param {string} filePath
1308
+ * @param {{ dryRun: boolean }} opts
1309
+ */
1310
+ function unlinkQuiet(filePath, opts) {
1311
+ if (!fs.existsSync(filePath)) return;
1312
+ if (opts.dryRun) {
1313
+ console.log(`${dim}rm${reset} ${filePath}`);
1314
+ return;
1315
+ }
1316
+ fs.unlinkSync(filePath);
1317
+ }
1318
+
1319
+ /**
1320
+ * @param {string} dirPath
1321
+ * @param {{ dryRun: boolean }} opts
1322
+ */
1323
+ function rmDirIfEmpty(dirPath, opts) {
1324
+ if (!fs.existsSync(dirPath) || opts.dryRun) return;
1325
+ try {
1326
+ const n = fs.readdirSync(dirPath);
1327
+ if (n.length === 0) fs.rmdirSync(dirPath);
1328
+ } catch {
1329
+ /* ignore */
1330
+ }
1331
+ }
1332
+
1333
+ /**
1334
+ * @param {UninstallOpts} u
1335
+ */
1336
+ function runUninstall(u) {
1337
+ assertNotWslWindowsNode();
1338
+ const c = useAnsiColors();
1339
+ const home = os.homedir();
1340
+ const ideOpts = /** @type {InstallOpts} */ ({
1341
+ help: false,
1342
+ version: false,
1343
+ cursor: u.cursor,
1344
+ copilot: u.copilot,
1345
+ copilotCli: u.copilotCli,
1346
+ vscode: false,
1347
+ commands: false,
1348
+ agents: false,
1349
+ force: true,
1350
+ dryRun: u.dryRun,
1351
+ dir: u.dir,
1352
+ all: false,
1353
+ noInitOxe: true,
1354
+ oxeOnly: false,
1355
+ globalCli: false,
1356
+ noGlobalCli: true,
1357
+ installAssetsGlobal: false,
1358
+ explicitScope: true,
1359
+ integrationsUnset: false,
1360
+ explicitConfigDir: u.explicitConfigDir,
1361
+ parseError: false,
1362
+ unknownFlag: '',
1363
+ conflictFlags: '',
1364
+ });
1365
+
1366
+ printSection('OXE ▸ uninstall');
1367
+ console.log(` ${c ? green : ''}Projeto:${c ? reset : ''} ${c ? cyan : ''}${u.dir}${c ? reset : ''}`);
1368
+ if (u.dryRun) console.log(` ${c ? yellow : ''}(dry-run)${c ? reset : ''}`);
1369
+
1370
+ const removedPaths = [];
1371
+
1372
+ if (u.cursor) {
1373
+ const base = cursorUserDir(ideOpts);
1374
+ const cmdDir = path.join(base, 'commands');
1375
+ const ruleDir = path.join(base, 'rules');
1376
+ if (fs.existsSync(cmdDir)) {
1377
+ for (const name of fs.readdirSync(cmdDir)) {
1378
+ if (name.startsWith('oxe-') && name.endsWith('.md')) {
1379
+ const p = path.join(cmdDir, name);
1380
+ unlinkQuiet(p, u);
1381
+ removedPaths.push(p);
1382
+ }
1383
+ }
1384
+ }
1385
+ if (fs.existsSync(ruleDir)) {
1386
+ for (const name of fs.readdirSync(ruleDir)) {
1387
+ if (name.includes('oxe') && (name.endsWith('.mdc') || name.endsWith('.md'))) {
1388
+ const p = path.join(ruleDir, name);
1389
+ unlinkQuiet(p, u);
1390
+ removedPaths.push(p);
1391
+ }
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ if (u.copilot) {
1397
+ const root = copilotUserDir(ideOpts);
1398
+ const inst = path.join(root, 'copilot-instructions.md');
1399
+ stripOxeFromCopilotInstructions(inst, u);
1400
+ const pr = path.join(root, 'prompts');
1401
+ if (fs.existsSync(pr)) {
1402
+ for (const name of fs.readdirSync(pr)) {
1403
+ if (name.startsWith('oxe-')) {
1404
+ const p = path.join(pr, name);
1405
+ unlinkQuiet(p, u);
1406
+ removedPaths.push(p);
1407
+ }
1408
+ }
1409
+ }
1410
+ }
1411
+
1412
+ if (u.copilotCli) {
1413
+ for (const base of [claudeUserDir(ideOpts), copilotUserDir(ideOpts)]) {
1414
+ const cmdDir = path.join(base, 'commands');
1415
+ if (!fs.existsSync(cmdDir)) continue;
1416
+ for (const name of fs.readdirSync(cmdDir)) {
1417
+ if (name.startsWith('oxe-') && name.endsWith('.md')) {
1418
+ const p = path.join(cmdDir, name);
1419
+ unlinkQuiet(p, u);
1420
+ removedPaths.push(p);
1421
+ }
1422
+ }
1423
+ }
1424
+ }
1425
+
1426
+ if (!u.noProject) {
1427
+ const target = u.dir;
1428
+ const nestedWf = path.join(target, '.oxe', 'workflows');
1429
+ const nestedTpl = path.join(target, '.oxe', 'templates');
1430
+ const globalOxe = path.join(target, 'oxe');
1431
+ const globalCmd = path.join(target, 'commands', 'oxe');
1432
+
1433
+ const rmTree = (p) => {
1434
+ if (!fs.existsSync(p)) return;
1435
+ if (u.dryRun) {
1436
+ console.log(`${dim}rm -r${reset} ${p}`);
1437
+ return;
1438
+ }
1439
+ fs.rmSync(p, { recursive: true, force: true });
1440
+ };
1441
+
1442
+ if (fs.existsSync(nestedWf)) rmTree(nestedWf);
1443
+ if (fs.existsSync(nestedTpl)) rmTree(nestedTpl);
1444
+ if (fs.existsSync(globalOxe)) rmTree(globalOxe);
1445
+ if (fs.existsSync(globalCmd)) rmTree(globalCmd);
1446
+
1447
+ if (!u.dryRun) {
1448
+ rmDirIfEmpty(path.join(target, '.oxe', 'templates'), u);
1449
+ rmDirIfEmpty(path.join(target, '.oxe', 'workflows'), u);
1450
+ }
1451
+ }
1452
+
1453
+ if (!u.dryRun && (u.cursor || u.copilot || u.copilotCli)) {
1454
+ const prev = oxeManifest.loadFileManifest(home);
1455
+ const next = { ...prev };
1456
+ for (const p of removedPaths) delete next[p];
1457
+ if (u.copilot) {
1458
+ const instPath = path.join(copilotUserDir(ideOpts), 'copilot-instructions.md');
1459
+ if (fs.existsSync(instPath)) {
1460
+ try {
1461
+ next[instPath] = oxeManifest.sha256File(instPath);
1462
+ } catch {
1463
+ delete next[instPath];
1464
+ }
1465
+ } else {
1466
+ delete next[instPath];
1467
+ }
1468
+ }
1469
+ oxeManifest.writeFileManifest(home, next, readPkgVersion());
1470
+ }
1471
+
1472
+ printSummaryAndNextSteps(c, buildUninstallFooter(u));
1473
+ console.log(` ${c ? green : ''}✓${c ? reset : ''} Desinstalação concluída com sucesso.\n`);
1474
+ }
1475
+
1476
+ /** @typedef {{ help: boolean, dryRun: boolean, dir: string, rest: string[], parseError: boolean, unknownFlag: string }} UpdateOpts */
1477
+
1478
+ /**
1479
+ * @param {string[]} argv
1480
+ * @returns {UpdateOpts}
1481
+ */
1482
+ function parseUpdateArgs(argv) {
1483
+ /** @type {UpdateOpts} */
1484
+ const out = {
1485
+ help: false,
1486
+ dryRun: false,
1487
+ dir: process.cwd(),
1488
+ rest: [],
1489
+ parseError: false,
1490
+ unknownFlag: '',
1491
+ };
1492
+ let dirExplicit = false;
1493
+ let firstPositionalConsumed = false;
1494
+ for (let i = 0; i < argv.length; i++) {
1495
+ const a = argv[i];
1496
+ if (a === '-h' || a === '--help') out.help = true;
1497
+ else if (a === '--dry-run') out.dryRun = true;
1498
+ else if (a === '--dir' && argv[i + 1]) {
1499
+ out.dir = path.resolve(argv[++i]);
1500
+ dirExplicit = true;
1501
+ } else if (!a.startsWith('-')) {
1502
+ if (!dirExplicit && !firstPositionalConsumed) {
1503
+ out.dir = path.resolve(a);
1504
+ firstPositionalConsumed = true;
1505
+ } else out.rest.push(a);
1506
+ } else {
1507
+ out.parseError = true;
1508
+ out.unknownFlag = a;
1509
+ break;
1510
+ }
1511
+ }
1512
+ return out;
1513
+ }
1514
+
1515
+ /**
1516
+ * @param {UpdateOpts} u
1517
+ */
1518
+ function runUpdate(u) {
1519
+ assertNotWslWindowsNode();
1520
+ const c = useAnsiColors();
1521
+ if (u.dryRun) {
1522
+ printSection('OXE ▸ update (simulação)');
1523
+ console.log(` ${dim}Comando que seria executado:${reset}`);
1524
+ console.log(` ${cyan}npx -y oxe-cc@latest --force --no-global-cli -l${reset} ${u.rest.join(' ')}`);
1525
+ console.log(` ${dim}Diretório:${reset} ${u.dir}`);
1526
+ printSummaryAndNextSteps(c, {
1527
+ bullets: ['[simulação] O npx baixaria o pacote oxe-cc@latest e rodaria a instalação com --force.'],
1528
+ nextSteps: [
1529
+ { desc: 'Rodar de verdade (sem --dry-run), na pasta do projeto:', cmd: 'npx oxe-cc update' },
1530
+ { desc: 'Depois, validar:', cmd: 'npx oxe-cc doctor' },
1531
+ ],
1532
+ dryRun: true,
1533
+ });
1534
+ return;
1535
+ }
1536
+ printSection('OXE ▸ update');
1537
+ const args = ['-y', 'oxe-cc@latest', '--force', '--no-global-cli', '-l', ...u.rest];
1538
+ const r = spawnSync('npx', args, {
1539
+ cwd: u.dir,
1540
+ stdio: 'inherit',
1541
+ env: { ...process.env },
1542
+ shell: process.platform === 'win32',
1543
+ });
1544
+ if (r.error) {
1545
+ console.error(`${red}Falha ao executar npx:${reset}`, r.error.message);
1546
+ process.exit(1);
1547
+ }
1548
+ if (r.status !== 0 && r.status !== null) process.exit(r.status);
1549
+ printSummaryAndNextSteps(c, {
1550
+ bullets: [
1551
+ 'Pacote oxe-cc atualizado via npx (--force); arquivos do projeto e integrações foram alinhados à versão publicada.',
1552
+ ],
1553
+ nextSteps: [
1554
+ { desc: 'Validar workflows e .oxe na raiz do projeto:', cmd: 'npx oxe-cc doctor' },
1555
+ { desc: 'Retomar o fluxo no agente:', cmd: '/oxe-scan' },
1556
+ { desc: 'Ajuda geral no chat:', cmd: '/oxe-help' },
1557
+ ],
1558
+ dryRun: false,
1559
+ });
1560
+ console.log(` ${c ? green : ''}✓${c ? reset : ''} Atualização concluída com sucesso.\n`);
421
1561
  }
422
1562
 
423
- function main() {
1563
+ async function main() {
424
1564
  const argv = process.argv.slice(2);
425
1565
  let command = 'install';
426
- if (argv[0] === 'doctor' || argv[0] === 'init-oxe') {
1566
+ if (
1567
+ argv[0] === 'doctor' ||
1568
+ argv[0] === 'status' ||
1569
+ argv[0] === 'init-oxe' ||
1570
+ argv[0] === 'uninstall' ||
1571
+ argv[0] === 'update'
1572
+ ) {
427
1573
  command = argv[0];
428
1574
  argv.shift();
429
1575
  }
430
1576
 
1577
+ if (command === 'uninstall') {
1578
+ const u = parseUninstallArgs(argv);
1579
+ if (u.help) {
1580
+ printBanner();
1581
+ usage();
1582
+ process.exit(0);
1583
+ }
1584
+ if (u.conflictFlags) {
1585
+ printBanner();
1586
+ console.error(`${red}${u.conflictFlags}${reset}`);
1587
+ usage();
1588
+ process.exit(1);
1589
+ }
1590
+ if (u.parseError) {
1591
+ printBanner();
1592
+ console.error(`${red}Opção desconhecida:${reset} ${u.unknownFlag}`);
1593
+ usage();
1594
+ process.exit(1);
1595
+ }
1596
+ printBanner();
1597
+ if (!u.dryRun && !fs.existsSync(u.dir)) {
1598
+ console.error(`${yellow}Diretório não encontrado: ${u.dir}${reset}`);
1599
+ process.exit(1);
1600
+ }
1601
+ runUninstall(u);
1602
+ return;
1603
+ }
1604
+
1605
+ if (command === 'update') {
1606
+ const u = parseUpdateArgs(argv);
1607
+ if (u.help) {
1608
+ printBanner();
1609
+ usage();
1610
+ process.exit(0);
1611
+ }
1612
+ if (u.parseError) {
1613
+ printBanner();
1614
+ console.error(`${red}Opção desconhecida:${reset} ${u.unknownFlag}`);
1615
+ usage();
1616
+ process.exit(1);
1617
+ }
1618
+ printBanner();
1619
+ if (!u.dryRun && !fs.existsSync(u.dir)) {
1620
+ console.error(`${yellow}Diretório não encontrado: ${u.dir}${reset}`);
1621
+ process.exit(1);
1622
+ }
1623
+ runUpdate(u);
1624
+ return;
1625
+ }
1626
+
431
1627
  const opts = parseInstallArgs(argv);
432
1628
 
433
1629
  if (opts.version) {
@@ -435,9 +1631,16 @@ function main() {
435
1631
  process.exit(0);
436
1632
  }
437
1633
 
1634
+ if (opts.conflictFlags) {
1635
+ printBanner();
1636
+ console.error(`${red}${opts.conflictFlags}${reset}`);
1637
+ usage();
1638
+ process.exit(1);
1639
+ }
1640
+
438
1641
  if (opts.parseError) {
439
1642
  printBanner();
440
- console.error(`${red}Unknown option:${reset} ${opts.unknownFlag}`);
1643
+ console.error(`${red}Opção desconhecida:${reset} ${opts.unknownFlag}`);
441
1644
  usage();
442
1645
  process.exit(1);
443
1646
  }
@@ -453,26 +1656,54 @@ function main() {
453
1656
  const target = opts.dir;
454
1657
  if (command === 'doctor') {
455
1658
  if (!fs.existsSync(target)) {
456
- console.error(`${yellow}Target directory does not exist: ${target}${reset}`);
1659
+ console.error(`${yellow}Diretório não encontrado: ${target}${reset}`);
457
1660
  process.exit(1);
458
1661
  }
459
1662
  runDoctor(target);
460
1663
  return;
461
1664
  }
462
1665
 
1666
+ if (command === 'status') {
1667
+ if (!fs.existsSync(target)) {
1668
+ console.error(`${yellow}Diretório não encontrado: ${target}${reset}`);
1669
+ process.exit(1);
1670
+ }
1671
+ printBanner();
1672
+ runStatus(target);
1673
+ return;
1674
+ }
1675
+
463
1676
  if (command === 'init-oxe') {
464
1677
  if (!opts.dryRun && !fs.existsSync(target)) {
465
- console.error(`${yellow}Target directory does not exist: ${target}${reset}`);
1678
+ console.error(`${yellow}Diretório não encontrado: ${target}${reset}`);
466
1679
  process.exit(1);
467
1680
  }
468
- console.log(`${cyan}OXE${reset} init-oxe → ${green}${target}${reset}`);
469
- if (opts.dryRun) console.log(`${yellow}(dry-run)${reset}`);
1681
+ printSection('OXE init-oxe');
1682
+ const c0 = useAnsiColors();
1683
+ console.log(` ${c0 ? green : ''}Destino:${c0 ? reset : ''} ${c0 ? cyan : ''}${target}${c0 ? reset : ''}`);
1684
+ if (opts.dryRun) console.log(` ${c0 ? yellow : ''}(dry-run)${c0 ? reset : ''}`);
470
1685
  bootstrapOxe(target, { dryRun: opts.dryRun, force: opts.force });
471
- console.log(`\n${green}Done.${reset}`);
1686
+ printSummaryAndNextSteps(c0, {
1687
+ bullets: opts.dryRun
1688
+ ? ['[simulação] Seriam criados ou atualizados .oxe/STATE.md, .oxe/config.json e .oxe/codebase/']
1689
+ : ['.oxe/STATE.md, .oxe/config.json e pasta .oxe/codebase/ (criados ou atualizados conforme --force)'],
1690
+ nextSteps: [
1691
+ { desc: 'Validar o projeto:', cmd: 'npx oxe-cc doctor' },
1692
+ { desc: 'Instalar integrações Cursor/Copilot (se ainda não fez):', cmd: 'npx oxe-cc@latest' },
1693
+ { desc: 'Começar o fluxo no agente:', cmd: '/oxe-scan' },
1694
+ ],
1695
+ dryRun: opts.dryRun,
1696
+ });
1697
+ console.log(` ${c0 ? green : ''}✓${c0 ? reset : ''} init-oxe concluído com sucesso.\n`);
472
1698
  return;
473
1699
  }
474
1700
 
1701
+ await resolveInteractiveInstall(opts);
475
1702
  runInstall(opts);
1703
+ await maybePromptGlobalCli(opts);
476
1704
  }
477
1705
 
478
- main();
1706
+ main().catch((err) => {
1707
+ console.error(err);
1708
+ process.exit(1);
1709
+ });