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.
- package/bin/trackfw +1 -19
- package/package.json +9 -16
- package/src/commands/adr.js +30 -0
- package/src/commands/index.js +28 -0
- package/src/commands/init.js +143 -0
- package/src/commands/log.js +29 -0
- package/src/commands/plugins.js +96 -0
- package/src/commands/req.js +69 -0
- package/src/commands/roadmap.js +36 -0
- package/src/commands/status.js +11 -0
- package/src/commands/validate.js +28 -0
- package/src/generators/adr.js +172 -0
- package/src/generators/init.js +643 -0
- package/src/generators/req.js +239 -0
- package/src/generators/roadmap.js +224 -0
- package/src/validator/index.js +340 -0
- package/bin/.gitkeep +0 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/postinstall.js +0 -221
|
@@ -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
|
package/scripts/postinstall.js
DELETED
|
@@ -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
|
-
});
|