oxe-cc 0.3.5 → 0.3.6

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.
@@ -5,6 +5,7 @@
5
5
  * Referências: OpenCode (~/.config/opencode/commands), Gemini CLI (~/.gemini/commands/*.toml),
6
6
  * Codex (~/.agents/skills + ~/.codex/prompts), Copilot (~/.copilot/skills), Antigravity (~/.gemini/antigravity/skills),
7
7
  * Windsurf (~/.codeium/windsurf/global_workflows).
8
+ * Com `--ide-local`, destinos equivalentes sob a raiz do projeto (ex.: ./.opencode/commands).
8
9
  */
9
10
 
10
11
  const fs = require('fs');
@@ -21,6 +22,50 @@ function expandTilde(p) {
21
22
  return p;
22
23
  }
23
24
 
25
+ /**
26
+ * @typedef {{
27
+ * ideGlobal: boolean,
28
+ * opencodeCommandDirs: string[],
29
+ * geminiCommandsBase: string,
30
+ * windsurfWorkflowsDir: string,
31
+ * codexPromptsDir: string,
32
+ * codexAgentsSkillsRoot: string,
33
+ * antigravitySkillsRoot: string,
34
+ * }} AgentInstallPaths
35
+ */
36
+
37
+ /**
38
+ * @param {boolean} ideGlobal
39
+ * @param {string} projectRoot
40
+ * @returns {AgentInstallPaths}
41
+ */
42
+ function buildAgentInstallPaths(ideGlobal, projectRoot) {
43
+ const home = os.homedir();
44
+ const root = path.resolve(projectRoot);
45
+ if (ideGlobal) {
46
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
47
+ const codexHome = process.env.CODEX_HOME ? path.resolve(expandTilde(process.env.CODEX_HOME)) : path.join(home, '.codex');
48
+ return {
49
+ ideGlobal: true,
50
+ opencodeCommandDirs: [path.join(xdg, 'opencode', 'commands'), path.join(home, '.opencode', 'commands')],
51
+ geminiCommandsBase: path.join(home, '.gemini', 'commands'),
52
+ windsurfWorkflowsDir: path.join(home, '.codeium', 'windsurf', 'global_workflows'),
53
+ codexPromptsDir: path.join(codexHome, 'prompts'),
54
+ codexAgentsSkillsRoot: path.join(home, '.agents', 'skills'),
55
+ antigravitySkillsRoot: path.join(home, '.gemini', 'antigravity', 'skills'),
56
+ };
57
+ }
58
+ return {
59
+ ideGlobal: false,
60
+ opencodeCommandDirs: [path.join(root, '.opencode', 'commands')],
61
+ geminiCommandsBase: path.join(root, '.gemini', 'commands'),
62
+ windsurfWorkflowsDir: path.join(root, '.windsurf', 'global_workflows'),
63
+ codexPromptsDir: path.join(root, '.codex', 'prompts'),
64
+ codexAgentsSkillsRoot: path.join(root, '.agents', 'skills'),
65
+ antigravitySkillsRoot: path.join(root, '.gemini', 'antigravity', 'skills'),
66
+ };
67
+ }
68
+
24
69
  /** @param {string} content */
25
70
  function adjustWorkflowPathsForNestedLayout(content) {
26
71
  return content
@@ -76,12 +121,11 @@ function buildAgentSkillMarkdown(skillName, description, body) {
76
121
  * @returns {string[]}
77
122
  */
78
123
  function opencodeCommandDirs() {
79
- const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
80
- return [path.join(xdg, 'opencode', 'commands'), path.join(os.homedir(), '.opencode', 'commands')];
124
+ return buildAgentInstallPaths(true, process.cwd()).opencodeCommandDirs;
81
125
  }
82
126
 
83
127
  function windsurfGlobalWorkflowsDir() {
84
- return path.join(os.homedir(), '.codeium', 'windsurf', 'global_workflows');
128
+ return buildAgentInstallPaths(true, process.cwd()).windsurfWorkflowsDir;
85
129
  }
86
130
 
87
131
  function geminiUserDir() {
@@ -89,16 +133,15 @@ function geminiUserDir() {
89
133
  }
90
134
 
91
135
  function codexAgentsSkillsRoot() {
92
- return path.join(os.homedir(), '.agents', 'skills');
136
+ return buildAgentInstallPaths(true, process.cwd()).codexAgentsSkillsRoot;
93
137
  }
94
138
 
95
139
  function codexPromptsDir() {
96
- const home = process.env.CODEX_HOME ? path.resolve(expandTilde(process.env.CODEX_HOME)) : path.join(os.homedir(), '.codex');
97
- return path.join(home, 'prompts');
140
+ return buildAgentInstallPaths(true, process.cwd()).codexPromptsDir;
98
141
  }
99
142
 
100
143
  function antigravitySkillsRoot() {
101
- return path.join(geminiUserDir(), 'antigravity', 'skills');
144
+ return buildAgentInstallPaths(true, process.cwd()).antigravitySkillsRoot;
102
145
  }
103
146
 
104
147
  /**
@@ -169,10 +212,11 @@ function installSkillTreeFromCursorCommands(cCmdSrc, skillsRoot, opts, pathRewri
169
212
 
170
213
  /**
171
214
  * Copia .md dos comandos Cursor para pastas OpenCode (markdown nativo).
215
+ * @param {AgentInstallPaths} paths
172
216
  */
173
- function installOpenCodeCommands(cCmdSrc, opts, pathRewriteNested, logOmitido, logWrite) {
217
+ function installOpenCodeCommands(cCmdSrc, paths, opts, pathRewriteNested, logOmitido, logWrite) {
174
218
  if (!fs.existsSync(cCmdSrc)) return;
175
- for (const destDir of opencodeCommandDirs()) {
219
+ for (const destDir of paths.opencodeCommandDirs) {
176
220
  for (const name of fs.readdirSync(cCmdSrc)) {
177
221
  if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
178
222
  const src = path.join(cCmdSrc, name);
@@ -195,10 +239,11 @@ function installOpenCodeCommands(cCmdSrc, opts, pathRewriteNested, logOmitido, l
195
239
 
196
240
  /**
197
241
  * ~/.gemini/commands/oxe.toml → /oxe ; oxe/scan.toml → /oxe:scan
242
+ * @param {AgentInstallPaths} paths
198
243
  */
199
- function installGeminiTomlCommands(cCmdSrc, opts, pathRewriteNested, logOmitido, logWrite) {
244
+ function installGeminiTomlCommands(cCmdSrc, paths, opts, pathRewriteNested, logOmitido, logWrite) {
200
245
  if (!fs.existsSync(cCmdSrc)) return;
201
- const base = path.join(geminiUserDir(), 'commands');
246
+ const base = paths.geminiCommandsBase;
202
247
 
203
248
  const writeToml = (relPath, srcPath, descSuffix) => {
204
249
  let raw = fs.readFileSync(srcPath, 'utf8');
@@ -233,11 +278,12 @@ function installGeminiTomlCommands(cCmdSrc, opts, pathRewriteNested, logOmitido,
233
278
  }
234
279
 
235
280
  /**
236
- * Windsurf Cascade: workflows globais (~/.codeium/windsurf/global_workflows).
281
+ * Windsurf Cascade: workflows globais ou ./.windsurf/global_workflows (local).
282
+ * @param {AgentInstallPaths} paths
237
283
  */
238
- function installWindsurfGlobalWorkflows(cCmdSrc, opts, pathRewriteNested, logOmitido, logWrite) {
284
+ function installWindsurfGlobalWorkflows(cCmdSrc, paths, opts, pathRewriteNested, logOmitido, logWrite) {
239
285
  if (!fs.existsSync(cCmdSrc)) return;
240
- const destDir = windsurfGlobalWorkflowsDir();
286
+ const destDir = paths.windsurfWorkflowsDir;
241
287
  for (const name of fs.readdirSync(cCmdSrc)) {
242
288
  if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
243
289
  const src = path.join(cCmdSrc, name);
@@ -287,11 +333,12 @@ function installWindsurfGlobalWorkflows(cCmdSrc, opts, pathRewriteNested, logOmi
287
333
  }
288
334
 
289
335
  /**
290
- * Codex: ~/.codex/prompts/oxe-scan.md /prompts:oxe-scan (deprecado mas ainda suportado).
336
+ * Codex: prompts em ~/.codex/prompts ou ./.codex/prompts (local).
337
+ * @param {AgentInstallPaths} paths
291
338
  */
292
- function installCodexPrompts(cCmdSrc, opts, pathRewriteNested, logOmitido, logWrite) {
339
+ function installCodexPrompts(cCmdSrc, paths, opts, pathRewriteNested, logOmitido, logWrite) {
293
340
  if (!fs.existsSync(cCmdSrc)) return;
294
- const destDir = codexPromptsDir();
341
+ const destDir = paths.codexPromptsDir;
295
342
  for (const name of fs.readdirSync(cCmdSrc)) {
296
343
  if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
297
344
  const src = path.join(cCmdSrc, name);
@@ -322,13 +369,16 @@ function installCodexPrompts(cCmdSrc, opts, pathRewriteNested, logOmitido, logWr
322
369
  /**
323
370
  * Remove apenas ficheiros/pastas criados pelo oxe-cc (marcadores).
324
371
  * @param {{ dryRun: boolean }} u
372
+ * @param {AgentInstallPaths} [paths] omissão = instalação global (HOME)
325
373
  */
326
- function cleanupMarkedUnifiedArtifacts(u) {
327
- const unlinkQuiet = (p) => {
328
- if (!fs.existsSync(p)) return;
374
+ function cleanupMarkedUnifiedArtifacts(u, paths) {
375
+ const p = paths || buildAgentInstallPaths(true, process.cwd());
376
+
377
+ const unlinkQuiet = (filePath) => {
378
+ if (!fs.existsSync(filePath)) return;
329
379
  if (u.dryRun) return;
330
380
  try {
331
- fs.unlinkSync(p);
381
+ fs.unlinkSync(filePath);
332
382
  } catch {
333
383
  /* ignore */
334
384
  }
@@ -352,22 +402,22 @@ function cleanupMarkedUnifiedArtifacts(u) {
352
402
  }
353
403
  };
354
404
 
355
- for (const dir of opencodeCommandDirs()) {
405
+ for (const dir of p.opencodeCommandDirs) {
356
406
  if (!fs.existsSync(dir)) continue;
357
407
  for (const name of fs.readdirSync(dir)) {
358
408
  if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
359
- const p = path.join(dir, name);
409
+ const filePath = path.join(dir, name);
360
410
  let txt = '';
361
411
  try {
362
- txt = fs.readFileSync(p, 'utf8');
412
+ txt = fs.readFileSync(filePath, 'utf8');
363
413
  } catch {
364
414
  continue;
365
415
  }
366
- if (txt.includes(OXE_MANAGED_HTML)) unlinkQuiet(p);
416
+ if (txt.includes(OXE_MANAGED_HTML)) unlinkQuiet(filePath);
367
417
  }
368
418
  }
369
419
 
370
- const gBase = path.join(geminiUserDir(), 'commands');
420
+ const gBase = p.geminiCommandsBase;
371
421
  const oxeToml = path.join(gBase, 'oxe.toml');
372
422
  if (fs.existsSync(oxeToml)) {
373
423
  try {
@@ -380,9 +430,9 @@ function cleanupMarkedUnifiedArtifacts(u) {
380
430
  if (fs.existsSync(oxeSub)) {
381
431
  for (const name of fs.readdirSync(oxeSub)) {
382
432
  if (!name.endsWith('.toml')) continue;
383
- const p = path.join(oxeSub, name);
433
+ const filePath = path.join(oxeSub, name);
384
434
  try {
385
- if (fs.readFileSync(p, 'utf8').includes(OXE_MANAGED_TOML)) unlinkQuiet(p);
435
+ if (fs.readFileSync(filePath, 'utf8').includes(OXE_MANAGED_TOML)) unlinkQuiet(filePath);
386
436
  } catch {
387
437
  /* ignore */
388
438
  }
@@ -394,33 +444,33 @@ function cleanupMarkedUnifiedArtifacts(u) {
394
444
  }
395
445
  }
396
446
 
397
- const wfDir = windsurfGlobalWorkflowsDir();
447
+ const wfDir = p.windsurfWorkflowsDir;
398
448
  if (fs.existsSync(wfDir)) {
399
449
  for (const name of fs.readdirSync(wfDir)) {
400
450
  if (name !== 'oxe.md' && !(name.startsWith('oxe-') && name.endsWith('.md'))) continue;
401
- const p = path.join(wfDir, name);
451
+ const filePath = path.join(wfDir, name);
402
452
  try {
403
- if (fs.readFileSync(p, 'utf8').includes(OXE_MANAGED_HTML)) unlinkQuiet(p);
453
+ if (fs.readFileSync(filePath, 'utf8').includes(OXE_MANAGED_HTML)) unlinkQuiet(filePath);
404
454
  } catch {
405
455
  /* ignore */
406
456
  }
407
457
  }
408
458
  }
409
459
 
410
- const cpDir = codexPromptsDir();
460
+ const cpDir = p.codexPromptsDir;
411
461
  if (fs.existsSync(cpDir)) {
412
462
  for (const name of fs.readdirSync(cpDir)) {
413
463
  if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
414
- const p = path.join(cpDir, name);
464
+ const filePath = path.join(cpDir, name);
415
465
  try {
416
- if (fs.readFileSync(p, 'utf8').includes(OXE_MANAGED_HTML)) unlinkQuiet(p);
466
+ if (fs.readFileSync(filePath, 'utf8').includes(OXE_MANAGED_HTML)) unlinkQuiet(filePath);
417
467
  } catch {
418
468
  /* ignore */
419
469
  }
420
470
  }
421
471
  }
422
472
 
423
- const agRoot = antigravitySkillsRoot();
473
+ const agRoot = p.antigravitySkillsRoot;
424
474
  if (fs.existsSync(agRoot)) {
425
475
  for (const name of fs.readdirSync(agRoot, { withFileTypes: true })) {
426
476
  if (!name.isDirectory()) continue;
@@ -429,7 +479,7 @@ function cleanupMarkedUnifiedArtifacts(u) {
429
479
  }
430
480
  }
431
481
 
432
- const cxRoot = codexAgentsSkillsRoot();
482
+ const cxRoot = p.codexAgentsSkillsRoot;
433
483
  if (fs.existsSync(cxRoot)) {
434
484
  for (const name of fs.readdirSync(cxRoot, { withFileTypes: true })) {
435
485
  if (!name.isDirectory()) continue;
@@ -442,6 +492,7 @@ function cleanupMarkedUnifiedArtifacts(u) {
442
492
  module.exports = {
443
493
  OXE_MANAGED_HTML,
444
494
  OXE_MANAGED_TOML,
495
+ buildAgentInstallPaths,
445
496
  adjustWorkflowPathsForNestedLayout,
446
497
  parseCursorCommandFrontmatter,
447
498
  buildAgentSkillMarkdown,
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const semver = require('semver');
5
+
6
+ /**
7
+ * Extrai versão semver do stdout de `npm view <pkg> version`.
8
+ * @param {string} stdout
9
+ * @returns {string | null}
10
+ */
11
+ function parseNpmViewVersion(stdout) {
12
+ if (!stdout || typeof stdout !== 'string') return null;
13
+ const line = stdout.trim().split(/\r?\n/)[0].trim();
14
+ if (!line) return null;
15
+ let candidate = line;
16
+ if ((candidate.startsWith('"') && candidate.endsWith('"')) || (candidate.startsWith("'") && candidate.endsWith("'"))) {
17
+ try {
18
+ candidate = JSON.parse(candidate);
19
+ } catch {
20
+ candidate = line.slice(1, -1);
21
+ }
22
+ }
23
+ candidate = String(candidate).replace(/^v/i, '').trim();
24
+ const coerced = semver.coerce(candidate);
25
+ return coerced ? coerced.version : null;
26
+ }
27
+
28
+ /**
29
+ * @param {string} latest
30
+ * @param {string} current
31
+ * @returns {boolean}
32
+ */
33
+ function isNewerThan(latest, current) {
34
+ if (!semver.valid(latest) || !semver.valid(current)) return false;
35
+ return semver.gt(latest, current);
36
+ }
37
+
38
+ /**
39
+ * @param {string} packageName
40
+ * @param {Record<string, string | undefined>} [spawnOpts] opções extra para spawnSync (ex.: env)
41
+ * @returns {{ ok: true, version: string } | { ok: false, error: string }}
42
+ */
43
+ function syncNpmViewVersion(packageName, spawnOpts = {}) {
44
+ const r = spawnSync('npm', ['view', packageName, 'version'], {
45
+ encoding: 'utf8',
46
+ env: process.env,
47
+ shell: process.platform === 'win32',
48
+ ...spawnOpts,
49
+ });
50
+ if (r.error) return { ok: false, error: r.error.message || String(r.error) };
51
+ if (r.status !== 0 && r.status !== null) {
52
+ const err = (r.stderr || r.stdout || '').trim() || `npm view exited ${r.status}`;
53
+ return { ok: false, error: err };
54
+ }
55
+ const v = parseNpmViewVersion(r.stdout || '');
56
+ if (!v) return { ok: false, error: 'Não foi possível interpretar a versão devolvida pelo npm.' };
57
+ return { ok: true, version: v };
58
+ }
59
+
60
+ module.exports = {
61
+ parseNpmViewVersion,
62
+ isNewerThan,
63
+ syncNpmViewVersion,
64
+ };
@@ -254,6 +254,30 @@ function specSectionWarnings(specPath, requiredHeadings) {
254
254
  return out;
255
255
  }
256
256
 
257
+ /**
258
+ * Avisos quando uma tarefa `### Tn` em PLAN.md não tem linha **Aceite vinculado:** no seu bloco.
259
+ * @param {string} planPath
260
+ * @returns {string[]}
261
+ */
262
+ function planTaskAceiteWarnings(planPath) {
263
+ if (!fs.existsSync(planPath)) return [];
264
+ const raw = fs.readFileSync(planPath, 'utf8');
265
+ const parts = raw.split(/^###\s+(T\d+)\s+/m);
266
+ if (parts.length < 3) return [];
267
+ /** @type {string[]} */
268
+ const out = [];
269
+ for (let i = 1; i < parts.length; i += 2) {
270
+ const taskId = parts[i];
271
+ const body = parts[i + 1] || '';
272
+ if (!/\*\*Aceite\s+vinculado:\*\*/i.test(body)) {
273
+ out.push(
274
+ `PLAN.md: tarefa ${taskId} sem linha **Aceite vinculado:** — ligue cada Tn aos critérios A* da SPEC (ou declare gap explícito no plano)`
275
+ );
276
+ }
277
+ }
278
+ return out;
279
+ }
280
+
257
281
  function planWaveWarningsFixed(planPath, maxPerWave) {
258
282
  if (!maxPerWave || maxPerWave <= 0 || !fs.existsSync(planPath)) return [];
259
283
  const raw = fs.readFileSync(planPath, 'utf8');
@@ -422,7 +446,10 @@ function buildHealthReport(target) {
422
446
  const sumWarn = verifyGapsWithoutSummaryWarning(p.verify, p.summary);
423
447
  const specReq = Array.isArray(config.spec_required_sections) ? config.spec_required_sections : [];
424
448
  const specWarn = specSectionWarnings(p.spec, specReq.map(String));
425
- const planWarn = planWaveWarningsFixed(p.plan, Number(config.plan_max_tasks_per_wave) || 0);
449
+ const planWarn = [
450
+ ...planWaveWarningsFixed(p.plan, Number(config.plan_max_tasks_per_wave) || 0),
451
+ ...planTaskAceiteWarnings(p.plan),
452
+ ];
426
453
  const next = suggestNextStep(target, { discuss_before_plan: config.discuss_before_plan });
427
454
 
428
455
  return {
@@ -458,6 +485,7 @@ module.exports = {
458
485
  verifyGapsWithoutSummaryWarning,
459
486
  specSectionWarnings,
460
487
  planWaveWarningsFixed,
488
+ planTaskAceiteWarnings,
461
489
  suggestNextStep,
462
490
  buildHealthReport,
463
491
  oxePaths,