parche-cli 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +95 -2
- package/bin/parche.js +171 -0
- package/package.json +43 -4
- package/packs/ai.model-retirements.json +44 -0
- package/packs/arca.wsfe.condicion-iva-receptor.json +49 -0
- package/packs/mercadopago.payments-to-orders.json +57 -0
- package/src/ast.js +184 -0
- package/src/codemods/arca-wsfe-condicion-iva.js +138 -0
- package/src/codemods/mercadopago-payments-to-orders.js +476 -0
- package/src/detectors/ai-model-retirements.js +37 -0
- package/src/engine.js +85 -0
- package/src/git.js +49 -0
- package/src/github.js +22 -0
- package/src/ledger.js +16 -0
- package/src/packs.js +39 -0
- package/src/report.js +91 -0
- package/src/verify.js +31 -0
- package/templates/github-workflow.yml +27 -0
package/src/ledger.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Registro append-only de eventos. En el producto real esto se sube (anonimizado) y es lo que
|
|
2
|
+
// hace que la base de packs mejore: qué fixes se mergean, cuáles se editan y cuáles se rechazan.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export function record(root, event) {
|
|
7
|
+
const dir = path.join(root, '.parche');
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
fs.appendFileSync(path.join(dir, 'ledger.jsonl'), JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function read(root) {
|
|
13
|
+
const f = path.join(root, '.parche', 'ledger.jsonl');
|
|
14
|
+
if (!fs.existsSync(f)) return [];
|
|
15
|
+
return fs.readFileSync(f, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
16
|
+
}
|
package/src/packs.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Carga de packs (la base de cambios) y el registro de transformaciones.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import arca from './codemods/arca-wsfe-condicion-iva.js';
|
|
6
|
+
import mercadopago from './codemods/mercadopago-payments-to-orders.js';
|
|
7
|
+
import aiModels from './detectors/ai-model-retirements.js';
|
|
8
|
+
|
|
9
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
export const PACKS_DIR = path.join(HERE, '..', 'packs');
|
|
11
|
+
|
|
12
|
+
const TRANSFORMS = {
|
|
13
|
+
'arca.wsfe.condicion-iva-receptor': arca,
|
|
14
|
+
'mercadopago.payments-to-orders': mercadopago,
|
|
15
|
+
'ai.model-retirements': aiModels,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const REQUIRED_FIELDS = ['id', 'version', 'provider', 'title', 'kind', 'severity', 'deadline', 'last_verified',
|
|
19
|
+
'summary_es', 'why_es', 'sources', 'detect', 'codemod', 'review_checklist_es'];
|
|
20
|
+
|
|
21
|
+
export function validatePack(pack) {
|
|
22
|
+
const missing = REQUIRED_FIELDS.filter((f) => !(f in pack));
|
|
23
|
+
if (missing.length) throw new Error(`Pack ${pack.id || '?'}: faltan campos ${missing.join(', ')}`);
|
|
24
|
+
if (!Array.isArray(pack.sources) || pack.sources.some((s) => !s.url)) throw new Error(`Pack ${pack.id}: sources inválidas`);
|
|
25
|
+
if (!['critical', 'high', 'medium', 'low'].includes(pack.severity)) throw new Error(`Pack ${pack.id}: severity inválida`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadPacks(dir = PACKS_DIR) {
|
|
30
|
+
return fs.readdirSync(dir)
|
|
31
|
+
.filter((f) => f.endsWith('.json'))
|
|
32
|
+
.map((f) => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')))
|
|
33
|
+
.map((p) => (validatePack(p), p))
|
|
34
|
+
.sort((a, b) => (a.deadline || '9999').localeCompare(b.deadline || '9999'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function transformFor(packId) {
|
|
38
|
+
return TRANSFORMS[packId] || null;
|
|
39
|
+
}
|
package/src/report.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Reporte del scan y cuerpo del PR, en castellano.
|
|
2
|
+
const LABEL = {
|
|
3
|
+
auto: '✅ automático',
|
|
4
|
+
'needs-review': '👀 revisar',
|
|
5
|
+
'needs-input': '✋ falta un dato',
|
|
6
|
+
blocked: '⛔ bloqueado',
|
|
7
|
+
'detect-only': '🔎 detectado',
|
|
8
|
+
};
|
|
9
|
+
const SEVERITY = { critical: 'crítica', high: 'alta', medium: 'media', low: 'baja' };
|
|
10
|
+
|
|
11
|
+
export function daysUntil(date, today = new Date()) {
|
|
12
|
+
if (!date) return null;
|
|
13
|
+
const ms = new Date(date + 'T00:00:00-03:00') - today;
|
|
14
|
+
return Math.ceil(ms / 86400000);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function deadlineText(pack, today) {
|
|
18
|
+
if (!pack.deadline) return 'sin fecha límite';
|
|
19
|
+
const d = daysUntil(pack.deadline, today);
|
|
20
|
+
return d >= 0 ? `${pack.deadline} (faltan ${d} días)` : `${pack.deadline} (vencida hace ${-d} días)`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function counts(findings) {
|
|
24
|
+
const c = {};
|
|
25
|
+
for (const f of findings) c[f.status] = (c[f.status] || 0) + 1;
|
|
26
|
+
return c;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function scanReport({ inventory, findings, packs, changed }, today = new Date()) {
|
|
30
|
+
const out = [];
|
|
31
|
+
out.push(`# Parche — reporte de \`${inventory.repo}\``, '');
|
|
32
|
+
out.push('## APIs que usa este repo', '');
|
|
33
|
+
const pk = Object.entries(inventory.packages);
|
|
34
|
+
if (pk.length) out.push(...pk.map(([n, v]) => `- \`${n}\` ${v}`), '');
|
|
35
|
+
for (const [prov, p] of Object.entries(inventory.providers)) {
|
|
36
|
+
out.push(`- **${prov}**: ${p.callSites} usos en ${p.files.length} archivo(s) — ${Object.entries(p.ops).map(([o, n]) => `${o} ×${n}`).join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
out.push('');
|
|
39
|
+
for (const pack of packs) {
|
|
40
|
+
const fs = findings.filter((f) => f.pack === pack.id);
|
|
41
|
+
if (!fs.length) continue;
|
|
42
|
+
out.push(`## ${pack.title}`, '');
|
|
43
|
+
out.push(`**Proveedor:** ${pack.provider} · **Severidad:** ${SEVERITY[pack.severity]} · **Fecha límite:** ${deadlineText(pack, today)}`, '');
|
|
44
|
+
out.push(pack.summary_es, '');
|
|
45
|
+
for (const f of fs) {
|
|
46
|
+
out.push(`- ${LABEL[f.status]} \`${f.file}${f.line ? `:${f.line}` : ''}\` — ${f.message}`);
|
|
47
|
+
if (f.suggestion) out.push(` - Sugerido: \`${f.suggestion}\``);
|
|
48
|
+
}
|
|
49
|
+
out.push('');
|
|
50
|
+
}
|
|
51
|
+
if (changed && changed.length) {
|
|
52
|
+
out.push('## Archivos modificados', '', ...changed.map((c) => `- \`${c.file}\` (${c.packs.join(', ')})`), '');
|
|
53
|
+
}
|
|
54
|
+
return out.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function prBody({ pack, findings, changed, verification }, today = new Date()) {
|
|
58
|
+
const fs = findings.filter((f) => f.pack === pack.id);
|
|
59
|
+
const c = counts(fs);
|
|
60
|
+
const out = [];
|
|
61
|
+
out.push(`## ${pack.title}`, '');
|
|
62
|
+
out.push(`> **${pack.provider}** · severidad ${SEVERITY[pack.severity]} · fecha límite: ${deadlineText(pack, today)}`, '');
|
|
63
|
+
out.push('### Qué cambió', '', pack.why_es, '');
|
|
64
|
+
out.push('### Qué hace este PR', '');
|
|
65
|
+
const applied = fs.filter((f) => f.status === 'auto' || f.status === 'needs-review');
|
|
66
|
+
out.push(changed.length
|
|
67
|
+
? `Modifica ${changed.length} archivo(s): ${changed.map((x) => `\`${x.file}\``).join(', ')}.`
|
|
68
|
+
: 'No modifica archivos: solo reporta lo que hay que resolver a mano.', '');
|
|
69
|
+
for (const f of applied) out.push(`- ${LABEL[f.status]} \`${f.file}:${f.line}\` — ${f.message}`);
|
|
70
|
+
const pending = fs.filter((f) => f.status === 'needs-input' || f.status === 'blocked');
|
|
71
|
+
if (pending.length) {
|
|
72
|
+
out.push('', '### Lo que este PR NO resuelve (necesita una persona)', '');
|
|
73
|
+
for (const f of pending) {
|
|
74
|
+
out.push(`- ${LABEL[f.status]} \`${f.file}:${f.line}\` — ${f.message}`);
|
|
75
|
+
if (f.suggestion) out.push(` - Agregá: \`${f.suggestion}\``);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
out.push('', '### Verificación', '');
|
|
79
|
+
if (verification) {
|
|
80
|
+
out.push(`- Sintaxis de los archivos modificados: ${verification.parse.ok ? '✅ OK' : '❌ ' + verification.parse.errors.join('; ')}`);
|
|
81
|
+
if (verification.tests) out.push(`- Tests del repo (\`npm test\`): ${verification.tests.ok ? '✅ pasan' : verification.tests.skipped ? '— no hay script de test' : '❌ fallan'}`);
|
|
82
|
+
} else {
|
|
83
|
+
out.push('- Sin verificar (corré `parche pr --verify`).');
|
|
84
|
+
}
|
|
85
|
+
out.push('', '### Checklist antes de mergear', '', ...pack.review_checklist_es.map((x) => `- [ ] ${x}`), '');
|
|
86
|
+
out.push('### Fuentes', '', ...pack.sources.map((s) => `- [${s.title}](${s.url})`), '');
|
|
87
|
+
out.push(`<sub>Pack \`${pack.id}\` v${pack.version}, verificado el ${pack.last_verified}. Resumen: ${Object.entries(c).map(([k, v]) => `${LABEL[k]} ${v}`).join(' · ')}. ¿Este PR te sirvió? Mergealo, editalo o cerralo: Parche aprende de cada resultado.</sub>`, '');
|
|
88
|
+
return out.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { LABEL };
|
package/src/verify.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Verificación: ningún PR sale sin que el código modificado parsee y, si se pide, sin que pasen los tests.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { parseCheck } from './ast.js';
|
|
6
|
+
|
|
7
|
+
export function verify(root, changedFiles, { runTests = false } = {}) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
for (const rel of changedFiles) {
|
|
10
|
+
try {
|
|
11
|
+
parseCheck(fs.readFileSync(path.join(root, rel), 'utf8'), rel);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
errors.push(`${rel}: ${e.message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const result = { parse: { ok: errors.length === 0, errors } };
|
|
17
|
+
if (runTests) {
|
|
18
|
+
const pkgPath = path.join(root, 'package.json');
|
|
19
|
+
const pkg = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, 'utf8')) : {};
|
|
20
|
+
if (!pkg.scripts || !pkg.scripts.test) {
|
|
21
|
+
result.tests = { ok: true, skipped: true };
|
|
22
|
+
} else {
|
|
23
|
+
// En Windows npm es npm.cmd y Node solo lo ejecuta a través de un shell.
|
|
24
|
+
const r = spawnSync('npm', ['test', '--silent'], { cwd: root, encoding: 'utf8', timeout: 300000, shell: process.platform === 'win32' });
|
|
25
|
+
const output = (r.stdout || '') + (r.stderr || '') + (r.error ? `\n${r.error.message}` : '');
|
|
26
|
+
result.tests = { ok: r.status === 0, output: output.slice(-2000) };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
result.ok = result.parse.ok && (!result.tests || result.tests.ok);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Copiá este archivo a .github/workflows/parche.yml en tu repo.
|
|
2
|
+
# Cada lunes Parche revisa tu código contra la base de cambios de APIs
|
|
3
|
+
# y abre un PR por cada cambio que te afecta (en borrador si hay algo para revisar).
|
|
4
|
+
name: Parche
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "17 11 * * 1" # lunes 8:17 (hora de Argentina)
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
pull-requests: write
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
parche:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
- uses: actions/setup-node@v7
|
|
23
|
+
with:
|
|
24
|
+
node-version: 24
|
|
25
|
+
- name: Instalar dependencias del proyecto (para correr sus tests antes de cada PR)
|
|
26
|
+
run: npm ci
|
|
27
|
+
- uses: Pepo08/parche@v0
|