oxe-cc 0.3.3 → 0.3.5

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 (42) hide show
  1. package/.cursor/commands/oxe-discuss.md +4 -2
  2. package/.cursor/commands/oxe-execute.md +6 -2
  3. package/.cursor/commands/oxe-help.md +10 -2
  4. package/.cursor/commands/oxe-next.md +10 -2
  5. package/.cursor/commands/oxe-plan.md +13 -2
  6. package/.cursor/commands/oxe-quick.md +6 -2
  7. package/.cursor/commands/oxe-review-pr.md +16 -0
  8. package/.cursor/commands/oxe-scan.md +13 -2
  9. package/.cursor/commands/oxe-spec.md +13 -2
  10. package/.cursor/commands/oxe-verify.md +13 -2
  11. package/.github/copilot-instructions.md +35 -14
  12. package/AGENTS.md +4 -2
  13. package/README.md +310 -244
  14. package/assets/readme-banner.svg +19 -18
  15. package/bin/lib/oxe-agent-install.cjs +460 -0
  16. package/bin/lib/oxe-install-resolve.cjs +93 -0
  17. package/bin/lib/oxe-manifest.cjs +117 -0
  18. package/bin/lib/oxe-project-health.cjs +464 -0
  19. package/bin/lib/oxe-workflows.cjs +145 -0
  20. package/bin/oxe-cc.js +1406 -123
  21. package/lib/sdk/README.md +54 -0
  22. package/lib/sdk/index.cjs +241 -0
  23. package/lib/sdk/index.d.ts +89 -0
  24. package/oxe/templates/CONFIG.md +32 -12
  25. package/oxe/templates/PLAN.template.md +1 -1
  26. package/oxe/templates/SPEC.template.md +9 -5
  27. package/oxe/templates/STATE.md +9 -1
  28. package/oxe/templates/WORKFLOW_AUTHORING.md +73 -0
  29. package/oxe/templates/config.template.json +18 -6
  30. package/oxe/workflows/discuss.md +31 -31
  31. package/oxe/workflows/execute.md +36 -28
  32. package/oxe/workflows/help.md +56 -22
  33. package/oxe/workflows/next.md +27 -13
  34. package/oxe/workflows/plan.md +14 -13
  35. package/oxe/workflows/quick.md +45 -32
  36. package/oxe/workflows/review-pr.md +13 -13
  37. package/oxe/workflows/scan.md +13 -11
  38. package/oxe/workflows/spec.md +15 -14
  39. package/oxe/workflows/verify.md +19 -16
  40. package/oxe/workflows/workflow-authoring.md +34 -0
  41. package/package.json +30 -3
  42. package/.cursor/rules/oxe-workflow.mdc +0 -15
