@fontdata/ai-harness 3.3.1 → 4.0.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +22 -2
- package/dist/cli.js +6 -0
- package/dist/doctor/checks.js +12 -0
- package/dist/update/update.js +3 -1
- package/dist/versao/command.js +424 -0
- package/dist/versao/config.js +325 -0
- package/dist/versao/detectar.js +332 -0
- package/dist/versao/diff.js +58 -0
- package/dist/versao/extratores/comum.js +41 -0
- package/dist/versao/extratores/csproj.js +28 -0
- package/dist/versao/extratores/dproj.js +149 -0
- package/dist/versao/extratores/index.js +26 -0
- package/dist/versao/extratores/json.js +118 -0
- package/dist/versao/extratores/pubspec.js +15 -0
- package/dist/versao/extratores/pyproject.js +26 -0
- package/dist/versao/extratores/regex.js +20 -0
- package/dist/versao/extratores/versoes-txt.js +29 -0
- package/dist/versao/formato.js +67 -0
- package/dist/versao/git.js +91 -0
- package/dist/versao/texto.js +16 -0
- package/dist/versao/tipos.js +1 -0
- package/package.json +1 -1
- package/templates/catalog.json +2 -1
- package/templates/commands/publicar-release.md +93 -0
- package/templates/commands/versao.md +153 -116
- package/templates/skills/evoluir-harness/SKILL.md +3 -2
- package/templates/skills/evoluir-harness/references/versao.md +70 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { decodificar } from './extratores/comum.js';
|
|
2
|
+
function linhas(conteudo) {
|
|
3
|
+
return decodificar(conteudo).texto.split('\n').map((l) => l.replace(/\r$/, ''));
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Diff por linha: cada bloco alterado vira pares antes/depois; linha sem par fica com o outro lado `null`.
|
|
7
|
+
* `linha` é a do arquivo original, ou a do arquivo novo quando a linha só foi acrescentada.
|
|
8
|
+
*/
|
|
9
|
+
export function diffLinhas(original, novo) {
|
|
10
|
+
const a = linhas(original);
|
|
11
|
+
const b = linhas(novo);
|
|
12
|
+
let ini = 0;
|
|
13
|
+
while (ini < a.length && ini < b.length && a[ini] === b[ini])
|
|
14
|
+
ini++;
|
|
15
|
+
let fa = a.length;
|
|
16
|
+
let fb = b.length;
|
|
17
|
+
while (fa > ini && fb > ini && a[fa - 1] === b[fb - 1]) {
|
|
18
|
+
fa--;
|
|
19
|
+
fb--;
|
|
20
|
+
}
|
|
21
|
+
const n = fa - ini;
|
|
22
|
+
const m = fb - ini;
|
|
23
|
+
const lcs = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
24
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
25
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
26
|
+
lcs[i][j] = a[ini + i] === b[ini + j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const saida = [];
|
|
30
|
+
let removidas = [];
|
|
31
|
+
let acrescentadas = [];
|
|
32
|
+
const fechar = () => {
|
|
33
|
+
for (let k = 0; k < Math.max(removidas.length, acrescentadas.length); k++) {
|
|
34
|
+
const r = removidas[k];
|
|
35
|
+
const s = acrescentadas[k];
|
|
36
|
+
saida.push({ linha: (r ?? s) + 1, antes: r === undefined ? null : a[r], depois: s === undefined ? null : b[s] });
|
|
37
|
+
}
|
|
38
|
+
removidas = [];
|
|
39
|
+
acrescentadas = [];
|
|
40
|
+
};
|
|
41
|
+
let i = 0;
|
|
42
|
+
let j = 0;
|
|
43
|
+
while (i < n || j < m) {
|
|
44
|
+
if (i < n && j < m && a[ini + i] === b[ini + j]) {
|
|
45
|
+
fechar();
|
|
46
|
+
i++;
|
|
47
|
+
j++;
|
|
48
|
+
}
|
|
49
|
+
else if (j >= m || (i < n && lcs[i + 1][j] >= lcs[i][j + 1])) {
|
|
50
|
+
removidas.push(ini + i++);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
acrescentadas.push(ini + j++);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
fechar();
|
|
57
|
+
return saida;
|
|
58
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { substituirTrecho } from '../texto.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decodifica como UTF-8 quando o arquivo é UTF-8 válido e como latin1 (1 char = 1 byte) quando não é,
|
|
4
|
+
* para arquivos ANSI de Delphi não corromperem os offsets em bytes.
|
|
5
|
+
*/
|
|
6
|
+
export function decodificar(conteudo) {
|
|
7
|
+
const utf8 = conteudo.toString('utf8');
|
|
8
|
+
if (Buffer.from(utf8, 'utf8').equals(conteudo)) {
|
|
9
|
+
return { texto: utf8, conteudo, bytes: (indice) => Buffer.byteLength(utf8.slice(0, indice), 'utf8') };
|
|
10
|
+
}
|
|
11
|
+
return { texto: conteudo.toString('latin1'), conteudo, bytes: (indice) => indice };
|
|
12
|
+
}
|
|
13
|
+
export function linhaDoByte(conteudo, offset) {
|
|
14
|
+
let linha = 1;
|
|
15
|
+
for (let i = conteudo.indexOf(0x0a); i !== -1 && i < offset; i = conteudo.indexOf(0x0a, i + 1))
|
|
16
|
+
linha++;
|
|
17
|
+
return linha;
|
|
18
|
+
}
|
|
19
|
+
/** Monta a ocorrência a partir de índices de caractere no texto decodificado. */
|
|
20
|
+
export function ocorrencia(arquivo, dec, inicio, fim, grupo) {
|
|
21
|
+
const bInicio = dec.bytes(inicio);
|
|
22
|
+
const bFim = dec.bytes(fim);
|
|
23
|
+
const o = {
|
|
24
|
+
arquivo,
|
|
25
|
+
linha: linhaDoByte(dec.conteudo, bInicio),
|
|
26
|
+
inicio: bInicio,
|
|
27
|
+
fim: bFim,
|
|
28
|
+
valor: dec.conteudo.subarray(bInicio, bFim).toString('utf8'),
|
|
29
|
+
};
|
|
30
|
+
if (grupo !== undefined)
|
|
31
|
+
o.grupo = grupo;
|
|
32
|
+
return o;
|
|
33
|
+
}
|
|
34
|
+
/** Troca o valor de cada ocorrência por `versao`, de trás para frente; lança erro se não houver nenhuma. */
|
|
35
|
+
export function gravarOcorrencias(conteudo, ocorrencias, versao, descricao) {
|
|
36
|
+
if (ocorrencias.length === 0)
|
|
37
|
+
throw new Error(`versão não encontrada: ${descricao}`);
|
|
38
|
+
return [...ocorrencias]
|
|
39
|
+
.sort((a, b) => b.inicio - a.inicio)
|
|
40
|
+
.reduce((buffer, o) => substituirTrecho(buffer, o.inicio, o.fim, versao), conteudo);
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { decodificar, gravarOcorrencias, ocorrencia } from './comum.js';
|
|
2
|
+
const GRUPO = /<PropertyGroup\b[^>]*>([\s\S]*?)<\/PropertyGroup>/dg;
|
|
3
|
+
const TAG = /<(Version|AssemblyVersion|FileVersion)>[ \t]*([^<\s]+)[ \t]*<\/\1>/dg;
|
|
4
|
+
/**
|
|
5
|
+
* `<Version>`, `<AssemblyVersion>` e `<FileVersion>` dos `<PropertyGroup>`; `grupo` é o nome da tag.
|
|
6
|
+
* Valor com `$(...)` referencia outra propriedade e não é tocado.
|
|
7
|
+
*/
|
|
8
|
+
export const extratorCsproj = {
|
|
9
|
+
nome: 'csproj',
|
|
10
|
+
ler(caminho, conteudo) {
|
|
11
|
+
const dec = decodificar(conteudo);
|
|
12
|
+
const texto = dec.texto.replace(/<!--[\s\S]*?-->/g, (c) => ' '.repeat(c.length));
|
|
13
|
+
const ocorrencias = [];
|
|
14
|
+
for (const g of texto.matchAll(GRUPO)) {
|
|
15
|
+
const [inicioGrupo] = g.indices[1];
|
|
16
|
+
for (const t of g[1].matchAll(TAG)) {
|
|
17
|
+
if (t[2].includes('$('))
|
|
18
|
+
continue;
|
|
19
|
+
const [inicio, fim] = t.indices[2];
|
|
20
|
+
ocorrencias.push(ocorrencia(caminho, dec, inicioGrupo + inicio, inicioGrupo + fim, t[1]));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return ocorrencias;
|
|
24
|
+
},
|
|
25
|
+
gravar(caminho, conteudo, versao) {
|
|
26
|
+
return gravarOcorrencias(conteudo, this.ler(caminho, conteudo), versao, `<Version> em ${caminho}`);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { formatarVersao, parseVersao } from '../formato.js';
|
|
2
|
+
import { substituirTrecho } from '../texto.js';
|
|
3
|
+
const NUMERICAS = ['MajorVer', 'MinorVer', 'Release', 'Build'];
|
|
4
|
+
const PADRAO_OMITIDA = { MajorVer: '1', MinorVer: '0', Release: '0', Build: '0' };
|
|
5
|
+
function nomeDoGrupo(atributos) {
|
|
6
|
+
const condicao = /\bCondition="([^"]*)"/.exec(atributos)?.[1];
|
|
7
|
+
if (condicao === undefined)
|
|
8
|
+
return 'PropertyGroup';
|
|
9
|
+
return /^\s*'\$\((\w+)\)'\s*!=\s*''\s*$/.exec(condicao)?.[1] ?? condicao;
|
|
10
|
+
}
|
|
11
|
+
function lerGrupos(texto) {
|
|
12
|
+
const grupos = [];
|
|
13
|
+
const padrao = /<PropertyGroup\b([^>]*?)(?:\/>|>([\s\S]*?)<\/PropertyGroup>)/g;
|
|
14
|
+
for (const m of texto.matchAll(padrao)) {
|
|
15
|
+
const corpo = m[2];
|
|
16
|
+
if (corpo === undefined)
|
|
17
|
+
continue;
|
|
18
|
+
const base = m.index + m[0].length - '</PropertyGroup>'.length - corpo.length;
|
|
19
|
+
const grupo = { nome: nomeDoGrupo(m[1]), chaves: [], fimDasKeys: [], numericas: new Map() };
|
|
20
|
+
for (const k of corpo.matchAll(/<VerInfo_Keys>([^<]*)<\/VerInfo_Keys>/g)) {
|
|
21
|
+
const inicioValor = base + k.index + '<VerInfo_Keys>'.length;
|
|
22
|
+
grupo.fimDasKeys.push(base + k.index + k[0].length);
|
|
23
|
+
for (const c of k[1].matchAll(/(^|;)(FileVersion|ProductVersion)=([^;]*)/g)) {
|
|
24
|
+
const inicio = inicioValor + c.index + c[1].length + c[2].length + 1;
|
|
25
|
+
grupo.chaves.push({ chave: c[2], inicio, fim: inicio + c[3].length, valor: c[3] });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const n of corpo.matchAll(/<VerInfo_(MajorVer|MinorVer|Release|Build)>([^<]*)<\/VerInfo_\1>/g)) {
|
|
29
|
+
const nome = n[1];
|
|
30
|
+
if (grupo.numericas.has(nome))
|
|
31
|
+
continue;
|
|
32
|
+
const inicio = base + n.index + `<VerInfo_${nome}>`.length;
|
|
33
|
+
grupo.numericas.set(nome, {
|
|
34
|
+
inicio,
|
|
35
|
+
fim: inicio + n[2].length,
|
|
36
|
+
valor: n[2],
|
|
37
|
+
fimTag: base + n.index + n[0].length,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
grupos.push(grupo);
|
|
41
|
+
}
|
|
42
|
+
return grupos;
|
|
43
|
+
}
|
|
44
|
+
function lerLegado(texto) {
|
|
45
|
+
const legado = { numericas: new Map(), chaves: [] };
|
|
46
|
+
for (const m of texto.matchAll(/<VersionInfo Name="(MajorVer|MinorVer|Release|Build)">([^<]*)<\/VersionInfo>/g)) {
|
|
47
|
+
const nome = m[1];
|
|
48
|
+
if (legado.numericas.has(nome))
|
|
49
|
+
continue;
|
|
50
|
+
const inicio = m.index + m[0].length - '</VersionInfo>'.length - m[2].length;
|
|
51
|
+
legado.numericas.set(nome, { inicio, fim: inicio + m[2].length, valor: m[2] });
|
|
52
|
+
}
|
|
53
|
+
for (const m of texto.matchAll(/<VersionInfoKeys Name="(FileVersion|ProductVersion)">([^<]*)<\/VersionInfoKeys>/g)) {
|
|
54
|
+
const inicio = m.index + m[0].length - '</VersionInfoKeys>'.length - m[2].length;
|
|
55
|
+
legado.chaves.push({ chave: m[1], inicio, fim: inicio + m[2].length, valor: m[2] });
|
|
56
|
+
}
|
|
57
|
+
return legado;
|
|
58
|
+
}
|
|
59
|
+
function composta(numericas) {
|
|
60
|
+
return NUMERICAS.map((n) => numericas.get(n)?.valor.trim() ?? PADRAO_OMITIDA[n]).join('.');
|
|
61
|
+
}
|
|
62
|
+
function contarLinha(texto, posicao) {
|
|
63
|
+
let linha = 1;
|
|
64
|
+
for (let i = texto.indexOf('\n'); i !== -1 && i < posicao; i = texto.indexOf('\n', i + 1))
|
|
65
|
+
linha++;
|
|
66
|
+
return linha;
|
|
67
|
+
}
|
|
68
|
+
function indentacaoDaLinha(texto, posicao) {
|
|
69
|
+
const inicioLinha = texto.lastIndexOf('\n', posicao - 1) + 1;
|
|
70
|
+
return /^[ \t]*/.exec(texto.slice(inicioLinha, posicao))[0];
|
|
71
|
+
}
|
|
72
|
+
function ler(caminho, conteudo) {
|
|
73
|
+
const texto = conteudo.toString('latin1');
|
|
74
|
+
const brutas = [];
|
|
75
|
+
for (const g of lerGrupos(texto)) {
|
|
76
|
+
for (const c of g.chaves)
|
|
77
|
+
brutas.push({ ...c, grupo: `${g.nome}/VerInfo_Keys:${c.chave}` });
|
|
78
|
+
const valor = composta(g.numericas);
|
|
79
|
+
for (const [nome, t] of g.numericas)
|
|
80
|
+
brutas.push({ ...t, valor, grupo: `${g.nome}/VerInfo_${nome}` });
|
|
81
|
+
}
|
|
82
|
+
const legado = lerLegado(texto);
|
|
83
|
+
const valorLegado = composta(legado.numericas);
|
|
84
|
+
for (const [nome, t] of legado.numericas)
|
|
85
|
+
brutas.push({ ...t, valor: valorLegado, grupo: `VersionInfo:${nome}` });
|
|
86
|
+
for (const c of legado.chaves)
|
|
87
|
+
brutas.push({ ...c, grupo: `VersionInfoKeys:${c.chave}` });
|
|
88
|
+
return brutas
|
|
89
|
+
.sort((a, b) => a.inicio - b.inicio)
|
|
90
|
+
.map((o) => ({
|
|
91
|
+
arquivo: caminho,
|
|
92
|
+
linha: contarLinha(texto, o.inicio),
|
|
93
|
+
inicio: o.inicio,
|
|
94
|
+
fim: o.fim,
|
|
95
|
+
valor: Buffer.from(o.valor, 'latin1').toString('utf8'),
|
|
96
|
+
grupo: o.grupo,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
function gravar(_caminho, conteudo, versao) {
|
|
100
|
+
const segmentos = parseVersao(versao);
|
|
101
|
+
if (segmentos.length !== 4)
|
|
102
|
+
throw new Error(`o .dproj exige versão com 4 segmentos; recebeu "${versao}"`);
|
|
103
|
+
const completa = formatarVersao(segmentos);
|
|
104
|
+
const porNumerica = Object.fromEntries(NUMERICAS.map((n, i) => [n, String(segmentos[i])]));
|
|
105
|
+
const texto = conteudo.toString('latin1');
|
|
106
|
+
const lf = texto.indexOf('\n');
|
|
107
|
+
const eol = lf > 0 && texto[lf - 1] === '\r' ? '\r\n' : '\n';
|
|
108
|
+
const trocas = [];
|
|
109
|
+
const insercoes = new Map();
|
|
110
|
+
const trocar = (t, novo) => {
|
|
111
|
+
if (t.valor !== novo)
|
|
112
|
+
trocas.push({ inicio: t.inicio, fim: t.fim, valor: novo });
|
|
113
|
+
};
|
|
114
|
+
for (const g of lerGrupos(texto)) {
|
|
115
|
+
for (const c of g.chaves)
|
|
116
|
+
trocar(c, completa);
|
|
117
|
+
for (const [nome, t] of g.numericas)
|
|
118
|
+
trocar(t, porNumerica[nome]);
|
|
119
|
+
if (g.fimDasKeys.length === 0)
|
|
120
|
+
continue;
|
|
121
|
+
let ancora = g.fimDasKeys[0];
|
|
122
|
+
for (const nome of NUMERICAS) {
|
|
123
|
+
const existente = g.numericas.get(nome);
|
|
124
|
+
if (existente) {
|
|
125
|
+
ancora = existente.fimTag;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const tag = `${eol}${indentacaoDaLinha(texto, ancora)}<VerInfo_${nome}>${porNumerica[nome]}</VerInfo_${nome}>`;
|
|
129
|
+
insercoes.set(ancora, (insercoes.get(ancora) ?? '') + tag);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const legado = lerLegado(texto);
|
|
133
|
+
for (const [nome, t] of legado.numericas)
|
|
134
|
+
trocar(t, porNumerica[nome]);
|
|
135
|
+
for (const c of legado.chaves)
|
|
136
|
+
trocar(c, completa);
|
|
137
|
+
for (const [posicao, novo] of insercoes)
|
|
138
|
+
trocas.push({ inicio: posicao, fim: posicao, valor: novo });
|
|
139
|
+
return trocas
|
|
140
|
+
.sort((a, b) => b.inicio - a.inicio)
|
|
141
|
+
.reduce((buffer, t) => substituirTrecho(buffer, t.inicio, t.fim, t.valor), Buffer.from(conteudo));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Versão do .dproj nas 3 famílias: `FileVersion`/`ProductVersion` de cada `VerInfo_Keys`, as tags
|
|
145
|
+
* `VerInfo_MajorVer/MinorVer/Release/Build` por PropertyGroup e o bloco legado `<VersionInfo>`/`<VersionInfoKeys>`.
|
|
146
|
+
* Nas tags numéricas, `valor` é a versão composta do grupo (tag omitida vale 0; `MajorVer` omitida vale 1)
|
|
147
|
+
* e `inicio`/`fim` apontam o número da própria tag. `gravar` cria as numéricas omitidas nos grupos com `VerInfo_Keys`.
|
|
148
|
+
*/
|
|
149
|
+
export const extratorDproj = { nome: 'dproj', ler, gravar };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { extratorCsproj } from './csproj.js';
|
|
2
|
+
import { extratorDproj } from './dproj.js';
|
|
3
|
+
import { criarExtratorJson } from './json.js';
|
|
4
|
+
import { extratorPubspec } from './pubspec.js';
|
|
5
|
+
import { extratorPyproject } from './pyproject.js';
|
|
6
|
+
import { criarExtratorRegex } from './regex.js';
|
|
7
|
+
import { extratorVersoesTxt } from './versoes-txt.js';
|
|
8
|
+
/** Devolve o extrator da config; lança erro para regex sem grupo de captura. */
|
|
9
|
+
export function criarExtrator(cfg) {
|
|
10
|
+
switch (cfg.tipo) {
|
|
11
|
+
case 'json':
|
|
12
|
+
return criarExtratorJson(cfg.caminho);
|
|
13
|
+
case 'regex':
|
|
14
|
+
return criarExtratorRegex(cfg.padrao);
|
|
15
|
+
case 'pubspec':
|
|
16
|
+
return extratorPubspec;
|
|
17
|
+
case 'pyproject':
|
|
18
|
+
return extratorPyproject;
|
|
19
|
+
case 'csproj':
|
|
20
|
+
return extratorCsproj;
|
|
21
|
+
case 'versoes-txt':
|
|
22
|
+
return extratorVersoesTxt;
|
|
23
|
+
case 'dproj':
|
|
24
|
+
return extratorDproj;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { decodificar, gravarOcorrencias, ocorrencia } from './comum.js';
|
|
2
|
+
class Varredor {
|
|
3
|
+
texto;
|
|
4
|
+
i = 0;
|
|
5
|
+
constructor(texto) {
|
|
6
|
+
this.texto = texto;
|
|
7
|
+
if (texto.charCodeAt(0) === 0xfeff)
|
|
8
|
+
this.i = 1;
|
|
9
|
+
}
|
|
10
|
+
erro(mensagem) {
|
|
11
|
+
return new Error(`JSON inválido na posição ${this.i}: ${mensagem}`);
|
|
12
|
+
}
|
|
13
|
+
espacos() {
|
|
14
|
+
while (/[ \t\r\n]/.test(this.texto[this.i] ?? ''))
|
|
15
|
+
this.i++;
|
|
16
|
+
}
|
|
17
|
+
esperar(char) {
|
|
18
|
+
this.espacos();
|
|
19
|
+
if (this.texto[this.i] !== char)
|
|
20
|
+
throw this.erro(`esperado "${char}"`);
|
|
21
|
+
this.i++;
|
|
22
|
+
}
|
|
23
|
+
string() {
|
|
24
|
+
this.esperar('"');
|
|
25
|
+
const inicio = this.i;
|
|
26
|
+
while (this.i < this.texto.length && this.texto[this.i] !== '"')
|
|
27
|
+
this.i += this.texto[this.i] === '\\' ? 2 : 1;
|
|
28
|
+
if (this.i >= this.texto.length)
|
|
29
|
+
throw this.erro('string sem fechamento');
|
|
30
|
+
this.i++;
|
|
31
|
+
return { inicio, fim: this.i - 1 };
|
|
32
|
+
}
|
|
33
|
+
/** Percorre o valor atual; quando `chaves` acaba num valor string, devolve o trecho dele (sem aspas). */
|
|
34
|
+
valor(chaves) {
|
|
35
|
+
this.espacos();
|
|
36
|
+
const c = this.texto[this.i];
|
|
37
|
+
if (c === '{')
|
|
38
|
+
return this.objeto(chaves);
|
|
39
|
+
if (c === '[')
|
|
40
|
+
return this.lista(chaves);
|
|
41
|
+
if (c === '"') {
|
|
42
|
+
const trecho = this.string();
|
|
43
|
+
return chaves?.length === 0 ? trecho : null;
|
|
44
|
+
}
|
|
45
|
+
const m = /^(?:true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(this.texto.slice(this.i, this.i + 64));
|
|
46
|
+
if (!m)
|
|
47
|
+
throw this.erro('valor inesperado');
|
|
48
|
+
this.i += m[0].length;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
objeto(chaves) {
|
|
52
|
+
this.i++;
|
|
53
|
+
let achado = null;
|
|
54
|
+
this.espacos();
|
|
55
|
+
if (this.texto[this.i] === '}') {
|
|
56
|
+
this.i++;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
for (;;) {
|
|
60
|
+
const chave = this.string();
|
|
61
|
+
this.esperar(':');
|
|
62
|
+
const nome = JSON.parse(this.texto.slice(chave.inicio - 1, chave.fim + 1));
|
|
63
|
+
const casa = chaves !== null && chaves.length > 0 && nome === chaves[0];
|
|
64
|
+
const resultado = this.valor(casa ? chaves.slice(1) : null);
|
|
65
|
+
if (casa)
|
|
66
|
+
achado = resultado;
|
|
67
|
+
this.espacos();
|
|
68
|
+
if (this.texto[this.i] === '}') {
|
|
69
|
+
this.i++;
|
|
70
|
+
return achado;
|
|
71
|
+
}
|
|
72
|
+
this.esperar(',');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
lista(chaves) {
|
|
76
|
+
this.i++;
|
|
77
|
+
let achado = null;
|
|
78
|
+
this.espacos();
|
|
79
|
+
if (this.texto[this.i] === ']') {
|
|
80
|
+
this.i++;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
for (let indice = 0;; indice++) {
|
|
84
|
+
const casa = chaves !== null && chaves.length > 0 && chaves[0] === String(indice);
|
|
85
|
+
const resultado = this.valor(casa ? chaves.slice(1) : null);
|
|
86
|
+
if (casa)
|
|
87
|
+
achado = resultado;
|
|
88
|
+
this.espacos();
|
|
89
|
+
if (this.texto[this.i] === ']') {
|
|
90
|
+
this.i++;
|
|
91
|
+
return achado;
|
|
92
|
+
}
|
|
93
|
+
this.esperar(',');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function localizar(dec, chaves) {
|
|
98
|
+
const varredor = new Varredor(dec.texto);
|
|
99
|
+
const trecho = varredor.valor(chaves);
|
|
100
|
+
varredor.espacos();
|
|
101
|
+
if (varredor.i < dec.texto.length)
|
|
102
|
+
throw varredor.erro('conteúdo depois do valor raiz');
|
|
103
|
+
return trecho;
|
|
104
|
+
}
|
|
105
|
+
/** Extrator `json:<caminho>`: varre o texto até o valor string do caminho, sem reserializar o arquivo. */
|
|
106
|
+
export function criarExtratorJson(chaves) {
|
|
107
|
+
const descricao = chaves.map((c) => (/^[^.[\]"]+$/.test(c) ? c : `[${JSON.stringify(c)}]`)).join('.').replace(/\.\[/g, '[');
|
|
108
|
+
const ler = (caminho, conteudo) => {
|
|
109
|
+
const dec = decodificar(conteudo);
|
|
110
|
+
const trecho = localizar(dec, chaves);
|
|
111
|
+
return trecho ? [ocorrencia(caminho, dec, trecho.inicio, trecho.fim)] : [];
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
nome: `json:${descricao}`,
|
|
115
|
+
ler,
|
|
116
|
+
gravar: (caminho, conteudo, versao) => gravarOcorrencias(conteudo, ler(caminho, conteudo), JSON.stringify(versao).slice(1, -1), `json:${descricao} em ${caminho}`),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { decodificar, gravarOcorrencias, ocorrencia } from './comum.js';
|
|
2
|
+
const VERSAO = /^version[ \t]*:[ \t]*(?:"([^"\r\n]*)"|'([^'\r\n]*)'|([^\s#"']+))/dgm;
|
|
3
|
+
/** `version:` da raiz do pubspec.yaml; o valor inclui o `+build` (ex.: `1.2.3+45`). */
|
|
4
|
+
export const extratorPubspec = {
|
|
5
|
+
nome: 'pubspec',
|
|
6
|
+
ler(caminho, conteudo) {
|
|
7
|
+
const dec = decodificar(conteudo);
|
|
8
|
+
const m = [...dec.texto.matchAll(VERSAO)][0];
|
|
9
|
+
const indices = m?.indices?.slice(1).find((i) => i !== undefined);
|
|
10
|
+
return indices ? [ocorrencia(caminho, dec, indices[0], indices[1])] : [];
|
|
11
|
+
},
|
|
12
|
+
gravar(caminho, conteudo, versao) {
|
|
13
|
+
return gravarOcorrencias(conteudo, this.ler(caminho, conteudo), versao, `version do pubspec em ${caminho}`);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { decodificar, gravarOcorrencias, ocorrencia } from './comum.js';
|
|
2
|
+
const SECOES = new Set(['project', 'tool.poetry']);
|
|
3
|
+
const LINHA = /^[ \t]*(?:\[[ \t]*([^[\]\r\n]+?)[ \t]*\][ \t]*(?:#.*)?$|\[\[.*$|version[ \t]*=[ \t]*(?:"([^"\r\n]*)"|'([^'\r\n]*)'))/dgm;
|
|
4
|
+
/** `version = "..."` das tabelas `[project]` e `[tool.poetry]`; `grupo` é o nome da tabela. */
|
|
5
|
+
export const extratorPyproject = {
|
|
6
|
+
nome: 'pyproject',
|
|
7
|
+
ler(caminho, conteudo) {
|
|
8
|
+
const dec = decodificar(conteudo);
|
|
9
|
+
const ocorrencias = [];
|
|
10
|
+
let secao = null;
|
|
11
|
+
for (const m of dec.texto.matchAll(LINHA)) {
|
|
12
|
+
if (m[1] !== undefined)
|
|
13
|
+
secao = m[1].replace(/[ \t]/g, '');
|
|
14
|
+
else if (m[2] === undefined && m[3] === undefined)
|
|
15
|
+
secao = null;
|
|
16
|
+
else if (secao !== null && SECOES.has(secao)) {
|
|
17
|
+
const indices = (m.indices?.[2] ?? m.indices?.[3]);
|
|
18
|
+
ocorrencias.push(ocorrencia(caminho, dec, indices[0], indices[1], secao));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return ocorrencias;
|
|
22
|
+
},
|
|
23
|
+
gravar(caminho, conteudo, versao) {
|
|
24
|
+
return gravarOcorrencias(conteudo, this.ler(caminho, conteudo), versao, `version de [project] ou [tool.poetry] em ${caminho}`);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { decodificar, gravarOcorrencias, ocorrencia } from './comum.js';
|
|
2
|
+
/** Extrator `regex`: a versão é o primeiro grupo de captura de cada casamento (flags `g` e `m`). */
|
|
3
|
+
export function criarExtratorRegex(padrao) {
|
|
4
|
+
const regex = new RegExp(padrao, 'dgm');
|
|
5
|
+
if ((new RegExp(`${padrao}|`).exec('')?.length ?? 1) < 2) {
|
|
6
|
+
throw new Error(`o padrão do extrator regex precisa de um grupo de captura com a versão: ${padrao}`);
|
|
7
|
+
}
|
|
8
|
+
const ler = (caminho, conteudo) => {
|
|
9
|
+
const dec = decodificar(conteudo);
|
|
10
|
+
return [...dec.texto.matchAll(regex)].flatMap((m) => {
|
|
11
|
+
const indices = m.indices?.[1];
|
|
12
|
+
return indices ? [ocorrencia(caminho, dec, indices[0], indices[1])] : [];
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
nome: 'regex',
|
|
17
|
+
ler,
|
|
18
|
+
gravar: (caminho, conteudo, versao) => gravarOcorrencias(conteudo, ler(caminho, conteudo), versao, `regex ${padrao} em ${caminho}`),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { decodificar, ocorrencia } from './comum.js';
|
|
2
|
+
const ENTRADA = /^\uFEFF?[ \t]*(?:V(?:ers[aã]o)?[ \t]*)?(\d+(?:\.\d+)+)/dgim;
|
|
3
|
+
function comparar(a, b) {
|
|
4
|
+
const x = a.split('.').map(Number);
|
|
5
|
+
const y = b.split('.').map(Number);
|
|
6
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
7
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
8
|
+
if (d !== 0)
|
|
9
|
+
return d;
|
|
10
|
+
}
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
/** Versoes.txt: `ler` devolve a maior versão que abre uma linha; o bloco novo é escrito pelo changelog do /versao. */
|
|
14
|
+
export const extratorVersoesTxt = {
|
|
15
|
+
nome: 'versoes-txt',
|
|
16
|
+
ler(caminho, conteudo) {
|
|
17
|
+
const dec = decodificar(conteudo);
|
|
18
|
+
let maior;
|
|
19
|
+
for (const m of dec.texto.matchAll(ENTRADA)) {
|
|
20
|
+
if (!maior || comparar(m[1], maior[1]) > 0)
|
|
21
|
+
maior = m;
|
|
22
|
+
}
|
|
23
|
+
const indices = maior?.indices?.[1];
|
|
24
|
+
return indices ? [ocorrencia(caminho, dec, indices[0], indices[1])] : [];
|
|
25
|
+
},
|
|
26
|
+
gravar(caminho) {
|
|
27
|
+
throw new Error(`${caminho}: o extrator versoes-txt só lê; o bloco da versão nova no Versoes.txt é escrito pelo changelog do /versao`);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const INDICE = { major: 0, minor: 1, patch: 2, build: 3 };
|
|
2
|
+
function partesDoFormato(formato) {
|
|
3
|
+
const literais = [];
|
|
4
|
+
const larguras = [];
|
|
5
|
+
const marcador = /\{(\d+)\}/g;
|
|
6
|
+
let ultimo = 0;
|
|
7
|
+
for (const m of formato.matchAll(marcador)) {
|
|
8
|
+
literais.push(formato.slice(ultimo, m.index));
|
|
9
|
+
larguras.push(Number(m[1]));
|
|
10
|
+
ultimo = m.index + m[0].length;
|
|
11
|
+
}
|
|
12
|
+
literais.push(formato.slice(ultimo));
|
|
13
|
+
if (larguras.length === 0)
|
|
14
|
+
throw new Error(`formato de versão sem segmento {n}: "${formato}"`);
|
|
15
|
+
return { literais, larguras };
|
|
16
|
+
}
|
|
17
|
+
function escaparRegex(texto) {
|
|
18
|
+
return texto.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Lê a versão como lista de inteiros. Com `formato` (ex.: `{1}.{3}.{2}.{2}`), o texto precisa
|
|
22
|
+
* seguir os literais e ter um segmento por marcador; sem formato, aceita inteiros separados por ponto.
|
|
23
|
+
*/
|
|
24
|
+
export function parseVersao(texto, formato) {
|
|
25
|
+
if (formato === undefined) {
|
|
26
|
+
if (!/^\d+(\.\d+)*$/.test(texto))
|
|
27
|
+
throw new Error(`versão inválida: "${texto}"`);
|
|
28
|
+
return texto.split('.').map(Number);
|
|
29
|
+
}
|
|
30
|
+
const { literais, larguras } = partesDoFormato(formato);
|
|
31
|
+
const padrao = literais.map(escaparRegex).join('(\\d+)');
|
|
32
|
+
const m = new RegExp(`^${padrao}$`).exec(texto);
|
|
33
|
+
if (!m)
|
|
34
|
+
throw new Error(`versão "${texto}" não segue o formato "${formato}"`);
|
|
35
|
+
return larguras.map((_, i) => Number(m[i + 1]));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Monta o texto da versão. Cada `{n}` do formato é a largura mínima do segmento, completada com
|
|
39
|
+
* zeros à esquerda; sem formato, junta os segmentos por ponto sem padding.
|
|
40
|
+
*/
|
|
41
|
+
export function formatarVersao(segmentos, formato) {
|
|
42
|
+
for (const s of segmentos) {
|
|
43
|
+
if (!Number.isSafeInteger(s) || s < 0)
|
|
44
|
+
throw new Error(`segmento de versão inválido: ${s}`);
|
|
45
|
+
}
|
|
46
|
+
if (formato === undefined)
|
|
47
|
+
return segmentos.join('.');
|
|
48
|
+
const { literais, larguras } = partesDoFormato(formato);
|
|
49
|
+
if (larguras.length !== segmentos.length) {
|
|
50
|
+
throw new Error(`o formato "${formato}" tem ${larguras.length} segmentos e a versão tem ${segmentos.length}`);
|
|
51
|
+
}
|
|
52
|
+
return literais.reduce((acc, literal, i) => i < larguras.length ? acc + literal + String(segmentos[i]).padStart(larguras[i], '0') : acc + literal, '');
|
|
53
|
+
}
|
|
54
|
+
/** Soma 1 no segmento do tipo (major=0 … build=3) e zera os da direita; devolve um array novo. */
|
|
55
|
+
export function incrementar(segmentos, tipo) {
|
|
56
|
+
const indice = INDICE[tipo];
|
|
57
|
+
if (tipo === 'build' && segmentos.length < 4) {
|
|
58
|
+
throw new Error(`o incremento build exige versão com 4 segmentos; a versão tem ${segmentos.length}`);
|
|
59
|
+
}
|
|
60
|
+
const novo = [...segmentos];
|
|
61
|
+
while (novo.length <= indice)
|
|
62
|
+
novo.push(0);
|
|
63
|
+
novo[indice] = novo[indice] + 1;
|
|
64
|
+
for (let i = indice + 1; i < novo.length; i++)
|
|
65
|
+
novo[i] = 0;
|
|
66
|
+
return novo;
|
|
67
|
+
}
|