trackfw 1.0.2 → 1.0.4

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.
@@ -0,0 +1,340 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ const STALE_WIP_DAYS = 7
7
+
8
+ // listDir retorna array de nomes de arquivo (não-diretórios) em dir.
9
+ // Retorna [] se o diretório não existir.
10
+ function listDir(dir) {
11
+ try {
12
+ return fs.readdirSync(dir).filter(name => {
13
+ try {
14
+ return !fs.statSync(path.join(dir, name)).isDirectory()
15
+ } catch (_) {
16
+ return false
17
+ }
18
+ })
19
+ } catch (_) {
20
+ return []
21
+ }
22
+ }
23
+
24
+ // parseBlockedADRs extrai basenames de ADRs da seção "## Blocked by ADRs" de um arquivo REQ.
25
+ function parseBlockedADRs(filePath) {
26
+ let content
27
+ try {
28
+ content = fs.readFileSync(filePath, 'utf8')
29
+ } catch (_) {
30
+ return []
31
+ }
32
+ const lines = content.split('\n')
33
+ const adrs = []
34
+ let inSection = false
35
+ for (const line of lines) {
36
+ if (line === '## Blocked by ADRs') {
37
+ inSection = true
38
+ continue
39
+ }
40
+ if (inSection) {
41
+ if (line.startsWith('## ')) break
42
+ if (line.startsWith('- ')) {
43
+ const item = line.slice(2).trim()
44
+ const parts = item.split(/\s+/)
45
+ if (parts.length > 0 && parts[0].endsWith('.md')) {
46
+ adrs.push(parts[0])
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return adrs
52
+ }
53
+
54
+ // adrIsDraft verifica se docs/adr/<basename> contém "Status: Draft".
55
+ function adrIsDraft(basename) {
56
+ try {
57
+ const content = fs.readFileSync(path.join('docs', 'adr', basename), 'utf8')
58
+ return content.includes('Status: Draft')
59
+ } catch (_) {
60
+ return false
61
+ }
62
+ }
63
+
64
+ // validateWIPHasREQ — roadmaps em docs/roadmaps/wip/ sem "REQ:" no conteúdo → violation
65
+ function validateWIPHasREQ() {
66
+ const entries = listDir('docs/roadmaps/wip')
67
+ const violations = []
68
+ for (const name of entries) {
69
+ try {
70
+ const content = fs.readFileSync(path.join('docs/roadmaps/wip', name), 'utf8')
71
+ if (!content.includes('REQ:') || content.includes('REQ: \n')) {
72
+ violations.push(`roadmap "${name}" is in wip but has no linked REQ`)
73
+ }
74
+ } catch (_) {
75
+ // ignorar erro de leitura
76
+ }
77
+ }
78
+ return violations
79
+ }
80
+
81
+ // validateREQsHaveADR — REQs em docs/req/ sem "ADR:" no conteúdo → violation
82
+ function validateREQsHaveADR() {
83
+ const entries = listDir('docs/req')
84
+ const violations = []
85
+ for (const name of entries) {
86
+ try {
87
+ const content = fs.readFileSync(path.join('docs/req', name), 'utf8')
88
+ if (!content.includes('ADR:') || content.includes('ADR: \n')) {
89
+ violations.push(`req "${name}" has no linked ADR`)
90
+ }
91
+ } catch (_) {
92
+ // ignorar
93
+ }
94
+ }
95
+ return violations
96
+ }
97
+
98
+ // validateBlockedHasREQ — roadmaps em docs/roadmaps/blocked/ sem "REQ:" → violation
99
+ function validateBlockedHasREQ() {
100
+ const entries = listDir('docs/roadmaps/blocked')
101
+ const violations = []
102
+ for (const name of entries) {
103
+ try {
104
+ const content = fs.readFileSync(path.join('docs/roadmaps/blocked', name), 'utf8')
105
+ if (!content.includes('REQ:') || content.includes('REQ: \n')) {
106
+ violations.push(`roadmap "${name}" is in blocked but has no linked REQ`)
107
+ }
108
+ } catch (_) {
109
+ // ignorar
110
+ }
111
+ }
112
+ return violations
113
+ }
114
+
115
+ // validateREQsHaveRoadmap — REQs sem "Roadmap:" → violation
116
+ function validateREQsHaveRoadmap() {
117
+ const entries = listDir('docs/req')
118
+ const violations = []
119
+ for (const name of entries) {
120
+ try {
121
+ const content = fs.readFileSync(path.join('docs/req', name), 'utf8')
122
+ if (!content.includes('Roadmap:') || content.includes('Roadmap: \n')) {
123
+ violations.push(`req "${name}" has no linked Roadmap`)
124
+ }
125
+ } catch (_) {
126
+ // ignorar
127
+ }
128
+ }
129
+ return violations
130
+ }
131
+
132
+ // validateADRsAreReferenced — ADRs em docs/adr/ não referenciados em nenhuma REQ → violation
133
+ function validateADRsAreReferenced() {
134
+ const adrs = listDir('docs/adr')
135
+ const reqEntries = listDir('docs/req')
136
+
137
+ let combined = ''
138
+ for (const name of reqEntries) {
139
+ try {
140
+ combined += fs.readFileSync(path.join('docs/req', name), 'utf8')
141
+ } catch (_) {
142
+ // ignorar
143
+ }
144
+ }
145
+
146
+ const violations = []
147
+ for (const adr of adrs) {
148
+ if (!combined.includes(adr)) {
149
+ violations.push(`adr "${adr}" is not referenced by any REQ`)
150
+ }
151
+ }
152
+ return violations
153
+ }
154
+
155
+ // validateWIPHasAcceptanceCriteria — roadmaps wip sem bloco de critérios de aceite → violation
156
+ function validateWIPHasAcceptanceCriteria() {
157
+ const entries = listDir('docs/roadmaps/wip')
158
+ const violations = []
159
+ for (const name of entries) {
160
+ try {
161
+ const content = fs.readFileSync(path.join('docs/roadmaps/wip', name), 'utf8')
162
+ const hasBlock =
163
+ content.includes('## Acceptance Criteria') ||
164
+ content.includes('## Critérios de Aceite') ||
165
+ content.includes('acceptance criteria') ||
166
+ content.includes('Acceptance Criteria:')
167
+ if (!hasBlock) {
168
+ violations.push(`roadmap "${name}" is in wip but has no acceptance criteria block`)
169
+ }
170
+ } catch (_) {
171
+ // ignorar
172
+ }
173
+ }
174
+ return violations
175
+ }
176
+
177
+ // validateSingleWIP — mais de 1 roadmap em wip → warning
178
+ function validateSingleWIP() {
179
+ const entries = listDir('docs/roadmaps/wip')
180
+ if (entries.length > 1) {
181
+ return [`${entries.length} roadmaps in wip/ (recommended: keep only 1 active at a time)`]
182
+ }
183
+ return []
184
+ }
185
+
186
+ // validateStaleWIP — roadmaps wip com mtime >= 7 dias → warning
187
+ function validateStaleWIP() {
188
+ let files = []
189
+ try {
190
+ files = fs.readdirSync('docs/roadmaps/wip')
191
+ .filter(f => f.endsWith('.md'))
192
+ .map(f => path.join('docs/roadmaps/wip', f))
193
+ } catch (_) {
194
+ return []
195
+ }
196
+
197
+ const warnings = []
198
+ const now = Date.now()
199
+ for (const filePath of files) {
200
+ try {
201
+ const stat = fs.statSync(filePath)
202
+ const ageMs = now - stat.mtimeMs
203
+ const days = Math.floor(ageMs / (1000 * 60 * 60 * 24))
204
+ if (days >= STALE_WIP_DAYS) {
205
+ const lastModified = stat.mtime.toISOString().slice(0, 10)
206
+ const basename = path.basename(filePath)
207
+ warnings.push(
208
+ `roadmap/wip/${basename} has been in WIP for ${days} days (last modified ${lastModified})`
209
+ )
210
+ }
211
+ } catch (_) {
212
+ // ignorar
213
+ }
214
+ }
215
+ return warnings
216
+ }
217
+
218
+ // validateREQsNotBlockedByDraftADRs — REQs Open com ADRs Draft na seção "## Blocked by ADRs" → violation
219
+ function validateREQsNotBlockedByDraftADRs() {
220
+ const entries = listDir('docs/req')
221
+ const violations = []
222
+ for (const name of entries) {
223
+ const filePath = path.join('docs/req', name)
224
+ let content
225
+ try {
226
+ content = fs.readFileSync(filePath, 'utf8')
227
+ } catch (_) {
228
+ continue
229
+ }
230
+ if (!content.includes('Status: Open')) continue
231
+
232
+ const blockedADRs = parseBlockedADRs(filePath)
233
+ for (const adrBasename of blockedADRs) {
234
+ if (adrIsDraft(adrBasename)) {
235
+ violations.push(`REQ ${name} is blocked by Draft ADR: ${adrBasename}`)
236
+ }
237
+ }
238
+ }
239
+ return violations
240
+ }
241
+
242
+ // blockedREQs retorna mapa de reqBasename → [adrBasenames Draft] para uso em getStatus()
243
+ function blockedREQs() {
244
+ const entries = listDir('docs/req')
245
+ const result = {}
246
+ for (const name of entries) {
247
+ const filePath = path.join('docs/req', name)
248
+ let content
249
+ try {
250
+ content = fs.readFileSync(filePath, 'utf8')
251
+ } catch (_) {
252
+ continue
253
+ }
254
+ if (!content.includes('Status: Open')) continue
255
+
256
+ const adrNames = parseBlockedADRs(filePath)
257
+ const draftADRs = adrNames.filter(a => adrIsDraft(a))
258
+ if (draftADRs.length > 0) {
259
+ result[name] = draftADRs
260
+ }
261
+ }
262
+ return result
263
+ }
264
+
265
+ // validate executa todas as validações e retorna { violations, warnings }
266
+ async function validate() {
267
+ const violations = [
268
+ ...validateWIPHasREQ(),
269
+ ...validateREQsHaveADR(),
270
+ ...validateBlockedHasREQ(),
271
+ ...validateREQsHaveRoadmap(),
272
+ ...validateADRsAreReferenced(),
273
+ ...validateWIPHasAcceptanceCriteria(),
274
+ ...validateREQsNotBlockedByDraftADRs(),
275
+ ]
276
+ const warnings = [
277
+ ...validateSingleWIP(),
278
+ ...validateStaleWIP(),
279
+ ]
280
+ return { violations, warnings }
281
+ }
282
+
283
+ // getStatus retorna string formatada com o status de governança do projeto
284
+ async function getStatus() {
285
+ const wip = listDir('docs/roadmaps/wip')
286
+ const blocked = listDir('docs/roadmaps/blocked')
287
+ const done = listDir('docs/roadmaps/done')
288
+
289
+ let out = ''
290
+ out += '── trackfw status ──────────────────────\n'
291
+
292
+ out += `\n🔄 WIP (${wip.length})\n`
293
+ for (const f of wip) out += ` ${f}\n`
294
+
295
+ out += `\n❌ Blocked (${blocked.length})\n`
296
+ for (const f of blocked) out += ` ${f}\n`
297
+
298
+ const staleWIPs = validateStaleWIP()
299
+ if (staleWIPs.length > 0) {
300
+ out += `\n⚠ Stale WIP (${staleWIPs.length})\n`
301
+ for (const w of staleWIPs) out += ` ${w}\n`
302
+ }
303
+
304
+ const blockedByDraft = blockedREQs()
305
+ const blockedKeys = Object.keys(blockedByDraft)
306
+ if (blockedKeys.length > 0) {
307
+ out += `\n⏳ REQs blocked by Draft ADRs (${blockedKeys.length})\n`
308
+ for (const reqFile of blockedKeys) {
309
+ out += ` ${reqFile}\n`
310
+ for (const adr of blockedByDraft[reqFile]) {
311
+ out += ` → ${adr} (Draft)\n`
312
+ }
313
+ }
314
+ }
315
+
316
+ out += `\n✅ Done (last 5)\n`
317
+ const last5 = done.length > 5 ? done.slice(done.length - 5) : done
318
+ for (const f of last5) out += ` ${f}\n`
319
+
320
+ out += '\n────────────────────────────────────────\n'
321
+ return out
322
+ }
323
+
324
+ module.exports = {
325
+ validate,
326
+ getStatus,
327
+ // exportadas para testes unitários
328
+ validateWIPHasREQ,
329
+ validateREQsHaveADR,
330
+ validateBlockedHasREQ,
331
+ validateREQsHaveRoadmap,
332
+ validateADRsAreReferenced,
333
+ validateWIPHasAcceptanceCriteria,
334
+ validateSingleWIP,
335
+ validateStaleWIP,
336
+ validateREQsNotBlockedByDraftADRs,
337
+ parseBlockedADRs,
338
+ adrIsDraft,
339
+ listDir,
340
+ }
package/bin/.gitkeep DELETED
File without changes
package/scripts/.gitkeep DELETED
File without changes
@@ -1,221 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- 'use strict';
4
-
5
- const https = require('https');
6
- const fs = require('fs');
7
- const path = require('path');
8
- const os = require('os');
9
- const child_process = require('child_process');
10
-
11
- // ---------------------------------------------------------------------------
12
- // Mapeamento de plataforma e arquitetura
13
- // ---------------------------------------------------------------------------
14
-
15
- const PLATFORM_MAP = {
16
- linux: 'linux',
17
- darwin: 'darwin',
18
- win32: 'windows',
19
- };
20
-
21
- const ARCH_MAP = {
22
- x64: 'amd64',
23
- arm64: 'arm64',
24
- };
25
-
26
- const platform = PLATFORM_MAP[process.platform];
27
- const arch = ARCH_MAP[process.arch];
28
-
29
- if (!platform || !arch) {
30
- console.warn('trackfw: plataforma não suportada, pulando instalação do binário');
31
- process.exit(0);
32
- }
33
-
34
- // ---------------------------------------------------------------------------
35
- // Versão — lida do package.json do wrapper npm
36
- // ---------------------------------------------------------------------------
37
-
38
- const pkgPath = path.join(__dirname, '..', 'package.json');
39
- const { version } = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
40
-
41
- // ---------------------------------------------------------------------------
42
- // URL de download
43
- // ---------------------------------------------------------------------------
44
-
45
- const isWindows = platform === 'windows';
46
- const ext = isWindows ? '.zip' : '.tar.gz';
47
- const archiveName = `trackfw_${version}_${platform}_${arch}${ext}`;
48
- const downloadUrl = `https://github.com/kgsaran/trackfw/releases/download/v${version}/${archiveName}`;
49
-
50
- // ---------------------------------------------------------------------------
51
- // Destino final do binário
52
- // ---------------------------------------------------------------------------
53
-
54
- const binDir = path.join(__dirname, '..', 'bin');
55
- const binName = isWindows ? 'trackfw-bin.exe' : 'trackfw-bin';
56
- const binDest = path.join(binDir, binName);
57
-
58
- if (!fs.existsSync(binDir)) {
59
- fs.mkdirSync(binDir, { recursive: true });
60
- }
61
-
62
- // ---------------------------------------------------------------------------
63
- // Helpers
64
- // ---------------------------------------------------------------------------
65
-
66
- function download(url, destFile) {
67
- return new Promise((resolve, reject) => {
68
- const file = fs.createWriteStream(destFile);
69
-
70
- function get(currentUrl) {
71
- https
72
- .get(currentUrl, (res) => {
73
- if (res.statusCode === 301 || res.statusCode === 302) {
74
- const location = res.headers['location'];
75
- if (!location) {
76
- reject(new Error('Redirect sem Location header'));
77
- return;
78
- }
79
- res.resume();
80
- // reabrir o arquivo para a requisição seguinte não acumular lixo
81
- file.close(() => {
82
- const file2 = fs.createWriteStream(destFile);
83
- file2.on('finish', () => file2.close(resolve));
84
- file2.on('error', (err) => { fs.unlink(destFile, () => {}); reject(err); });
85
- https.get(location, (res2) => {
86
- if (res2.statusCode !== 200) {
87
- reject(new Error(`Falha ao baixar ${location}: HTTP ${res2.statusCode}`));
88
- return;
89
- }
90
- res2.pipe(file2);
91
- }).on('error', (err) => { fs.unlink(destFile, () => {}); reject(err); });
92
- });
93
- return;
94
- }
95
-
96
- if (res.statusCode !== 200) {
97
- reject(new Error(`Falha ao baixar ${currentUrl}: HTTP ${res.statusCode}`));
98
- return;
99
- }
100
-
101
- res.pipe(file);
102
- file.on('finish', () => file.close(resolve));
103
- file.on('error', (err) => {
104
- fs.unlink(destFile, () => {});
105
- reject(err);
106
- });
107
- })
108
- .on('error', (err) => {
109
- fs.unlink(destFile, () => {});
110
- reject(err);
111
- });
112
- }
113
-
114
- get(url);
115
- });
116
- }
117
-
118
- function extract(archiveFile, destDir) {
119
- if (isWindows) {
120
- child_process.execSync(
121
- `powershell -NoProfile -Command "Expand-Archive -LiteralPath '${archiveFile}' -DestinationPath '${destDir}' -Force"`,
122
- { stdio: 'pipe' }
123
- );
124
- } else {
125
- child_process.execSync(
126
- `tar -xzf "${archiveFile}" -C "${destDir}"`,
127
- { stdio: 'pipe' }
128
- );
129
- }
130
- }
131
-
132
- // Busca recursiva pelo binário em qualquer subdiretório após extração
133
- function findBinary(dir, name) {
134
- const entries = fs.readdirSync(dir, { withFileTypes: true });
135
- for (const entry of entries) {
136
- const full = path.join(dir, entry.name);
137
- if (entry.isDirectory()) {
138
- const found = findBinary(full, name);
139
- if (found) return found;
140
- } else if (entry.name === name) {
141
- return full;
142
- }
143
- }
144
- return null;
145
- }
146
-
147
- function cleanup(target) {
148
- try {
149
- if (!fs.existsSync(target)) return;
150
- const stat = fs.statSync(target);
151
- if (stat.isDirectory()) {
152
- fs.rmSync(target, { recursive: true, force: true });
153
- } else {
154
- fs.unlinkSync(target);
155
- }
156
- } catch (_) {}
157
- }
158
-
159
- // ---------------------------------------------------------------------------
160
- // Função principal
161
- // ---------------------------------------------------------------------------
162
-
163
- async function main() {
164
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trackfw-'));
165
- const tmpFile = path.join(tmpDir, archiveName);
166
-
167
- try {
168
- console.log(`trackfw: baixando binário para ${platform}/${arch} v${version}...`);
169
- console.log(` ${downloadUrl}`);
170
-
171
- await download(downloadUrl, tmpFile);
172
-
173
- const fileSize = fs.statSync(tmpFile).size;
174
- if (fileSize < 1000) {
175
- throw new Error(`Arquivo baixado suspeito (${fileSize} bytes) — verifique a conexão ou se a versão v${version} foi publicada no GitHub`);
176
- }
177
-
178
- console.log('trackfw: extraindo arquivo...');
179
- extract(tmpFile, tmpDir);
180
-
181
- const extractedBinName = isWindows ? 'trackfw.exe' : 'trackfw';
182
- const extractedBin = findBinary(tmpDir, extractedBinName);
183
-
184
- if (!extractedBin) {
185
- const files = [];
186
- function listAll(d) {
187
- for (const e of fs.readdirSync(d, { withFileTypes: true })) {
188
- const p = path.join(d, e.name);
189
- files.push(p);
190
- if (e.isDirectory()) listAll(p);
191
- }
192
- }
193
- listAll(tmpDir);
194
- throw new Error(
195
- `Binário "${extractedBinName}" não encontrado após extração.\nArquivos encontrados:\n${files.join('\n')}`
196
- );
197
- }
198
-
199
- fs.renameSync(extractedBin, binDest);
200
-
201
- if (!isWindows) {
202
- fs.chmodSync(binDest, 0o755);
203
- }
204
-
205
- console.log('trackfw: binário instalado com sucesso em ' + binDest);
206
- } finally {
207
- cleanup(tmpFile);
208
- cleanup(tmpDir);
209
- }
210
- }
211
-
212
- main().catch((err) => {
213
- console.error('\ntrackfw: ERRO ao instalar binário:');
214
- console.error(' ' + err.message);
215
- console.error('\nAlternativas de instalação:');
216
- console.error(' curl -sSfL https://github.com/kgsaran/trackfw/releases/latest/download/install.sh | sh');
217
- console.error(' brew install kgsaran/tap/trackfw (macOS/Linux)');
218
- console.error(' go install github.com/kgsaran/trackfw/cmd/trackfw@latest\n');
219
- // Sair com 0 para não bloquear npm install em CIs sem acesso ao GitHub
220
- process.exit(0);
221
- });