@@ -0,0 +1,460 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Instalação multi-plataforma estilo: mesmos fluxos OXE em vários “homes” de agentes.
5
+ * Referências: OpenCode (~/.config/opencode/commands), Gemini CLI (~/.gemini/commands/*.toml),
6
+ * Codex (~/.agents/skills + ~/.codex/prompts), Copilot (~/.copilot/skills), Antigravity (~/.gemini/antigravity/skills),
7
+ * Windsurf (~/.codeium/windsurf/global_workflows).
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const OXE_MANAGED_HTML = '<!-- oxe-cc managed -->';
15
+ const OXE_MANAGED_TOML = '# oxe-cc managed';
16
+
17
+ function expandTilde(p) {
18
+ if (typeof p !== 'string') return p;
19
+ if (p === '~' || p.startsWith(`~${path.sep}`)) return path.join(os.homedir(), p.slice(2));
20
+ if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(os.homedir(), p.slice(2));
21
+ return p;
22
+ }
23
+
24
+ /** @param {string} content */
25
+ function adjustWorkflowPathsForNestedLayout(content) {
26
+ return content
27
+ .replace(/\boxe\/workflows\//g, '.oxe/workflows/')
28
+ .replace(/\boxe\/templates\//g, '.oxe/templates/');
29
+ }
30
+
31
+ /**
32
+ * @param {string} text
33
+ * @returns {{ description: string, body: string }}
34
+ */
35
+ function parseCursorCommandFrontmatter(text) {
36
+ const normalized = text.replace(/\r\n/g, '\n');
37
+ if (!normalized.startsWith('---\n')) {
38
+ return { description: '', body: normalized.trim() };
39
+ }
40
+ const end = normalized.indexOf('\n---\n', 4);
41
+ if (end === -1) {
42
+ return { description: '', body: normalized.trim() };
43
+ }
44
+ const yamlBlock = normalized.slice(4, end);
45
+ let description = '';
46
+ for (const line of yamlBlock.split('\n')) {
47
+ const m = line.match(/^description:\s*(.+)$/);
48
+ if (m) {
49
+ description = m[1].trim().replace(/^["']|["']$/g, '');
50
+ break;
51
+ }
52
+ }
53
+ const body = normalized.slice(end + 5).trim();
54
+ return { description, body };
55
+ }
56
+
57
+ /**
58
+ * @param {string} skillName
59
+ * @param {string} description
60
+ * @param {string} body
61
+ */
62
+ function buildAgentSkillMarkdown(skillName, description, body) {
63
+ const desc = description.trim() || `Comando OXE — ${skillName}`;
64
+ return (
65
+ `---\n` +
66
+ `name: ${skillName}\n` +
67
+ `description: ${JSON.stringify(desc)}\n` +
68
+ `user-invocable: true\n` +
69
+ `---\n\n` +
70
+ `${OXE_MANAGED_HTML}\n\n` +
71
+ `${body}\n`
72
+ );
73
+ }
74
+
75
+ /**
76
+ * @returns {string[]}
77
+ */
78
+ 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')];
81
+ }
82
+
83
+ function windsurfGlobalWorkflowsDir() {
84
+ return path.join(os.homedir(), '.codeium', 'windsurf', 'global_workflows');
85
+ }
86
+
87
+ function geminiUserDir() {
88
+ return path.join(os.homedir(), '.gemini');
89
+ }
90
+
91
+ function codexAgentsSkillsRoot() {
92
+ return path.join(os.homedir(), '.agents', 'skills');
93
+ }
94
+
95
+ 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');
98
+ }
99
+
100
+ function antigravitySkillsRoot() {
101
+ return path.join(geminiUserDir(), 'antigravity', 'skills');
102
+ }
103
+
104
+ /**
105
+ * Insere marcador após o frontmatter YAML inicial, se existir.
106
+ * @param {string} raw
107
+ * @param {boolean} pathRewriteNested
108
+ */
109
+ function injectManagedAfterFrontmatter(raw, pathRewriteNested) {
110
+ let t = pathRewriteNested ? adjustWorkflowPathsForNestedLayout(raw) : raw;
111
+ if (!t.startsWith('---\n')) return `${OXE_MANAGED_HTML}\n\n${t}`;
112
+ const end = t.indexOf('\n---\n', 4);
113
+ if (end === -1) return `${OXE_MANAGED_HTML}\n\n${t}`;
114
+ return t.slice(0, end + 5) + `\n${OXE_MANAGED_HTML}\n\n` + t.slice(end + 5);
115
+ }
116
+
117
+ /**
118
+ * @param {string} description
119
+ * @param {string} body
120
+ */
121
+ function buildGeminiToml(description, body) {
122
+ const desc = description.trim() || 'OXE';
123
+ const safeBody = body.replace(/"""/g, '\\"\\"\\"');
124
+ return `${OXE_MANAGED_TOML}\ndescription = ${JSON.stringify(desc)}\nprompt = """\n${safeBody}\n"""\n`;
125
+ }
126
+
127
+ /**
128
+ * @param {string} cCmdSrc
129
+ * @param {string} skillsRoot
130
+ * @param {{ dryRun: boolean, force: boolean }} opts
131
+ * @param {boolean} pathRewriteNested
132
+ * @param {(s: string) => void} [logOmitido]
133
+ * @param {(s: string) => void} [logWrite]
134
+ */
135
+ function installSkillTreeFromCursorCommands(cCmdSrc, skillsRoot, opts, pathRewriteNested, logOmitido, logWrite) {
136
+ if (!fs.existsSync(cCmdSrc)) return;
137
+
138
+ const writeOne = (skillName, srcPath, descSuffix) => {
139
+ let raw = fs.readFileSync(srcPath, 'utf8');
140
+ if (pathRewriteNested) raw = adjustWorkflowPathsForNestedLayout(raw);
141
+ const { description, body } = parseCursorCommandFrontmatter(raw);
142
+ const desc = descSuffix ? `${description.trim()} ${descSuffix}`.trim() : description.trim();
143
+ const md = buildAgentSkillMarkdown(skillName, desc, body);
144
+ const destDir = path.join(skillsRoot, skillName);
145
+ const dest = path.join(destDir, 'SKILL.md');
146
+ if (opts.dryRun) {
147
+ if (logWrite) logWrite(`${srcPath} → ${dest}`);
148
+ return;
149
+ }
150
+ if (fs.existsSync(dest) && !opts.force) {
151
+ if (logOmitido) logOmitido(dest);
152
+ return;
153
+ }
154
+ fs.mkdirSync(destDir, { recursive: true });
155
+ fs.writeFileSync(dest, md, 'utf8');
156
+ };
157
+
158
+ for (const name of fs.readdirSync(cCmdSrc)) {
159
+ if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
160
+ const skillName = name.replace(/\.md$/i, '');
161
+ writeOne(skillName, path.join(cCmdSrc, name));
162
+ }
163
+
164
+ const helpPath = path.join(cCmdSrc, 'oxe-help.md');
165
+ if (fs.existsSync(helpPath)) {
166
+ writeOne('oxe', helpPath, 'Ponto de entrada /oxe (mesmo fluxo que oxe-help).');
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Copia .md dos comandos Cursor para pastas OpenCode (markdown nativo).
172
+ */
173
+ function installOpenCodeCommands(cCmdSrc, opts, pathRewriteNested, logOmitido, logWrite) {
174
+ if (!fs.existsSync(cCmdSrc)) return;
175
+ for (const destDir of opencodeCommandDirs()) {
176
+ for (const name of fs.readdirSync(cCmdSrc)) {
177
+ if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
178
+ const src = path.join(cCmdSrc, name);
179
+ const dest = path.join(destDir, name);
180
+ if (opts.dryRun) {
181
+ if (logWrite) logWrite(`opencode ${src} → ${dest}`);
182
+ continue;
183
+ }
184
+ if (fs.existsSync(dest) && !opts.force) {
185
+ if (logOmitido) logOmitido(dest);
186
+ continue;
187
+ }
188
+ const raw = fs.readFileSync(src, 'utf8');
189
+ const out = injectManagedAfterFrontmatter(raw, pathRewriteNested);
190
+ fs.mkdirSync(destDir, { recursive: true });
191
+ fs.writeFileSync(dest, out, 'utf8');
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * ~/.gemini/commands/oxe.toml → /oxe ; oxe/scan.toml → /oxe:scan
198
+ */
199
+ function installGeminiTomlCommands(cCmdSrc, opts, pathRewriteNested, logOmitido, logWrite) {
200
+ if (!fs.existsSync(cCmdSrc)) return;
201
+ const base = path.join(geminiUserDir(), 'commands');
202
+
203
+ const writeToml = (relPath, srcPath, descSuffix) => {
204
+ let raw = fs.readFileSync(srcPath, 'utf8');
205
+ if (pathRewriteNested) raw = adjustWorkflowPathsForNestedLayout(raw);
206
+ const { description, body } = parseCursorCommandFrontmatter(raw);
207
+ const desc = descSuffix ? `${description} ${descSuffix}`.trim() : description;
208
+ const toml = buildGeminiToml(desc, body);
209
+ const dest = path.join(base, relPath);
210
+ if (opts.dryRun) {
211
+ if (logWrite) logWrite(`gemini ${srcPath} → ${dest}`);
212
+ return;
213
+ }
214
+ if (fs.existsSync(dest) && !opts.force) {
215
+ if (logOmitido) logOmitido(dest);
216
+ return;
217
+ }
218
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
219
+ fs.writeFileSync(dest, toml, 'utf8');
220
+ };
221
+
222
+ const helpPath = path.join(cCmdSrc, 'oxe-help.md');
223
+ if (fs.existsSync(helpPath)) {
224
+ writeToml('oxe.toml', helpPath, '(Gemini: /oxe)');
225
+ }
226
+
227
+ const oxeDir = path.join(base, 'oxe');
228
+ for (const name of fs.readdirSync(cCmdSrc)) {
229
+ if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
230
+ const short = name.replace(/^oxe-/i, '').replace(/\.md$/i, '');
231
+ writeToml(path.join('oxe', `${short}.toml`), path.join(cCmdSrc, name), `(Gemini: /oxe:${short})`);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Windsurf Cascade: workflows globais (~/.codeium/windsurf/global_workflows).
237
+ */
238
+ function installWindsurfGlobalWorkflows(cCmdSrc, opts, pathRewriteNested, logOmitido, logWrite) {
239
+ if (!fs.existsSync(cCmdSrc)) return;
240
+ const destDir = windsurfGlobalWorkflowsDir();
241
+ for (const name of fs.readdirSync(cCmdSrc)) {
242
+ if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
243
+ const src = path.join(cCmdSrc, name);
244
+ const dest = path.join(destDir, name);
245
+ if (opts.dryRun) {
246
+ if (logWrite) logWrite(`windsurf ${src} → ${dest}`);
247
+ continue;
248
+ }
249
+ if (fs.existsSync(dest) && !opts.force) {
250
+ if (logOmitido) logOmitido(dest);
251
+ continue;
252
+ }
253
+ let raw = fs.readFileSync(src, 'utf8');
254
+ if (pathRewriteNested) raw = adjustWorkflowPathsForNestedLayout(raw);
255
+ const { description, body } = parseCursorCommandFrontmatter(raw);
256
+ const title = description || name.replace(/\.md$/i, '');
257
+ const out =
258
+ `---\n` +
259
+ `description: ${JSON.stringify(title)}\n` +
260
+ `---\n\n` +
261
+ `${OXE_MANAGED_HTML}\n\n` +
262
+ `# ${name.replace(/\.md$/i, '')}\n\n` +
263
+ `${body}\n`;
264
+ fs.mkdirSync(destDir, { recursive: true });
265
+ fs.writeFileSync(dest, out, 'utf8');
266
+ }
267
+ const helpPath = path.join(cCmdSrc, 'oxe-help.md');
268
+ const helpDest = path.join(destDir, 'oxe.md');
269
+ if (fs.existsSync(helpPath)) {
270
+ if (opts.dryRun) {
271
+ if (logWrite) logWrite(`windsurf ${helpPath} → ${helpDest}`);
272
+ } else if (!fs.existsSync(helpDest) || opts.force) {
273
+ let raw = fs.readFileSync(helpPath, 'utf8');
274
+ if (pathRewriteNested) raw = adjustWorkflowPathsForNestedLayout(raw);
275
+ const { description, body } = parseCursorCommandFrontmatter(raw);
276
+ const out =
277
+ `---\n` +
278
+ `description: ${JSON.stringify(`${description} (OXE /oxe)`.trim())}\n` +
279
+ `---\n\n` +
280
+ `${OXE_MANAGED_HTML}\n\n` +
281
+ `# oxe\n\n` +
282
+ `${body}\n`;
283
+ fs.mkdirSync(destDir, { recursive: true });
284
+ fs.writeFileSync(helpDest, out, 'utf8');
285
+ } else if (logOmitido) logOmitido(helpDest);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Codex: ~/.codex/prompts/oxe-scan.md → /prompts:oxe-scan (deprecado mas ainda suportado).
291
+ */
292
+ function installCodexPrompts(cCmdSrc, opts, pathRewriteNested, logOmitido, logWrite) {
293
+ if (!fs.existsSync(cCmdSrc)) return;
294
+ const destDir = codexPromptsDir();
295
+ for (const name of fs.readdirSync(cCmdSrc)) {
296
+ if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
297
+ const src = path.join(cCmdSrc, name);
298
+ const dest = path.join(destDir, name);
299
+ if (opts.dryRun) {
300
+ if (logWrite) logWrite(`codex prompts ${src} → ${dest}`);
301
+ continue;
302
+ }
303
+ if (fs.existsSync(dest) && !opts.force) {
304
+ if (logOmitido) logOmitido(dest);
305
+ continue;
306
+ }
307
+ let raw = fs.readFileSync(src, 'utf8');
308
+ if (pathRewriteNested) raw = adjustWorkflowPathsForNestedLayout(raw);
309
+ const { description, body } = parseCursorCommandFrontmatter(raw);
310
+ const out =
311
+ `---\n` +
312
+ `description: ${JSON.stringify(description || 'OXE')}\n` +
313
+ `argument-hint: [texto livre opcional]\n` +
314
+ `---\n\n` +
315
+ `${OXE_MANAGED_HTML}\n\n` +
316
+ `${body}\n`;
317
+ fs.mkdirSync(destDir, { recursive: true });
318
+ fs.writeFileSync(dest, out, 'utf8');
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Remove apenas ficheiros/pastas criados pelo oxe-cc (marcadores).
324
+ * @param {{ dryRun: boolean }} u
325
+ */
326
+ function cleanupMarkedUnifiedArtifacts(u) {
327
+ const unlinkQuiet = (p) => {
328
+ if (!fs.existsSync(p)) return;
329
+ if (u.dryRun) return;
330
+ try {
331
+ fs.unlinkSync(p);
332
+ } catch {
333
+ /* ignore */
334
+ }
335
+ };
336
+
337
+ const rmDirIfOxeSkill = (skillDir) => {
338
+ const sm = path.join(skillDir, 'SKILL.md');
339
+ if (!fs.existsSync(sm)) return;
340
+ let txt = '';
341
+ try {
342
+ txt = fs.readFileSync(sm, 'utf8');
343
+ } catch {
344
+ return;
345
+ }
346
+ if (!txt.includes(OXE_MANAGED_HTML)) return;
347
+ if (u.dryRun) return;
348
+ try {
349
+ fs.rmSync(skillDir, { recursive: true, force: true });
350
+ } catch {
351
+ /* ignore */
352
+ }
353
+ };
354
+
355
+ for (const dir of opencodeCommandDirs()) {
356
+ if (!fs.existsSync(dir)) continue;
357
+ for (const name of fs.readdirSync(dir)) {
358
+ if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
359
+ const p = path.join(dir, name);
360
+ let txt = '';
361
+ try {
362
+ txt = fs.readFileSync(p, 'utf8');
363
+ } catch {
364
+ continue;
365
+ }
366
+ if (txt.includes(OXE_MANAGED_HTML)) unlinkQuiet(p);
367
+ }
368
+ }
369
+
370
+ const gBase = path.join(geminiUserDir(), 'commands');
371
+ const oxeToml = path.join(gBase, 'oxe.toml');
372
+ if (fs.existsSync(oxeToml)) {
373
+ try {
374
+ if (fs.readFileSync(oxeToml, 'utf8').includes(OXE_MANAGED_TOML)) unlinkQuiet(oxeToml);
375
+ } catch {
376
+ /* ignore */
377
+ }
378
+ }
379
+ const oxeSub = path.join(gBase, 'oxe');
380
+ if (fs.existsSync(oxeSub)) {
381
+ for (const name of fs.readdirSync(oxeSub)) {
382
+ if (!name.endsWith('.toml')) continue;
383
+ const p = path.join(oxeSub, name);
384
+ try {
385
+ if (fs.readFileSync(p, 'utf8').includes(OXE_MANAGED_TOML)) unlinkQuiet(p);
386
+ } catch {
387
+ /* ignore */
388
+ }
389
+ }
390
+ try {
391
+ if (!u.dryRun && fs.existsSync(oxeSub) && fs.readdirSync(oxeSub).length === 0) fs.rmdirSync(oxeSub);
392
+ } catch {
393
+ /* ignore */
394
+ }
395
+ }
396
+
397
+ const wfDir = windsurfGlobalWorkflowsDir();
398
+ if (fs.existsSync(wfDir)) {
399
+ for (const name of fs.readdirSync(wfDir)) {
400
+ if (name !== 'oxe.md' && !(name.startsWith('oxe-') && name.endsWith('.md'))) continue;
401
+ const p = path.join(wfDir, name);
402
+ try {
403
+ if (fs.readFileSync(p, 'utf8').includes(OXE_MANAGED_HTML)) unlinkQuiet(p);
404
+ } catch {
405
+ /* ignore */
406
+ }
407
+ }
408
+ }
409
+
410
+ const cpDir = codexPromptsDir();
411
+ if (fs.existsSync(cpDir)) {
412
+ for (const name of fs.readdirSync(cpDir)) {
413
+ if (!name.startsWith('oxe-') || !name.endsWith('.md')) continue;
414
+ const p = path.join(cpDir, name);
415
+ try {
416
+ if (fs.readFileSync(p, 'utf8').includes(OXE_MANAGED_HTML)) unlinkQuiet(p);
417
+ } catch {
418
+ /* ignore */
419
+ }
420
+ }
421
+ }
422
+
423
+ const agRoot = antigravitySkillsRoot();
424
+ if (fs.existsSync(agRoot)) {
425
+ for (const name of fs.readdirSync(agRoot, { withFileTypes: true })) {
426
+ if (!name.isDirectory()) continue;
427
+ if (!/^oxe($|-)/.test(name.name)) continue;
428
+ rmDirIfOxeSkill(path.join(agRoot, name.name));
429
+ }
430
+ }
431
+
432
+ const cxRoot = codexAgentsSkillsRoot();
433
+ if (fs.existsSync(cxRoot)) {
434
+ for (const name of fs.readdirSync(cxRoot, { withFileTypes: true })) {
435
+ if (!name.isDirectory()) continue;
436
+ if (!/^oxe($|-)/.test(name.name)) continue;
437
+ rmDirIfOxeSkill(path.join(cxRoot, name.name));
438
+ }
439
+ }
440
+ }
441
+
442
+ module.exports = {
443
+ OXE_MANAGED_HTML,
444
+ OXE_MANAGED_TOML,
445
+ adjustWorkflowPathsForNestedLayout,
446
+ parseCursorCommandFrontmatter,
447
+ buildAgentSkillMarkdown,
448
+ installSkillTreeFromCursorCommands,
449
+ installOpenCodeCommands,
450
+ installGeminiTomlCommands,
451
+ installWindsurfGlobalWorkflows,
452
+ installCodexPrompts,
453
+ opencodeCommandDirs,
454
+ windsurfGlobalWorkflowsDir,
455
+ geminiUserDir,
456
+ codexAgentsSkillsRoot,
457
+ codexPromptsDir,
458
+ antigravitySkillsRoot,
459
+ cleanupMarkedUnifiedArtifacts,
460
+ };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const health = require('./oxe-project-health.cjs');
5
+
6
+ /**
7
+ * Aplica o bloco `install` de `.oxe/config.json` a uma cópia das opções de instalação.
8
+ * Flags da CLI devem estar já refletidas em `optsIn` (este módulo não lê argv).
9
+ *
10
+ * @param {string} projectRoot raiz do projeto
11
+ * @param {Record<string, unknown>} optsIn opções parciais (ex.: resultado de parse)
12
+ * @returns {{ options: Record<string, unknown>, warnings: string[] }}
13
+ */
14
+ function resolveInstallOptionsFromConfig(projectRoot, optsIn) {
15
+ /** @type {string[]} */
16
+ const warnings = [];
17
+ const opts = { ...optsIn };
18
+
19
+ if (opts.ignoreInstallConfig) return { options: opts, warnings };
20
+ if (!fs.existsSync(projectRoot)) return { options: opts, warnings };
21
+
22
+ const { config, parseError } = health.loadOxeConfigMerged(projectRoot);
23
+ if (parseError) return { options: opts, warnings };
24
+
25
+ const inst = config.install;
26
+ if (!inst || typeof inst !== 'object' || Array.isArray(inst)) return { options: opts, warnings };
27
+
28
+ const profileSet = new Set(health.INSTALL_PROFILES);
29
+ const layoutSet = new Set(health.INSTALL_REPO_LAYOUTS);
30
+
31
+ if (!opts.explicitScope && !opts.oxeOnly && typeof inst.repo_layout === 'string' && layoutSet.has(inst.repo_layout)) {
32
+ opts.installAssetsGlobal = inst.repo_layout === 'classic';
33
+ opts.explicitScope = true;
34
+ }
35
+
36
+ if (!opts.oxeOnly && inst.vscode === true) {
37
+ opts.vscode = true;
38
+ }
39
+
40
+ if (!opts.integrationsUnset || opts.oxeOnly) return { options: opts, warnings };
41
+ if (inst.profile == null) return { options: opts, warnings };
42
+
43
+ if (typeof inst.profile !== 'string' || !profileSet.has(inst.profile)) {
44
+ const shown = typeof inst.profile === 'string' ? `"${inst.profile}"` : '(tipo inválido)';
45
+ warnings.push(`install.profile ${shown} ignorado — use um de: ${health.INSTALL_PROFILES.join(', ')}`);
46
+ return { options: opts, warnings };
47
+ }
48
+
49
+ const p = inst.profile;
50
+ opts.cursor = false;
51
+ opts.copilot = false;
52
+ opts.copilotCli = false;
53
+ opts.allAgents = false;
54
+
55
+ if (p === 'recommended') {
56
+ opts.cursor = true;
57
+ opts.copilot = true;
58
+ } else if (p === 'cursor') {
59
+ opts.cursor = true;
60
+ } else if (p === 'copilot') {
61
+ opts.copilot = true;
62
+ } else if (p === 'core') {
63
+ opts.commands = false;
64
+ opts.agents = false;
65
+ } else if (p === 'cli') {
66
+ opts.cursor = true;
67
+ opts.copilot = true;
68
+ opts.copilotCli = true;
69
+ } else if (p === 'all_agents') {
70
+ opts.cursor = true;
71
+ opts.copilot = true;
72
+ opts.copilotCli = true;
73
+ opts.allAgents = true;
74
+ }
75
+
76
+ if (p !== 'core') {
77
+ opts.commands = true;
78
+ opts.agents = true;
79
+ }
80
+ if (typeof inst.include_commands_dir === 'boolean') {
81
+ opts.commands = inst.include_commands_dir;
82
+ }
83
+ if (typeof inst.include_agents_md === 'boolean') {
84
+ opts.agents = inst.include_agents_md;
85
+ }
86
+
87
+ opts.integrationsUnset = false;
88
+ return { options: opts, warnings };
89
+ }
90
+
91
+ module.exports = {
92
+ resolveInstallOptionsFromConfig,
93
+ };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ const MANIFEST_DIR = '.oxe-cc';
8
+ const MANIFEST_FILE = 'manifest.json';
9
+ const PATCHES_DIR = 'oxe-local-patches';
10
+
11
+ function manifestPath(home) {
12
+ return path.join(home, MANIFEST_DIR, MANIFEST_FILE);
13
+ }
14
+
15
+ function sha256File(filePath) {
16
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
17
+ }
18
+
19
+ /**
20
+ * @param {string} home
21
+ * @returns {Record<string, string>}
22
+ */
23
+ function loadFileManifest(home) {
24
+ const p = manifestPath(home);
25
+ if (!fs.existsSync(p)) return {};
26
+ try {
27
+ const j = JSON.parse(fs.readFileSync(p, 'utf8'));
28
+ return j && typeof j.files === 'object' ? j.files : {};
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ /**
35
+ * @param {string} home
36
+ * @param {Record<string, string>} files absPath -> sha256
37
+ * @param {string} version
38
+ */
39
+ function writeFileManifest(home, files, version) {
40
+ const dir = path.join(home, MANIFEST_DIR);
41
+ fs.mkdirSync(dir, { recursive: true });
42
+ const payload = {
43
+ version,
44
+ updated_at: new Date().toISOString(),
45
+ files,
46
+ };
47
+ fs.writeFileSync(manifestPath(home), JSON.stringify(payload, null, 2) + '\n', 'utf8');
48
+ }
49
+
50
+ /**
51
+ * Before overwriting with --force, backup files that diverged from last manifest.
52
+ * @param {string} home
53
+ * @param {Record<string, string>} prevManifest
54
+ * @param {{ dryRun: boolean, force: boolean }} opts
55
+ * @param {{ yellow: string, cyan: string, dim: string, reset: string }} colors
56
+ * @returns {string[]} modified paths
57
+ */
58
+ function backupModifiedFromManifest(home, prevManifest, opts, colors) {
59
+ const { yellow, cyan, dim, reset } = colors;
60
+ if (!opts.force || opts.dryRun) return [];
61
+ const modified = [];
62
+ for (const [absPath, oldHash] of Object.entries(prevManifest)) {
63
+ if (!fs.existsSync(absPath)) continue;
64
+ let now;
65
+ try {
66
+ now = sha256File(absPath);
67
+ } catch {
68
+ continue;
69
+ }
70
+ if (now !== oldHash) modified.push(absPath);
71
+ }
72
+ if (modified.length === 0) return [];
73
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
74
+ const patchesRoot = path.join(home, MANIFEST_DIR, PATCHES_DIR, stamp);
75
+ for (const absPath of modified) {
76
+ const rel = path.basename(absPath);
77
+ const dest = path.join(patchesRoot, rel.replace(/[^a-zA-Z0-9._-]+/g, '_'));
78
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
79
+ fs.copyFileSync(absPath, dest);
80
+ }
81
+ const meta = { backed_up_at: new Date().toISOString(), files: modified };
82
+ fs.writeFileSync(path.join(patchesRoot, 'backup-meta.json'), JSON.stringify(meta, null, 2) + '\n', 'utf8');
83
+ console.log(
84
+ ` ${yellow}i${reset} ${modified.length} arquivo(s) OXE alterado(s) localmente — backup em ${cyan}${path.relative(home, patchesRoot)}${reset}`
85
+ );
86
+ for (const f of modified) console.log(` ${dim}${f}${reset}`);
87
+ return modified;
88
+ }
89
+
90
+ /**
91
+ * @param {string} dir
92
+ * @param {(f: string) => boolean} filter
93
+ */
94
+ function collectFilesRecursive(dir, filter) {
95
+ /** @type {string[]} */
96
+ const out = [];
97
+ if (!fs.existsSync(dir)) return out;
98
+ const walk = (d) => {
99
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
100
+ const p = path.join(d, e.name);
101
+ if (e.isDirectory()) walk(p);
102
+ else if (filter(e.name)) out.push(p);
103
+ }
104
+ };
105
+ walk(dir);
106
+ return out;
107
+ }
108
+
109
+ module.exports = {
110
+ loadFileManifest,
111
+ writeFileManifest,
112
+ backupModifiedFromManifest,
113
+ collectFilesRecursive,
114
+ sha256File,
115
+ MANIFEST_DIR,
116
+ PATCHES_DIR,
117
+ };