@thecodecodex/tools 0.0.0-stage → 0.1.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/src/regex.ts ADDED
@@ -0,0 +1,426 @@
1
+ export const REGEX_FLAGS = ["g", "i", "m", "s", "u"] as const
2
+ export type RegexFlag = (typeof REGEX_FLAGS)[number]
3
+
4
+ export const REGEX_MODES = ["match", "replace", "cheatsheet"] as const
5
+ export type RegexMode = (typeof REGEX_MODES)[number]
6
+
7
+ export interface RegexPreset {
8
+ id: string
9
+ category: "documents" | "dev"
10
+ label: { pt: string, en: string }
11
+ note: { pt: string, en: string }
12
+ pattern: string
13
+ flags: string
14
+ text: string
15
+ replacement: string
16
+ }
17
+
18
+ export const REGEX_PRESETS: RegexPreset[] = [
19
+ {
20
+ id: "iso-date",
21
+ category: "dev",
22
+ label: { pt: "Data ISO", en: "ISO Date" },
23
+ note: { pt: "Captura ano, mês e dia no formato AAAA-MM-DD e converte para DD/MM/AAAA.", en: "Captures year, month, and day in YYYY-MM-DD format and converts to DD/MM/YYYY." },
24
+ pattern: "(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})",
25
+ flags: "g",
26
+ text: "Release 2026-10-21 and LTS support until 2028-04-30.",
27
+ replacement: "$<day>/$<month>/$<year>",
28
+ },
29
+ {
30
+ id: "cpf",
31
+ category: "documents",
32
+ label: { pt: "CPF", en: "CPF (Brazil)" },
33
+ note: { pt: "Aceita com ou sem máscara, isola o 9º dígito (região fiscal) e aplica formatação padrão.", en: "Matches formatted or unformatted CPF, isolates the 9th digit (tax region) and formats." },
34
+ pattern: "\\b(?<b1>\\d{3})\\.?((?<b2>\\d{3}))\\.?((?<b3>\\d{2})(?<regiao>\\d))-?(?<dv>\\d{2})\\b",
35
+ flags: "g",
36
+ text: "CPFs de teste: 529.982.247-25 (RJ/ES) e sem máscara 11144477735.",
37
+ replacement: "$<b1>.$<b2>.$<b3>$<regiao>-$<dv>",
38
+ },
39
+ {
40
+ id: "cnpj",
41
+ category: "documents",
42
+ label: { pt: "CNPJ (Num/Alfa)", en: "CNPJ (Num/Alpha)" },
43
+ note: { pt: "Compatível com o CNPJ numérico tradicional e o novo CNPJ alfanumérico (IN RFB 2.229/2024).", en: "Matches both classic numeric CNPJ and the new alphanumeric CNPJ (IN RFB 2.229/2024)." },
44
+ pattern: "\\b(?<r1>[0-9A-Z]{2})\\.?(?<r2>[0-9A-Z]{3})\\.?(?<r3>[0-9A-Z]{3})\\/?(?<filial>[0-9A-Z]{4})-?(?<dv>\\d{2})\\b",
45
+ flags: "g",
46
+ text: "Matriz numérica 11.222.333/0001-81 e novo alfanumérico 12.ABC.345/01DE-35 (ou 12ABC34501DE35).",
47
+ replacement: "$<r1>.$<r2>.$<r3>/$<filial>-$<dv>",
48
+ },
49
+ {
50
+ id: "nfe",
51
+ category: "documents",
52
+ label: { pt: "Chave NF-e (44 díg.)", en: "NF-e Access Key (44d)" },
53
+ note: { pt: "Decompõe os 44 dígitos da NF-e (55), CT-e (57) ou NFC-e (65): UF, AAMM, CNPJ, modelo, série, número, emissão, código e DV.", en: "Parses all 44 characters of Brazilian NF-e/NFC-e/CT-e keys into UF, YYMM, CNPJ, model, series, number, and check digit." },
54
+ pattern: "\\b(?<uf>\\d{2})(?<aamm>\\d{4})(?<cnpj>[0-9A-Z]{14})(?<mod>55|57|65)(?<serie>\\d{3})(?<nnf>\\d{9})(?<tpEmis>[1-9])(?<cnf>\\d{8})(?<dv>\\d)\\b",
55
+ flags: "g",
56
+ text: "NF-e SP: 35261011222333000181550010000123451087654320 e NFC-e RS: 43261012ABC34501DE35650010000009871123456789.",
57
+ replacement: "UF:$<uf> AAMM:$<aamm> CNPJ:$<cnpj> Mod:$<mod> Nº:$<nnf>-$<dv>",
58
+ },
59
+ {
60
+ id: "cep",
61
+ category: "documents",
62
+ label: { pt: "CEP", en: "Postal Code (CEP)" },
63
+ note: { pt: "Reconhece CEP de 8 dígitos com ou sem hífen e aplica a máscara 00000-000.", en: "Matches 8-digit Brazilian postal codes with or without hyphen and formats as 00000-000." },
64
+ pattern: "\\b(?<setor>\\d{5})-?(?<sufixo>\\d{3})\\b",
65
+ flags: "g",
66
+ text: "Endereços: 01310-100 (São Paulo) e sem hífen 90010150 (Porto Alegre).",
67
+ replacement: "$<setor>-$<sufixo>",
68
+ },
69
+ {
70
+ id: "phone-br",
71
+ category: "documents",
72
+ label: { pt: "Telefone / Celular BR", en: "Phone BR (+55)" },
73
+ note: { pt: "Extrai DDD e número fixo (8 dígitos) ou móvel (9 dígitos), com DDI +55 opcional.", en: "Extracts area code (DDD) and 8- or 9-digit Brazilian phone numbers with optional +55." },
74
+ pattern: "(?:\\+?55\\s?)?\\(?(?<ddd>[1-9]{2})\\)?[\\s-]?(?<prefixo>9?\\d{4})-?(?<final>\\d{4})\\b",
75
+ flags: "g",
76
+ text: "Contatos: +55 (11) 98765-4321, (51) 3210-9988 e direto 21999887766.",
77
+ replacement: "($<ddd>) $<prefixo>-$<final>",
78
+ },
79
+ {
80
+ id: "placa-mercosul",
81
+ category: "documents",
82
+ label: { pt: "Placa Mercosul/Antiga", en: "Vehicle Plate (BR)" },
83
+ note: { pt: "Valida placas brasileiras tanto no padrão Mercosul (ABC1D23) quanto no antigo (ABC-1234).", en: "Matches both Mercosul (ABC1D23) and legacy (ABC-1234) Brazilian license plates." },
84
+ pattern: "\\b(?<letras>[A-Z]{3})-?(?<n1>\\d)(?<meio>[A-Z0-9])(?<n2>\\d{2})\\b",
85
+ flags: "g",
86
+ text: "Frota: BRA2E19 (Mercosul) e ABC-1234 (padrão cinza antigo).",
87
+ replacement: "$<letras>-$<n1>$<meio>$<n2>",
88
+ },
89
+ {
90
+ id: "email",
91
+ category: "dev",
92
+ label: { pt: "E-mail", en: "Email" },
93
+ note: { pt: "Separa usuário (incluindo alias +tag) e domínio.", en: "Extracts local username (including +tag alias) and domain." },
94
+ pattern: "(?<user>[a-zA-Z0-9._%+-]+)@(?<domain>[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})",
95
+ flags: "gi",
96
+ text: "Reach out to dev@thecodecodex.com or financeiro+nfe@empresa.com.br.",
97
+ replacement: "$<user> [at] $<domain>",
98
+ },
99
+ {
100
+ id: "uuid",
101
+ category: "dev",
102
+ label: { pt: "UUID / PIX EVP", en: "UUID / PIX Key" },
103
+ note: { pt: "Identifica UUIDs (v1 a v8, incluindo v4 usado em chaves PIX aleatórias e v7 ordenável por tempo).", en: "Matches UUIDs (v1–v8, including v4 random PIX keys and time-ordered v7)." },
104
+ pattern: "\\b(?<low>[0-9a-f]{8})-(?<mid>[0-9a-f]{4})-(?<ver>[1-8])(?<hi>[0-9a-f]{3})-(?<var>[89ab][0-9a-f]{3})-(?<node>[0-9a-f]{12})\\b",
105
+ flags: "gi",
106
+ text: "PIX EVP (v4): 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d e UUID v7: 018f4a7c-8d10-7b2a-9f11-3c4d5e6f7a8b.",
107
+ replacement: "v$<ver>:$<low>…$<node>",
108
+ },
109
+ {
110
+ id: "jwt",
111
+ category: "dev",
112
+ label: { pt: "JWT Token", en: "JWT Token" },
113
+ note: { pt: "Separa header, payload (ambos JSON em Base64URL iniciando com eyJ) e assinatura.", en: "Splits Base64URL-encoded JSON header, payload (starting with eyJ), and signature." },
114
+ pattern: "\\b(?<header>eyJ[A-Za-z0-9_-]+)\\.(?<payload>eyJ[A-Za-z0-9_-]+)\\.(?<signature>[A-Za-z0-9_-]+)\\b",
115
+ flags: "g",
116
+ text: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
117
+ replacement: "$<header>.$<payload>.[REDACTED_SIG]",
118
+ },
119
+ {
120
+ id: "semver",
121
+ category: "dev",
122
+ label: { pt: "SemVer", en: "SemVer" },
123
+ note: { pt: "Extrai major, minor, patch e sufixo de pré-lançamento opcional.", en: "Extracts major, minor, patch, and optional pre-release tag." },
124
+ pattern: "\\bv?(?<major>0|[1-9]\\d*)\\.(?<minor>0|[1-9]\\d*)\\.(?<patch>0|[1-9]\\d*)(?:-(?<pre>[0-9A-Za-z.-]+))?\\b",
125
+ flags: "g",
126
+ text: "Upgrading runtime from v1.14.2-beta.1 to v2.0.0 (legacy 0.9.4).",
127
+ replacement: "v$<major>.$<minor>.$<patch>",
128
+ },
129
+ {
130
+ id: "ipv4",
131
+ category: "dev",
132
+ label: { pt: "IPv4", en: "IPv4" },
133
+ note: { pt: "Valida cada octeto entre 0 e 255 (rejeita números como 999.1.1.1).", en: "Validates each octet within 0–255 (rejects out-of-range values like 999.1.1.1)." },
134
+ pattern: "\\b(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\b",
135
+ flags: "g",
136
+ text: "Nodes: 10.0.0.1, 192.168.1.254, loopback 127.0.0.1 (invalid: 999.1.1.1).",
137
+ replacement: "[REDACTED_IP]",
138
+ },
139
+ {
140
+ id: "log-line",
141
+ category: "dev",
142
+ label: { pt: "Linha de log", en: "Log line" },
143
+ note: { pt: "Extrai severidade, horário e mensagem linha a linha com a flag m.", en: "Extracts severity level, timestamp, and message per line using the m flag." },
144
+ pattern: "^\\[(?<level>INFO|WARN|ERROR)\\]\\s+(?<time>\\d{2}:\\d{2}:\\d{2})\\s+(?<msg>.+)$",
145
+ flags: "gm",
146
+ text: "[INFO] 14:02:10 Server listening on :8080\n[WARN] 14:02:12 High memory usage (84%)\n[ERROR] 14:02:15 Upstream timeout after 5000ms",
147
+ replacement: "$<time> ($<level>) -> $<msg>",
148
+ },
149
+ {
150
+ id: "url",
151
+ category: "dev",
152
+ label: { pt: "URL HTTP/HTTPS", en: "HTTP/HTTPS URL" },
153
+ note: { pt: "Decompõe protocolo, host, caminho e query string.", en: "Splits scheme, host, path, and query string." },
154
+ pattern: "https?:\\/\\/(?<host>[\\w.-]+)(?<path>\\/[^\\s?#]*)?(?:\\?(?<query>[^\\s#]*))?",
155
+ flags: "gi",
156
+ text: "Visit https://thecodecodex.com/pt/tools?cat=dev or http://localhost:4321/en.",
157
+ replacement: "$<host>$<path>",
158
+ },
159
+ ]
160
+
161
+ export interface RegexMatchItem {
162
+ index: number
163
+ length: number
164
+ match: string
165
+ line: number
166
+ column: number
167
+ captures: (string | null)[]
168
+ groups: Record<string, string>
169
+ }
170
+
171
+ export interface TextSegment {
172
+ text: string
173
+ matchIndex: number | null
174
+ }
175
+
176
+ export interface TokenExplanation {
177
+ token: string
178
+ description: string
179
+ }
180
+
181
+ export interface RegexEvaluation {
182
+ ok: boolean
183
+ error?: string
184
+ pattern: string
185
+ flags: string
186
+ matches: RegexMatchItem[]
187
+ segments: TextSegment[]
188
+ replaced: string
189
+ tokens: TokenExplanation[]
190
+ }
191
+
192
+ export function normalizeFlags(raw: string): string {
193
+ const seen = new Set<RegexFlag>()
194
+ for (const ch of raw) {
195
+ if ((REGEX_FLAGS as readonly string[]).includes(ch)) {
196
+ seen.add(ch as RegexFlag)
197
+ }
198
+ }
199
+ return REGEX_FLAGS.filter((f) => seen.has(f)).join("")
200
+ }
201
+
202
+ function lineAndColumn(text: string, offset: number): { line: number, column: number } {
203
+ let line = 1
204
+ let lastNewline = -1
205
+ for (let i = 0; i < offset && i < text.length; i++) {
206
+ if (text.charCodeAt(i) === 10) {
207
+ line++
208
+ lastNewline = i
209
+ }
210
+ }
211
+ return { line, column: offset - lastNewline }
212
+ }
213
+
214
+ const TOKEN_RULES: { regex: RegExp, pt: string, en: string }[] = [
215
+ { regex: /\(\?<([a-zA-Z0-9_]+)>/g, pt: "Grupo de captura nomeado (?<nome>…)", en: "Named capture group (?<name>…)" },
216
+ { regex: /\(\?:/g, pt: "Grupo sem captura (?:…)", en: "Non-capturing group (?:…)" },
217
+ { regex: /\(\?=/g, pt: "Lookahead positivo (?=…)", en: "Positive lookahead (?=…)" },
218
+ { regex: /\(\?!/g, pt: "Lookahead negativo (?!…)", en: "Negative lookahead (?!…)" },
219
+ { regex: /\(\?<=/g, pt: "Lookbehind positivo (?<=…)", en: "Positive lookbehind (?<=…)" },
220
+ { regex: /\(\?<!/g, pt: "Lookbehind negativo (?<!…)", en: "Negative lookbehind (?<!…)" },
221
+ { regex: /\\d/g, pt: "Dígito numérico [0-9]", en: "Digit character [0-9]" },
222
+ { regex: /\\D/g, pt: "Qualquer caractere não numérico [^0-9]", en: "Non-digit character [^0-9]" },
223
+ { regex: /\\w/g, pt: "Caractere de palavra [A-Za-z0-9_]", en: "Word character [A-Za-z0-9_]" },
224
+ { regex: /\\W/g, pt: "Caractere que não é de palavra", en: "Non-word character" },
225
+ { regex: /\\s/g, pt: "Espaço em branco (espaço, tab, quebra de linha)", en: "Whitespace (space, tab, newline)" },
226
+ { regex: /\\S/g, pt: "Caractere que não é espaço em branco", en: "Non-whitespace character" },
227
+ { regex: /\\b/g, pt: "Fronteira de palavra (início ou fim de palavra)", en: "Word boundary" },
228
+ { regex: /\^/g, pt: "Início da string (ou da linha com flag m)", en: "Start of string (or line with m flag)" },
229
+ { regex: /\$/g, pt: "Fim da string (ou da linha com flag m)", en: "End of string (or line with m flag)" },
230
+ { regex: /\{\d+(?:,\d*)?\}/g, pt: "Quantificador de repetição exata ou intervalo {n,m}", en: "Exact or range repetition quantifier {n,m}" },
231
+ { regex: /\[\^[^\]]+\]/g, pt: "Classe de caracteres negada [^…]", en: "Negated character class [^…]" },
232
+ { regex: /\[[^\^\]][^\]]*\]/g, pt: "Classe de caracteres […]", en: "Character class […]" },
233
+ { regex: /\+/g, pt: "Quantificador: 1 ou mais vezes (+)", en: "Quantifier: 1 or more times (+)" },
234
+ { regex: /\*/g, pt: "Quantificador: 0 ou mais vezes (*)", en: "Quantifier: 0 or more times (*)" },
235
+ { regex: /\?/g, pt: "Opcional (0 ou 1 vez) ou quantificador preguiçoso", en: "Optional (0 or 1 time) or lazy quantifier" },
236
+ { regex: /\|/g, pt: "Alternância lógica (OU)", en: "Alternation (OR)" },
237
+ ]
238
+
239
+ export function explainPattern(pattern: string, lang: "pt" | "en" = "pt"): TokenExplanation[] {
240
+ if (!pattern) return []
241
+ const found: TokenExplanation[] = []
242
+ const seen = new Set<string>()
243
+ for (const rule of TOKEN_RULES) {
244
+ const matches = pattern.match(rule.regex)
245
+ if (matches) {
246
+ for (const m of matches) {
247
+ if (!seen.has(m)) {
248
+ seen.add(m)
249
+ found.push({ token: m, description: rule[lang] })
250
+ }
251
+ }
252
+ }
253
+ }
254
+ return found
255
+ }
256
+
257
+ export function evaluateRegex(pattern: string, rawFlags: string, text: string, replacement = "", lang: "pt" | "en" = "pt"): RegexEvaluation {
258
+ const flags = normalizeFlags(rawFlags)
259
+ const tokens = explainPattern(pattern, lang)
260
+
261
+ if (!pattern) {
262
+ return {
263
+ ok: true,
264
+ pattern,
265
+ flags,
266
+ matches: [],
267
+ segments: text ? [{ text, matchIndex: null }] : [],
268
+ replaced: text,
269
+ tokens,
270
+ }
271
+ }
272
+
273
+ let regex: RegExp
274
+ try {
275
+ regex = new RegExp(pattern, flags)
276
+ } catch (err) {
277
+ return {
278
+ ok: false,
279
+ error: err instanceof Error ? err.message : String(err),
280
+ pattern,
281
+ flags,
282
+ matches: [],
283
+ segments: text ? [{ text, matchIndex: null }] : [],
284
+ replaced: text,
285
+ tokens,
286
+ }
287
+ }
288
+
289
+ const matches: RegexMatchItem[] = []
290
+ const MAX_MATCHES = 1000
291
+
292
+ if (flags.includes("g")) {
293
+ let execResult: RegExpExecArray | null
294
+ while ((execResult = regex.exec(text)) !== null && matches.length < MAX_MATCHES) {
295
+ const idx = execResult.index
296
+ const val = execResult[0] ?? ""
297
+ const pos = lineAndColumn(text, idx)
298
+ const groups: Record<string, string> = {}
299
+ if (execResult.groups) {
300
+ for (const [k, v] of Object.entries(execResult.groups)) {
301
+ if (v !== undefined) groups[k] = v
302
+ }
303
+ }
304
+ matches.push({
305
+ index: idx,
306
+ length: val.length,
307
+ match: val,
308
+ line: pos.line,
309
+ column: pos.column,
310
+ captures: execResult.slice(1).map((c) => c ?? null),
311
+ groups,
312
+ })
313
+ if (val.length === 0) {
314
+ regex.lastIndex++
315
+ if (regex.lastIndex > text.length) break
316
+ }
317
+ }
318
+ } else {
319
+ const execResult = regex.exec(text)
320
+ if (execResult) {
321
+ const idx = execResult.index
322
+ const val = execResult[0] ?? ""
323
+ const pos = lineAndColumn(text, idx)
324
+ const groups: Record<string, string> = {}
325
+ if (execResult.groups) {
326
+ for (const [k, v] of Object.entries(execResult.groups)) {
327
+ if (v !== undefined) groups[k] = v
328
+ }
329
+ }
330
+ matches.push({
331
+ index: idx,
332
+ length: val.length,
333
+ match: val,
334
+ line: pos.line,
335
+ column: pos.column,
336
+ captures: execResult.slice(1).map((c) => c ?? null),
337
+ groups,
338
+ })
339
+ }
340
+ }
341
+
342
+ const segments: TextSegment[] = []
343
+ let cursor = 0
344
+ matches.forEach((m, idx) => {
345
+ if (m.index > cursor) {
346
+ segments.push({ text: text.slice(cursor, m.index), matchIndex: null })
347
+ }
348
+ if (m.length > 0) {
349
+ segments.push({ text: text.slice(m.index, m.index + m.length), matchIndex: idx })
350
+ cursor = m.index + m.length
351
+ }
352
+ })
353
+ if (cursor < text.length) {
354
+ segments.push({ text: text.slice(cursor), matchIndex: null })
355
+ }
356
+
357
+ let replaced = text
358
+ try {
359
+ replaced = text.replace(new RegExp(pattern, flags), replacement)
360
+ } catch {
361
+ replaced = text
362
+ }
363
+
364
+ return {
365
+ ok: true,
366
+ pattern,
367
+ flags,
368
+ matches,
369
+ segments,
370
+ replaced,
371
+ tokens,
372
+ }
373
+ }
374
+
375
+ export interface CheatsheetCategory {
376
+ id: string
377
+ title: { pt: string, en: string }
378
+ items: { syntax: string, pt: string, en: string }[]
379
+ }
380
+
381
+ export const REGEX_CHEATSHEET: CheatsheetCategory[] = [
382
+ {
383
+ id: "classes",
384
+ title: { pt: "Classes de Caracteres", en: "Character Classes" },
385
+ items: [
386
+ { syntax: ".", pt: "Qualquer caractere exceto quebra de linha", en: "Any character except newline" },
387
+ { syntax: "\\d / \\D", pt: "Dígito [0-9] / Não dígito [^0-9]", en: "Digit [0-9] / Non-digit [^0-9]" },
388
+ { syntax: "\\w / \\W", pt: "Alfanumérico [A-Za-z0-9_] / Não palavra", en: "Word char [A-Za-z0-9_] / Non-word" },
389
+ { syntax: "\\s / \\S", pt: "Espaço em branco / Não espaço", en: "Whitespace / Non-whitespace" },
390
+ { syntax: "[abc] / [^abc]", pt: "Qualquer um da lista / Nenhum da lista", en: "Any of a, b, c / Not a, b, c" },
391
+ { syntax: "[a-z0-9]", pt: "Intervalo de caracteres", en: "Character range" },
392
+ ],
393
+ },
394
+ {
395
+ id: "quantifiers",
396
+ title: { pt: "Quantificadores", en: "Quantifiers" },
397
+ items: [
398
+ { syntax: "*", pt: "0 ou mais repetições (guloso)", en: "0 or more repetitions (greedy)" },
399
+ { syntax: "+", pt: "1 ou mais repetições", en: "1 or more repetitions" },
400
+ { syntax: "?", pt: "0 ou 1 repetição (opcional)", en: "0 or 1 repetition (optional)" },
401
+ { syntax: "{n} / {n,m}", pt: "Exatamente n / Entre n e m vezes", en: "Exactly n / Between n and m times" },
402
+ { syntax: "*? / +?", pt: "Quantificador preguiçoso (menor match)", en: "Lazy quantifier (shortest match)" },
403
+ ],
404
+ },
405
+ {
406
+ id: "anchors",
407
+ title: { pt: "Âncoras e Fronteiras", en: "Anchors & Boundaries" },
408
+ items: [
409
+ { syntax: "^", pt: "Início do texto (ou da linha com flag m)", en: "Start of input (or line with m)" },
410
+ { syntax: "$", pt: "Fim do texto (ou da linha com flag m)", en: "End of input (or line with m)" },
411
+ { syntax: "\\b / \\B", pt: "Fronteira de palavra / Fora de fronteira", en: "Word boundary / Non-word boundary" },
412
+ ],
413
+ },
414
+ {
415
+ id: "groups",
416
+ title: { pt: "Grupos, Lookaround e Substituição", en: "Groups, Lookaround & Replacement" },
417
+ items: [
418
+ { syntax: "(abc)", pt: "Grupo de captura numerado ($1, $2…)", en: "Numbered capture group ($1, $2…)" },
419
+ { syntax: "(?<name>abc)", pt: "Grupo nomeado (use $<name> no replace)", en: "Named capture group ($<name> in replace)" },
420
+ { syntax: "(?:abc)", pt: "Grupo de agrupamento sem captura", en: "Non-capturing group" },
421
+ { syntax: "a(?=b) / a(?!b)", pt: "Lookahead: 'a' seguido / não seguido de 'b'", en: "Lookahead: 'a' followed / not followed by 'b'" },
422
+ { syntax: "(?<=a)b / (?<!a)b", pt: "Lookbehind: 'b' precedido / não precedido de 'a'", en: "Lookbehind: 'b' preceded / not preceded by 'a'" },
423
+ { syntax: "$& / $1 / $<name>", pt: "No replace: match inteiro / grupo 1 / grupo nomeado", en: "In replace: full match / group 1 / named group" },
424
+ ],
425
+ },
426
+ ]
@@ -0,0 +1,60 @@
1
+ {
2
+ "skills": [
3
+ {
4
+ "name": "conventional-commits",
5
+ "description": "Writes git commit messages and PR titles following Conventional Commits (type(scope) subject, body explaining why, BREAKING CHANGE footers), split by intent. Use when the user asks to commit, write a commit message, squash, name a pull request, or prepare a changelog from commits.",
6
+ "descriptionPt": "Escreve mensagens de commit e títulos de PR no padrão Conventional Commits (tipo(escopo), corpo explicando o porquê, BREAKING CHANGE), separando commits por intenção.",
7
+ "license": "MIT",
8
+ "version": "1.0",
9
+ "files": [
10
+ {
11
+ "path": "SKILL.md",
12
+ "content": "---\nname: conventional-commits\ndescription: Writes git commit messages and PR titles following Conventional Commits (type(scope) subject, body explaining why, BREAKING CHANGE footers), split by intent. Use when the user asks to commit, write a commit message, squash, name a pull request, or prepare a changelog from commits.\nlicense: MIT\nmetadata:\n author: thecodecodex.com\n version: \"1.0\"\n description_pt: Escreve mensagens de commit e títulos de PR no padrão Conventional Commits (tipo(escopo), corpo explicando o porquê, BREAKING CHANGE), separando commits por intenção.\n---\n\n# Conventional Commits\n\n## Format\n\n```text\n<type>(<scope>): <subject>\n\n<body: what changed and why, wrapped at ~72 columns>\n\n<footers>\n```\n\n- **type** — one of:\n\n | Type | Use for |\n |---|---|\n | `feat` | A new capability for users |\n | `fix` | A bug fix |\n | `docs` | Documentation or content only |\n | `refactor` | Code change with no behavior change |\n | `perf` | Performance improvement |\n | `test` | Adding or fixing tests only |\n | `build` | Build system, dependencies, packaging |\n | `ci` | CI configuration |\n | `chore` | Maintenance that fits nothing above |\n | `revert` | Reverting an earlier commit |\n\n- **scope** — optional, a short noun for the area touched (`tools`, `cli`, `auth`, `content`). Reuse the scopes already present in `git log` instead of inventing new ones.\n- **subject** — imperative mood (\"add\", not \"added\"/\"adds\"), no trailing period, at most ~60 characters, lowercase start unless it is a proper noun.\n- **body** — explain *why* and anything a reviewer could not see from the diff. Skip it for trivial changes.\n- **footers** — `BREAKING CHANGE: <what breaks and how to migrate>` (or `!` after the type: `feat(api)!:`), `Refs: #123`, `Co-authored-by:` lines.\n\n## Workflow\n\n1. Look at the staged diff (`git diff --cached`); if nothing is staged, look at `git status` and ask what should go in.\n2. Check `git log --oneline -20` to match the project's language, scopes and casing. If the project writes messages in another language, keep the type and scope in English and follow the project for the rest.\n3. If the diff mixes unrelated intents (a feature plus an unrelated fix plus formatting), propose splitting it into separate commits and list which files go in each.\n4. Write the message. Pick the type by the user-visible effect, not by which files changed: a bug fixed by refactoring is `fix`.\n5. Only run `git commit` if the user asked you to commit; otherwise return the message.\n\n## Examples\n\n```text\nfeat(cli): install skills into .claude/skills\n\n`skills name=<skill> install=1` writes every file of the skill to disk, so\nagents can pick it up without a manual download.\n```\n\n```text\nfix(invoice): round totals per line before summing\n\nRounding only the final total produced a one-cent difference from the\nPDF rendered by the bank.\n```\n\n```text\nfeat(api)!: drop the v1 endpoints\n\nBREAKING CHANGE: /v1/* now returns 410. Clients must use /v2, which has\nbeen available since March.\n```\n\n## Avoid\n\n- Vague subjects: `fix: bug`, `chore: updates`, `wip`.\n- Listing files in the subject instead of the intent.\n- A `feat` that is really internal (`refactor`) — it pollutes the changelog.\n"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "name": "kubernetes-manifest-review",
18
+ "description": "Reviews Kubernetes YAML manifests and Helm output for production readiness — probes, resource requests and limits, rollout strategy, security context, labels and selectors, and Secret handling. Use when the user shares Deployments, StatefulSets, Services, HPAs or Helm charts, or asks why pods are Pending, crash looping or OOMKilled.",
19
+ "descriptionPt": "Revisa manifestos Kubernetes e saídas do Helm para produção — probes, requests e limits, estratégia de rollout, security context, labels e selectors e uso de Secrets.",
20
+ "license": "MIT",
21
+ "version": "1.0",
22
+ "files": [
23
+ {
24
+ "path": "SKILL.md",
25
+ "content": "---\nname: kubernetes-manifest-review\ndescription: Reviews Kubernetes YAML manifests and Helm output for production readiness — probes, resource requests and limits, rollout strategy, security context, labels and selectors, and Secret handling. Use when the user shares Deployments, StatefulSets, Services, HPAs or Helm charts, or asks why pods are Pending, crash looping or OOMKilled.\nlicense: MIT\nmetadata:\n author: thecodecodex.com\n version: \"1.0\"\n description_pt: Revisa manifestos Kubernetes e saídas do Helm para produção — probes, requests e limits, estratégia de rollout, security context, labels e selectors e uso de Secrets.\n---\n\n# Kubernetes manifest review\n\n## Workflow\n\n1. Identify each object (`kind`, `metadata.name`, namespace). If the user shared a Helm chart, review the rendered output (`helm template`) when possible, not just `values.yaml`.\n2. Go through the checks below for every workload (Deployment, StatefulSet, DaemonSet, Job).\n3. If the user reported a symptom, start from the symptom table and connect it to the manifest.\n4. Answer with a verdict, findings ordered by severity, and a corrected YAML snippet for each **high** finding.\n\n## Checks\n\n**Selectors and labels**\n- `spec.selector.matchLabels` matches `template.metadata.labels`, and every Service `selector` matches the pods it should reach. A mismatch means a Service with no endpoints.\n\n**Probes**\n- `readinessProbe` present on anything behind a Service.\n- `livenessProbe` checks only the process itself. A liveness probe that calls the database or another service is a **high** finding: when the dependency fails, every pod restarts in a loop.\n- Slow-starting apps use a `startupProbe` instead of a huge `initialDelaySeconds`.\n\n**Resources**\n- Every container has `requests` for CPU and memory, and a memory `limit`.\n- A CPU `limit` causes throttling, not kills; flag it only if it is very close to the request on a latency-sensitive service.\n- `requests` far above real usage waste capacity; far below cause noisy neighbours and evictions.\n\n**Rollouts and availability**\n- Deployments with `replicas: 1` in production → suggest at least 2 plus a `PodDisruptionBudget`.\n- `strategy.rollingUpdate.maxUnavailable: 0` for services that must not drop capacity during deploys.\n- Image tags are immutable versions (`1.4.2` or a digest), never `latest`.\n\n**Security**\n- `securityContext`: `runAsNonRoot: true`, `allowPrivilegeEscalation: false`, `readOnlyRootFilesystem: true` where the app allows it, capabilities dropped.\n- No `privileged: true` or `hostNetwork`/`hostPath` without a stated reason.\n- Secrets are referenced from `Secret` objects or an external secret store, never written in plain text in a ConfigMap or env value. Remind that a Secret is only base64-encoded unless etcd encryption is enabled.\n\n**Scaling**\n- An HPA targets a workload that has CPU/memory `requests` (utilization is computed against requests).\n- `minReplicas` of the HPA is not lower than what the PodDisruptionBudget needs.\n\n## Symptom table\n\n| Symptom | Look at |\n|---|---|\n| `Pending` | Requests larger than any node, missing PVC/StorageClass, node selectors or taints |\n| `ImagePullBackOff` | Image name/tag, registry credentials (`imagePullSecrets`) |\n| `CrashLoopBackOff` | App startup errors (`kubectl logs --previous`), liveness probe too strict, missing config |\n| `OOMKilled` | Memory limit vs real usage |\n| Service returns nothing | Selector vs pod labels, readiness failing (`kubectl get endpoints`) |\n| Slow but no errors | CPU throttling from a tight CPU limit |\n\n## Output format\n\n```markdown\n## Verdict\nReady | Ready with fixes | Not ready — one sentence why.\n\n## Findings\n1. **[high]** `Deployment/api` — liveness probe calls `/health/db` → restart loop when the database is down. Fix: point liveness to `/live` and move the DB check to readiness.\n\n## Suggested patch\n(YAML for the high findings)\n```\n"
26
+ }
27
+ ]
28
+ },
29
+ {
30
+ "name": "technical-guide-writer",
31
+ "description": "Researches and writes a structured technical guide in Markdown — what it is, the problem, how it works, hands-on examples, limits and when to use it — with Mermaid diagrams, tables and verified sources. Use when the user asks for a guide, tutorial, explainer or study notes about a technology, protocol or architecture concept.",
32
+ "descriptionPt": "Pesquisa e escreve um guia técnico em Markdown — o que é, o problema, como funciona, exemplos práticos, limites e quando usar — com diagramas Mermaid, tabelas e fontes verificadas.",
33
+ "license": "MIT",
34
+ "version": "1.0",
35
+ "files": [
36
+ {
37
+ "path": "SKILL.md",
38
+ "content": "---\nname: technical-guide-writer\ndescription: Researches and writes a structured technical guide in Markdown — what it is, the problem, how it works, hands-on examples, limits and when to use it — with Mermaid diagrams, tables and verified sources. Use when the user asks for a guide, tutorial, explainer or study notes about a technology, protocol or architecture concept.\nlicense: MIT\nmetadata:\n author: thecodecodex.com\n version: \"1.0\"\n description_pt: Pesquisa e escreve um guia técnico em Markdown — o que é, o problema, como funciona, exemplos práticos, limites e quando usar — com diagramas Mermaid, tabelas e fontes verificadas.\n---\n\n# Technical guide writer\n\nWrite guides that explain *why* before *how*, are accurate for the current version of the technology, and can be read in one sitting.\n\n## 1. Research first\n\n- Start with **primary sources**: official docs, changelogs, specifications, the project's repository. Then independent sources (reviews, postmortems, critiques), and say in the text when a claim comes only from the vendor.\n- For anything that evolves (languages, cloud services, CLIs), confirm the **current stable version** and state from which version each feature applies (\"since 1.11\").\n- Open every link you plan to cite. Drop the ones you could not verify.\n- Base code examples on official docs; mark your own adaptations.\n\n## 2. Structure\n\nUse `##` for sections (they become the table of contents) and `#####` for subsections. Default outline, adapt when the topic needs it:\n\n1. **What it is** — one-paragraph definition plus where the name comes from.\n2. **The problem** — what was painful before it existed.\n3. **How it works** — the mechanism, with a diagram; a table if there are types or variants.\n4. **In practice** — one to three short examples the reader can run, ideally a local lab that needs no paid account.\n5. **Limits** — what it does not do, caveats, what you could not verify.\n6. **When to use it (and when not)** — a two-column table with concrete alternatives.\n7. **Videos** and **References** — each link with one line of context.\n\nFor operational topics, a **Symptoms and causes** table (symptom → usual cause → command to investigate) is often the most useful section.\n\n## 3. Style\n\n- Direct sentences; no marketing adjectives. Vendor numbers always carry the caveat.\n- Bold the one idea per section the reader must remember.\n- Tables for comparisons, lists for steps, prose for reasoning.\n- 2 to 4 Mermaid diagrams per guide (` ```mermaid ` blocks). Each must explain something the text alone would leave confusing — flows, sequences, architecture. Quote labels that contain punctuation, and use `<br/>` for line breaks.\n- Code blocks always have a language tag and comments in the reader's language.\n- No raw HTML inside Markdown unless the target platform requires it.\n- Link concepts the reader may not know to a short explanation (a glossary entry or note) rather than explaining them inline at length.\n\n## 4. Before delivering\n\n- Re-check every version number and date against the sources you opened.\n- Render or lint the Mermaid diagrams if a tool is available; an invalid diagram usually breaks the build.\n- Deliver the guide plus a short summary: sections, sources used, and open points the author should decide.\n"
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "name": "terraform-review",
44
+ "description": "Reviews Terraform or OpenTofu code and plan output for destructive changes, state and locking problems, secrets leaking into state, fragile count/for_each usage and missing version pins. Use when the user shares .tf files, a terraform plan, a pull request touching infrastructure, or asks whether an apply is safe.",
45
+ "descriptionPt": "Revisa código e planos de Terraform ou OpenTofu procurando substituições destrutivas, problemas de state e lock, segredos no state, uso frágil de count e for_each e versões sem fixar.",
46
+ "license": "MIT",
47
+ "version": "1.0",
48
+ "files": [
49
+ {
50
+ "path": "SKILL.md",
51
+ "content": "---\nname: terraform-review\ndescription: Reviews Terraform or OpenTofu code and plan output for destructive changes, state and locking problems, secrets leaking into state, fragile count/for_each usage and missing version pins. Use when the user shares .tf files, a terraform plan, a pull request touching infrastructure, or asks whether an apply is safe.\nlicense: MIT\nmetadata:\n author: thecodecodex.com\n version: \"1.0\"\n description_pt: Revisa código e planos de Terraform ou OpenTofu procurando substituições destrutivas, problemas de state e lock, segredos no state, uso frágil de count e for_each e versões sem fixar.\n---\n\n# Terraform review\n\nReview infrastructure changes the way a careful platform engineer would before approving an apply: first what will be destroyed or replaced, then what could break later.\n\n## Workflow\n\n1. **Find the plan.** If the user only shared code, ask for (or run, if you have a shell and credentials are not needed) `terraform plan -no-color`. A review without the plan can only judge the code, so say so.\n2. **Read destructive actions first.** List every resource marked `-` (destroy), `-/+` (replace) or `+/-` (create before destroy). For each one, quote the `# forces replacement` attribute and say whether data can be lost (databases, disks, buckets, DNS zones, KMS keys).\n3. **Check the code** against [the checklist](references/checklist.md). Only report items that actually apply to the change.\n4. **Write the review** in the format below.\n\n## Output format\n\n```markdown\n## Verdict\nSafe to apply | Apply with care | Do not apply — one sentence why.\n\n## Destructive changes\n- `aws_db_instance.main` will be **replaced** because `identifier` changed → data loss unless restored from snapshot.\n\n## Findings\n1. **[high]** <problem> — <file:line> — <concrete fix, with a code snippet when short>\n2. **[medium]** ...\n\n## Nice to have\n- ...\n```\n\nSeverity: **high** = data loss, outage, security exposure or state corruption; **medium** = will hurt on the next change (count reorder, missing pin, missing lock); **low** = style and readability.\n\n## Rules of thumb\n\n- A rename is not a destroy: suggest a `moved` block instead of accepting a replace caused by a new address.\n- Adopting existing infrastructure goes through an `import` block, never by deleting and recreating it.\n- Never recommend `terraform state rm`, `state mv` or `force-unlock` without explaining the risk and asking for confirmation.\n- Do not invent provider behavior. If you are not sure whether an attribute forces replacement, say the plan is the source of truth.\n- Keep findings specific: a file, a resource address, a fix. Skip generic advice that does not apply to this diff.\n"
52
+ },
53
+ {
54
+ "path": "references/checklist.md",
55
+ "content": "# Terraform review checklist\n\nLoad this file during step 3 of the review. Report only what applies to the change being reviewed.\n\n## State and backend\n\n- Remote backend configured for any shared configuration (S3, GCS, Azure Blob, HCP Terraform). Local state in a team repo is a **high** finding.\n- Locking enabled. On S3 with Terraform 1.11+, `use_lockfile = true`; `dynamodb_table` is deprecated and should only remain during migration.\n- State bucket has versioning and encryption.\n- `*.tfstate`, `*.tfstate.backup` and `.terraform/` are in `.gitignore`; `.terraform.lock.hcl` is committed.\n- One huge state for everything → suggest splitting by layer (network, data, apps) and environment.\n\n## Versions\n\n- `required_version` set in the `terraform` block.\n- Every provider in `required_providers` has a `source` and a version constraint (`~> 5.0`, not unpinned).\n- Every registry or git module has a `version` or `?ref=` tag. A branch reference (`?ref=main`) is a **medium** finding.\n\n## Secrets\n\n- No credentials, tokens or passwords in `.tf`, `.tfvars` committed to git, or `default` values of variables.\n- Variables holding secrets are `sensitive = true`; on 1.10+ prefer `ephemeral = true` when the value is only needed during the run.\n- Passwords sent to resources use write-only attributes (1.11+) when the provider supports them; otherwise note that the value ends up in state.\n- Outputs exposing secrets are marked `sensitive`.\n\n## Resource identity\n\n- Collections use `for_each` with stable keys, not `count`, unless the items are truly interchangeable. Removing an element from a `count` list shifts every index after it.\n- Renamed or moved resources have a `moved` block.\n- Resources being dropped from management but kept in the cloud use a `removed` block with `lifecycle { destroy = false }`.\n- Stateful resources (databases, buckets with data, KMS keys) have `prevent_destroy = true` or deletion protection enabled at the provider level.\n\n## Dependencies and lifecycle\n\n- `depends_on` only where a hidden dependency exists (IAM propagation, API enablement); explain each one.\n- `ignore_changes` is narrow (specific attributes) and commented; `ignore_changes = all` is a **medium** finding.\n- `create_before_destroy` on resources that must not have downtime during replacement (certificates, launch templates), and the names allow two copies to coexist.\n\n## Validation and tests\n\n- Input variables have `type` and, where values are restricted, a `validation` block.\n- `terraform fmt -check` and `terraform validate` pass.\n- Reusable modules have `terraform test` files (`*.tftest.hcl`), at least with `command = plan`.\n\n## Security basics\n\n- No `0.0.0.0/0` ingress on SSH (22), RDP (3389) or database ports.\n- Buckets are not public unless intended, and the intent is written down.\n- IAM policies avoid `\"Action\": \"*\"` and `\"Resource\": \"*\"` together.\n- Encryption at rest enabled for storage and databases.\n"
56
+ }
57
+ ]
58
+ }
59
+ ]
60
+ }
package/src/skills.ts ADDED
@@ -0,0 +1,52 @@
1
+ // Catálogo de skills de IA (formato Agent Skills). Os arquivos vivem em skills/<nome>/ e o manifesto
2
+ // src/skills.json é gerado por scripts/build-skills.mjs — o site e o cli.js leem o mesmo manifesto.
3
+ import manifest from "./skills.json"
4
+
5
+ export interface SkillFile {
6
+ path: string
7
+ content: string
8
+ }
9
+
10
+ export interface Skill {
11
+ name: string
12
+ description: string
13
+ descriptionPt: string
14
+ license: string
15
+ version: string
16
+ files: SkillFile[]
17
+ }
18
+
19
+ export const SKILLS: Skill[] = manifest.skills
20
+
21
+ // o cli.js é publicado pelo site
22
+ export const SITE_URL = "https://thecodecodex.com"
23
+
24
+ // Pasta lida pelo Claude Code no projeto; outros agentes aceitam o mesmo formato em outra pasta (parâmetro dir)
25
+ export const DEFAULT_SKILLS_DIR = ".claude/skills"
26
+ export const SKILLS_TOOL = "ai-skills"
27
+
28
+ export const findSkill = (name: string): Skill | undefined => SKILLS.find((skill) => skill.name === name)
29
+
30
+ export const skillFile = (skill: Skill, path = "SKILL.md"): SkillFile | undefined => skill.files.find((file) => file.path === path)
31
+
32
+ // URL do arquivo cru publicado pelo site (src/pages/skills/[...path].ts)
33
+ export const skillFileUrl = (name: string, path: string): string => `/skills/${name}/${path}`
34
+
35
+ // Corpo do SKILL.md sem o frontmatter, para a prévia
36
+ export const skillBody = (content: string): string => content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "").trim()
37
+
38
+ export function installPlan(skills: Skill[], dir = DEFAULT_SKILLS_DIR): SkillFile[] {
39
+ const base = dir.trim().replace(/[\\/]+$/, "") || "."
40
+ if (base.split(/[\\/]/).includes("..")) throw new Error(`Pasta inválida: ${dir} (não use "..")`)
41
+ return skills.flatMap((skill) => skill.files.map((file) => ({ path: `${base}/${skill.name}/${file.path}`, content: file.content })))
42
+ }
43
+
44
+ export function installCommands(name: string, dir = DEFAULT_SKILLS_DIR): { deno: string, node: string } {
45
+ const script = `${SITE_URL}/cli.js`
46
+ const dirParam = dir === DEFAULT_SKILLS_DIR ? "" : ` dir=${dir}`
47
+ const root = dir.split(/[\\/]/)[0] || dir
48
+ return {
49
+ deno: `deno run -q --allow-write=${root} "${script}?tool=${SKILLS_TOOL}&name=${name}&install=1${dirParam ? `&dir=${encodeURIComponent(dir)}` : ""}"`,
50
+ node: `curl -s ${script} | node --input-type=module - ${SKILLS_TOOL} name=${name} install=1${dirParam}`,
51
+ }
52
+ }