trackfw 1.0.3 → 1.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/README.md +17 -8
- package/bin/trackfw +4 -0
- package/package.json +10 -13
- package/src/commands/adr.js +31 -0
- package/src/commands/index.js +28 -0
- package/src/commands/init.js +174 -0
- package/src/commands/log.js +30 -0
- package/src/commands/plugins.js +97 -0
- package/src/commands/req.js +70 -0
- package/src/commands/roadmap.js +37 -0
- package/src/commands/status.js +12 -0
- package/src/commands/validate.js +29 -0
- package/src/generators/adr.js +172 -0
- package/src/generators/init.js +702 -0
- package/src/generators/req.js +239 -0
- package/src/generators/roadmap.js +224 -0
- package/src/i18n/index.js +53 -0
- package/src/i18n/locales/en-US.json +122 -0
- package/src/i18n/locales/es-ES.json +122 -0
- package/src/i18n/locales/pt-BR.json +122 -0
- package/src/validator/index.js +340 -0
- package/bin/.gitkeep +0 -0
- package/bin/README.md +0 -301
- package/bin/trackfw-darwin-amd64 +0 -0
- package/bin/trackfw-darwin-arm64 +0 -0
- package/bin/trackfw-linux-amd64 +0 -0
- package/bin/trackfw-linux-arm64 +0 -0
- package/bin/trackfw-windows-amd64.exe +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Converte uma string em slug: lowercase + espaços → hifens.
|
|
8
|
+
* @param {string} s
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function toSlug(s) {
|
|
12
|
+
return s.toLowerCase().replace(/ /g, '-')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converte slug com hifens em Title Case.
|
|
17
|
+
* Ex: "authentication-strategy" → "Authentication Strategy"
|
|
18
|
+
* @param {string} slug
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function slugToTitle(slug) {
|
|
22
|
+
return slug
|
|
23
|
+
.split('-')
|
|
24
|
+
.map((w) => (w.length > 0 ? w[0].toUpperCase() + w.slice(1) : w))
|
|
25
|
+
.join(' ')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Retorna a data atual no formato YYYY-MM-DD.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function today() {
|
|
33
|
+
return new Date().toISOString().slice(0, 10)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cria um novo ADR em docs/adr/ADR-YYYY-MM-DD-<slug>.md.
|
|
38
|
+
* Campos vazios recebem placeholder HTML.
|
|
39
|
+
* @param {{ title: string, context?: string, decision?: string, consequences?: string, alternatives?: string }} content
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
*/
|
|
42
|
+
async function newADR(content) {
|
|
43
|
+
fs.mkdirSync('docs/adr', { recursive: true })
|
|
44
|
+
|
|
45
|
+
const slug = toSlug(content.title)
|
|
46
|
+
const date = today()
|
|
47
|
+
const filename = `docs/adr/ADR-${date}-${slug}.md`
|
|
48
|
+
|
|
49
|
+
const contextSection = content.context || '<!-- What is the situation that motivates this decision? -->'
|
|
50
|
+
const decisionSection = content.decision || '<!-- What was decided? -->'
|
|
51
|
+
const consequencesSection = content.consequences || '<!-- What are the positive and negative consequences of this decision? -->'
|
|
52
|
+
const alternativesSection = content.alternatives || '<!-- What other options were evaluated and why were they rejected? -->'
|
|
53
|
+
|
|
54
|
+
const body = `# ADR: ${content.title}
|
|
55
|
+
|
|
56
|
+
> Date: ${date} | Status: Proposed
|
|
57
|
+
|
|
58
|
+
## Context
|
|
59
|
+
${contextSection}
|
|
60
|
+
|
|
61
|
+
## Decision
|
|
62
|
+
${decisionSection}
|
|
63
|
+
|
|
64
|
+
## Consequences
|
|
65
|
+
${consequencesSection}
|
|
66
|
+
|
|
67
|
+
## Alternatives Considered
|
|
68
|
+
${alternativesSection}
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
fs.writeFileSync(filename, body, 'utf8')
|
|
72
|
+
console.log(`created ${filename}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Lista todos os ADRs (.md) em dir, imprimindo filename e status (coluna 60 chars).
|
|
77
|
+
* @param {string} dir
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
async function listADRs(dir) {
|
|
81
|
+
if (!fs.existsSync(dir)) {
|
|
82
|
+
console.log(`No ADRs found in ${dir}`)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort()
|
|
87
|
+
|
|
88
|
+
if (files.length === 0) {
|
|
89
|
+
console.log(`No ADRs found in ${dir}`)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const filepath = path.join(dir, file)
|
|
95
|
+
const status = parseADRStatus(filepath)
|
|
96
|
+
const padded = file.padEnd(60)
|
|
97
|
+
console.log(`${padded} ${status}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extrai o status de um arquivo ADR markdown.
|
|
103
|
+
* Procura pela linha "> Date: ... | Status: ..."
|
|
104
|
+
* @param {string} filepath
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function parseADRStatus(filepath) {
|
|
108
|
+
try {
|
|
109
|
+
const content = fs.readFileSync(filepath, 'utf8')
|
|
110
|
+
const lines = content.split('\n')
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
const idx = line.indexOf('| Status: ')
|
|
113
|
+
if (idx >= 0) {
|
|
114
|
+
let rest = line.slice(idx + '| Status: '.length)
|
|
115
|
+
rest = rest.replace(/[ >|]+$/, '').trim()
|
|
116
|
+
return rest
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (_) {
|
|
120
|
+
// ignorar erros de leitura
|
|
121
|
+
}
|
|
122
|
+
return 'unknown'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Cria um ADR com Status: Draft a partir de um slug.
|
|
127
|
+
* Idempotente: se já existe ADR-*-<slug>.md, pula e imprime mensagem.
|
|
128
|
+
* @param {string} slug
|
|
129
|
+
* @returns {Promise<string>} basename do arquivo criado
|
|
130
|
+
*/
|
|
131
|
+
async function newADRDraft(slug) {
|
|
132
|
+
fs.mkdirSync('docs/adr', { recursive: true })
|
|
133
|
+
|
|
134
|
+
// Verificar idempotência: buscar arquivo existente com o mesmo slug
|
|
135
|
+
const adrDir = 'docs/adr'
|
|
136
|
+
const existing = fs.existsSync(adrDir)
|
|
137
|
+
? fs.readdirSync(adrDir).find((f) => f.match(new RegExp(`^ADR-.*-${slug}\\.md$`)))
|
|
138
|
+
: null
|
|
139
|
+
|
|
140
|
+
if (existing) {
|
|
141
|
+
console.log(`skipped ${existing} (already exists)`)
|
|
142
|
+
return existing
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const date = today()
|
|
146
|
+
const filename = `ADR-${date}-${slug}.md`
|
|
147
|
+
const filepath = path.join('docs/adr', filename)
|
|
148
|
+
const title = slugToTitle(slug)
|
|
149
|
+
|
|
150
|
+
const body = `# ADR: ${title}
|
|
151
|
+
|
|
152
|
+
> Date: ${date} | Status: Draft
|
|
153
|
+
|
|
154
|
+
## Context
|
|
155
|
+
<!-- What is the situation that motivates this decision? -->
|
|
156
|
+
|
|
157
|
+
## Decision
|
|
158
|
+
<!-- What was decided? -->
|
|
159
|
+
|
|
160
|
+
## Consequences
|
|
161
|
+
<!-- What are the positive and negative consequences of this decision? -->
|
|
162
|
+
|
|
163
|
+
## Alternatives Considered
|
|
164
|
+
<!-- What other options were evaluated and why were they rejected? -->
|
|
165
|
+
`
|
|
166
|
+
|
|
167
|
+
fs.writeFileSync(filepath, body, 'utf8')
|
|
168
|
+
console.log(`created ${filename}`)
|
|
169
|
+
return filename
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { newADR, listADRs, newADRDraft, toSlug }
|