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.
- package/.cursor/commands/oxe-discuss.md +4 -2
- package/.cursor/commands/oxe-execute.md +6 -2
- package/.cursor/commands/oxe-help.md +10 -2
- package/.cursor/commands/oxe-next.md +10 -2
- package/.cursor/commands/oxe-plan.md +13 -2
- package/.cursor/commands/oxe-quick.md +6 -2
- package/.cursor/commands/oxe-review-pr.md +16 -0
- package/.cursor/commands/oxe-scan.md +13 -2
- package/.cursor/commands/oxe-spec.md +13 -2
- package/.cursor/commands/oxe-verify.md +13 -2
- package/.github/copilot-instructions.md +35 -14
- package/AGENTS.md +4 -2
- package/README.md +310 -244
- package/assets/readme-banner.svg +19 -18
- package/bin/lib/oxe-agent-install.cjs +460 -0
- package/bin/lib/oxe-install-resolve.cjs +93 -0
- package/bin/lib/oxe-manifest.cjs +117 -0
- package/bin/lib/oxe-project-health.cjs +464 -0
- package/bin/lib/oxe-workflows.cjs +145 -0
- package/bin/oxe-cc.js +1406 -123
- package/lib/sdk/README.md +54 -0
- package/lib/sdk/index.cjs +241 -0
- package/lib/sdk/index.d.ts +89 -0
- package/oxe/templates/CONFIG.md +32 -12
- package/oxe/templates/PLAN.template.md +1 -1
- package/oxe/templates/SPEC.template.md +9 -5
- package/oxe/templates/STATE.md +9 -1
- package/oxe/templates/WORKFLOW_AUTHORING.md +73 -0
- package/oxe/templates/config.template.json +18 -6
- package/oxe/workflows/discuss.md +31 -31
- package/oxe/workflows/execute.md +36 -28
- package/oxe/workflows/help.md +56 -22
- package/oxe/workflows/next.md +27 -13
- package/oxe/workflows/plan.md +14 -13
- package/oxe/workflows/quick.md +45 -32
- package/oxe/workflows/review-pr.md +13 -13
- package/oxe/workflows/scan.md +13 -11
- package/oxe/workflows/spec.md +15 -14
- package/oxe/workflows/verify.md +19 -16
- package/oxe/workflows/workflow-authoring.md +34 -0
- package/package.json +30 -3
- 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
|
+
};
|