trackfw 1.0.4 → 2.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/README.md +17 -8
- package/package.json +1 -1
- package/src/commands/adr.js +9 -8
- package/src/commands/discover.js +359 -0
- package/src/commands/index.js +1 -0
- package/src/commands/init.js +50 -19
- package/src/commands/log.js +5 -4
- package/src/commands/plugins.js +11 -10
- package/src/commands/req.js +11 -10
- package/src/commands/roadmap.js +6 -5
- package/src/commands/status.js +2 -1
- package/src/commands/validate.js +5 -4
- package/src/config/index.js +92 -0
- package/src/generators/adr.js +6 -5
- package/src/generators/init.js +66 -0
- package/src/generators/req.js +3 -2
- package/src/generators/roadmap.js +150 -57
- 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 +196 -97
package/README.md
CHANGED
|
@@ -79,12 +79,14 @@ brew install kgsaran/tap/trackfw
|
|
|
79
79
|
go install github.com/kgsaran/trackfw/cmd/trackfw@latest
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
### npm
|
|
82
|
+
### npm (pure Node.js — no binary)
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
85
|
npm install -g trackfw
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
The npm package is pure Node.js — no compiled binary, no postinstall download. Works everywhere Node.js ≥ 18 is installed, including corporate Windows environments where unsigned `.exe` files are blocked by antivirus.
|
|
89
|
+
|
|
88
90
|
### pip
|
|
89
91
|
|
|
90
92
|
```bash
|
|
@@ -127,18 +129,25 @@ trackfw status
|
|
|
127
129
|
| `trackfw req new "title"` | Create a REQ with guided ADR discovery |
|
|
128
130
|
| `trackfw req list` | List all REQs with status |
|
|
129
131
|
| `trackfw roadmap new "title"` | Create a roadmap in `backlog/` |
|
|
132
|
+
| `trackfw roadmap show <name>` | Print a roadmap with its current state |
|
|
130
133
|
| `trackfw roadmap move <name> <state>` | Move roadmap between states |
|
|
131
134
|
| `trackfw roadmap list` | List all roadmaps grouped by state |
|
|
132
135
|
| `trackfw validate` | Check governance consistency (use as CI gate) |
|
|
133
136
|
| `trackfw status` | Show wip, blocked, REQs waiting on ADRs |
|
|
134
|
-
| `trackfw
|
|
135
|
-
| `trackfw
|
|
136
|
-
| `trackfw
|
|
137
|
-
| `trackfw
|
|
138
|
-
| `trackfw
|
|
139
|
-
| `trackfw
|
|
137
|
+
| `trackfw log [--tail N]` | Show roadmap state transition history |
|
|
138
|
+
| `trackfw plugins list` | List installed plugins |
|
|
139
|
+
| `trackfw plugins add <user/repo>` | Install a plugin from GitHub Releases |
|
|
140
|
+
| `trackfw plugins remove <name>` | Remove an installed plugin |
|
|
141
|
+
| `trackfw agents` | Install Claude Code subagents *(Go binary only)* |
|
|
142
|
+
| `trackfw gemini` | Install Gemini CLI skills and commands *(Go binary only)* |
|
|
143
|
+
| `trackfw cursor` | Install Cursor rules *(Go binary only)* |
|
|
144
|
+
| `trackfw copilot` | Install GitHub Copilot instructions *(Go binary only)* |
|
|
145
|
+
| `trackfw windsurf` | Install Windsurf rules and workflows *(Go binary only)* |
|
|
146
|
+
| `trackfw amazonq` | Install Amazon Q Developer rules *(Go binary only)* |
|
|
140
147
|
| `trackfw version` | Print version |
|
|
141
148
|
|
|
149
|
+
> **Go binary only** commands (`agents`, `gemini`, `cursor`, `copilot`, `windsurf`, `amazonq`) are available when installed via brew, `install.sh`, or `go install`. When using the npm package, AI integrations are installed through `trackfw init`.
|
|
150
|
+
|
|
142
151
|
---
|
|
143
152
|
|
|
144
153
|
## Governance chain
|
|
@@ -227,7 +236,7 @@ $ trackfw status
|
|
|
227
236
|
|
|
228
237
|
## AI assistant integration
|
|
229
238
|
|
|
230
|
-
`trackfw init` asks which AI tools your team uses and installs native governance context for each.
|
|
239
|
+
`trackfw init` asks which AI tools your team uses and installs native governance context for each. When using the Go binary (brew, `install.sh`, `go install`), each integration can also be run as a standalone command.
|
|
231
240
|
|
|
232
241
|
| Command | Installs | Format |
|
|
233
242
|
|---|---|---|
|
package/package.json
CHANGED
package/src/commands/adr.js
CHANGED
|
@@ -3,28 +3,29 @@
|
|
|
3
3
|
const { Command } = require('commander')
|
|
4
4
|
const { input } = require('@inquirer/prompts')
|
|
5
5
|
const generators = require('../generators/adr')
|
|
6
|
+
const { t } = require('../i18n')
|
|
6
7
|
|
|
7
8
|
const cmd = new Command('adr')
|
|
8
|
-
cmd.description('
|
|
9
|
+
cmd.description(t('adr.description'))
|
|
9
10
|
|
|
10
11
|
cmd.command('new <title>')
|
|
11
|
-
.description('
|
|
12
|
+
.description(t('adr.new.description'))
|
|
12
13
|
.action(async (title) => {
|
|
13
14
|
const content = { title }
|
|
14
15
|
// wizard interativo se TTY
|
|
15
16
|
if (process.stdin.isTTY) {
|
|
16
|
-
content.context = await input({ message: '
|
|
17
|
-
content.decision = await input({ message: '
|
|
18
|
-
content.consequences = await input({ message: '
|
|
19
|
-
content.alternatives = await input({ message: '
|
|
17
|
+
content.context = await input({ message: t('adr.new.prompt.context'), default: '' })
|
|
18
|
+
content.decision = await input({ message: t('adr.new.prompt.decision'), default: '' })
|
|
19
|
+
content.consequences = await input({ message: t('adr.new.prompt.consequences'), default: '' })
|
|
20
|
+
content.alternatives = await input({ message: t('adr.new.prompt.alternatives'), default: '' })
|
|
20
21
|
}
|
|
21
22
|
await generators.newADR(content)
|
|
22
23
|
})
|
|
23
24
|
|
|
24
25
|
cmd.command('list')
|
|
25
|
-
.description('
|
|
26
|
+
.description(t('adr.list.description'))
|
|
26
27
|
.action(async () => {
|
|
27
|
-
await generators.listADRs('
|
|
28
|
+
await generators.listADRs(require('../config').load().adrDirs[0])
|
|
28
29
|
})
|
|
29
30
|
|
|
30
31
|
module.exports = cmd
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function scan(rootDir) {
|
|
8
|
+
const r = {
|
|
9
|
+
adrDirs: [],
|
|
10
|
+
reqDir: '',
|
|
11
|
+
roadmapDir: '',
|
|
12
|
+
roadmapNamespacing: 'flat',
|
|
13
|
+
agents: [],
|
|
14
|
+
adrCount: 0,
|
|
15
|
+
reqCount: 0,
|
|
16
|
+
roadmapCount: 0,
|
|
17
|
+
hasTrackfwYAML: false,
|
|
18
|
+
hasTrackfwLog: false,
|
|
19
|
+
governanceScore: 0,
|
|
20
|
+
hookFramework: 'none',
|
|
21
|
+
ciSystem: 'none',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// trackfw.yaml
|
|
25
|
+
r.hasTrackfwYAML = fs.existsSync(path.join(rootDir, 'trackfw.yaml'));
|
|
26
|
+
|
|
27
|
+
// REQ dir
|
|
28
|
+
for (const candidate of ['docs/req', 'docs/requisições', 'docs/requirements', 'docs/reqs']) {
|
|
29
|
+
const full = path.join(rootDir, candidate);
|
|
30
|
+
if (isDir(full)) {
|
|
31
|
+
r.reqDir = candidate;
|
|
32
|
+
r.reqCount = countMD(full);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ADR dirs
|
|
38
|
+
const adrRoot = path.join(rootDir, 'docs', 'adr');
|
|
39
|
+
if (isDir(adrRoot)) {
|
|
40
|
+
const subDirs = listSubDirs(adrRoot);
|
|
41
|
+
if (subDirs.length > 0) {
|
|
42
|
+
for (const sub of subDirs) {
|
|
43
|
+
const rel = 'docs/adr/' + sub;
|
|
44
|
+
r.adrDirs.push(rel);
|
|
45
|
+
r.adrCount += countMD(path.join(rootDir, rel));
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
r.adrDirs = ['docs/adr'];
|
|
49
|
+
r.adrCount = countMD(adrRoot);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Roadmap dir e namespacing
|
|
54
|
+
const roadmapRoot = path.join(rootDir, 'docs', 'roadmaps');
|
|
55
|
+
if (isDir(roadmapRoot)) {
|
|
56
|
+
r.roadmapDir = 'docs/roadmaps';
|
|
57
|
+
const agentDirs = listSubDirs(roadmapRoot);
|
|
58
|
+
let byAgent = false;
|
|
59
|
+
const agents = [];
|
|
60
|
+
for (const sub of agentDirs) {
|
|
61
|
+
const wipDir = path.join(roadmapRoot, sub, 'wip');
|
|
62
|
+
const backlogDir = path.join(roadmapRoot, sub, 'backlog');
|
|
63
|
+
const doneDir = path.join(roadmapRoot, sub, 'done');
|
|
64
|
+
const abandonedDir = path.join(roadmapRoot, sub, 'abandoned');
|
|
65
|
+
const blockedDir = path.join(roadmapRoot, sub, 'blocked');
|
|
66
|
+
if (isDir(wipDir) || isDir(backlogDir) || isDir(doneDir) || isDir(abandonedDir) || isDir(blockedDir)) {
|
|
67
|
+
byAgent = true;
|
|
68
|
+
agents.push(sub);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (byAgent) {
|
|
72
|
+
r.roadmapNamespacing = 'by_agent';
|
|
73
|
+
r.agents = agents;
|
|
74
|
+
for (const agent of agents) {
|
|
75
|
+
for (const state of ['backlog', 'wip', 'blocked', 'done', 'abandoned']) {
|
|
76
|
+
r.roadmapCount += countMD(path.join(roadmapRoot, agent, state));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
r.roadmapNamespacing = 'flat';
|
|
81
|
+
for (const state of ['backlog', 'wip', 'blocked', 'done', 'abandoned']) {
|
|
82
|
+
r.roadmapCount += countMD(path.join(roadmapRoot, state));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
r.hasTrackfwLog = fs.existsSync(path.join(roadmapRoot, '.trackfw-log'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Hook framework
|
|
90
|
+
if (isFile(path.join(rootDir, 'lefthook.yml')) || isFile(path.join(rootDir, '.lefthook.yml'))) {
|
|
91
|
+
r.hookFramework = 'lefthook';
|
|
92
|
+
} else if (isDir(path.join(rootDir, '.husky'))) {
|
|
93
|
+
r.hookFramework = 'husky';
|
|
94
|
+
} else if (isFile(path.join(rootDir, '.pre-commit-config.yaml'))) {
|
|
95
|
+
r.hookFramework = 'pre-commit';
|
|
96
|
+
} else {
|
|
97
|
+
r.hookFramework = 'none';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// CI
|
|
101
|
+
if (isDir(path.join(rootDir, '.github', 'workflows'))) {
|
|
102
|
+
r.ciSystem = 'github-actions';
|
|
103
|
+
} else if (isFile(path.join(rootDir, '.gitlab-ci.yml'))) {
|
|
104
|
+
r.ciSystem = 'gitlab';
|
|
105
|
+
} else {
|
|
106
|
+
r.ciSystem = 'none';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
r.governanceScore = calcScore(r);
|
|
110
|
+
return r;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function calcScore(r) {
|
|
114
|
+
let score = 0;
|
|
115
|
+
if (r.adrCount > 0) score += 20;
|
|
116
|
+
if (r.reqCount > 0) score += 20;
|
|
117
|
+
if (r.roadmapCount > 0) score += 20;
|
|
118
|
+
if (r.hasTrackfwYAML) score += 20;
|
|
119
|
+
if (r.hasTrackfwLog) score += 20;
|
|
120
|
+
return score;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function generateYAML(r) {
|
|
124
|
+
let out = '# trackfw configuration — gerado por trackfw discover\n';
|
|
125
|
+
out += '# governance_mode: lenient permite validação não-bloqueante durante onboarding\n\n';
|
|
126
|
+
out += 'governance_mode: lenient\n\n';
|
|
127
|
+
|
|
128
|
+
if (r.adrDirs.length > 0) {
|
|
129
|
+
out += 'adr_dirs:\n';
|
|
130
|
+
r.adrDirs.forEach(d => { out += ` - ${d}\n`; });
|
|
131
|
+
} else {
|
|
132
|
+
out += 'adr_dirs:\n - docs/adr\n';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
out += `req_dir: ${r.reqDir || 'docs/req'}\n`;
|
|
136
|
+
out += `roadmap_dir: ${r.roadmapDir || 'docs/roadmaps'}\n`;
|
|
137
|
+
out += `roadmap_namespacing: ${r.roadmapNamespacing}\n`;
|
|
138
|
+
|
|
139
|
+
if (r.agents.length > 0) {
|
|
140
|
+
out += 'agents:\n';
|
|
141
|
+
r.agents.forEach(a => { out += ` - ${a}\n`; });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
out += `hooks: ${r.hookFramework}\n`;
|
|
145
|
+
out += `ci: ${r.ciSystem}\n`;
|
|
146
|
+
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function generateBootstrapLog(r, rootDir) {
|
|
151
|
+
let out = '';
|
|
152
|
+
const roadmapRoot = path.join(rootDir, r.roadmapDir);
|
|
153
|
+
|
|
154
|
+
const appendEntries = (dir, agent) => {
|
|
155
|
+
if (!isDir(dir)) return;
|
|
156
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
157
|
+
if (!entry.endsWith('.md')) continue;
|
|
158
|
+
const filePath = path.join(dir, entry);
|
|
159
|
+
const stat = fs.statSync(filePath);
|
|
160
|
+
const ts = stat.mtime.toISOString().slice(0, 16).replace('T', ' ');
|
|
161
|
+
const basename = agent ? agent + '/' + entry : entry;
|
|
162
|
+
out += `${ts} ${basename.padEnd(50)} backlog → done\n`;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (r.roadmapNamespacing === 'by_agent') {
|
|
167
|
+
for (const agent of r.agents) {
|
|
168
|
+
appendEntries(path.join(roadmapRoot, agent, 'done'), agent);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
appendEntries(path.join(roadmapRoot, 'done'), '');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// installGates instala artefatos de governança: validate script, hook entry, CI workflow.
|
|
178
|
+
function installGates(r, rootDir) {
|
|
179
|
+
writeValidateScript(rootDir);
|
|
180
|
+
installHook(r.hookFramework, rootDir);
|
|
181
|
+
if (r.ciSystem === 'github-actions') {
|
|
182
|
+
writeCIWorkflow(rootDir);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function writeValidateScript(rootDir) {
|
|
187
|
+
const scriptsDir = path.join(rootDir, 'scripts');
|
|
188
|
+
if (!isDir(scriptsDir)) fs.mkdirSync(scriptsDir, { recursive: true });
|
|
189
|
+
const content = '#!/usr/bin/env bash\nset -euo pipefail\ntrackfw validate\n';
|
|
190
|
+
const dest = path.join(scriptsDir, 'trackfw-validate.sh');
|
|
191
|
+
fs.writeFileSync(dest, content, { mode: 0o755 });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function installHook(framework, rootDir) {
|
|
195
|
+
const hookEntry = '\npre-commit:\n commands:\n trackfw-validate:\n run: scripts/trackfw-validate.sh\n';
|
|
196
|
+
const huskyEntry = '\nscripts/trackfw-validate.sh\n';
|
|
197
|
+
|
|
198
|
+
if (framework === 'lefthook') {
|
|
199
|
+
let cfgPath = path.join(rootDir, 'lefthook.yml');
|
|
200
|
+
if (!isFile(cfgPath)) cfgPath = path.join(rootDir, '.lefthook.yml');
|
|
201
|
+
const content = fs.readFileSync(cfgPath, 'utf8');
|
|
202
|
+
if (content.includes('trackfw')) return; // idempotente
|
|
203
|
+
fs.appendFileSync(cfgPath, hookEntry, 'utf8');
|
|
204
|
+
} else if (framework === 'husky') {
|
|
205
|
+
const huskyHook = path.join(rootDir, '.husky', 'pre-commit');
|
|
206
|
+
fs.appendFileSync(huskyHook, huskyEntry, 'utf8');
|
|
207
|
+
} else {
|
|
208
|
+
console.log('⚠ No hook framework detected — skipping hook installation');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function writeCIWorkflow(rootDir) {
|
|
213
|
+
const workflowsDir = path.join(rootDir, '.github', 'workflows');
|
|
214
|
+
if (!isDir(workflowsDir)) fs.mkdirSync(workflowsDir, { recursive: true });
|
|
215
|
+
const dest = path.join(workflowsDir, 'trackfw-validate.yml');
|
|
216
|
+
if (isFile(dest)) return; // idempotente
|
|
217
|
+
const content = `name: trackfw validate
|
|
218
|
+
on: [push, pull_request]
|
|
219
|
+
jobs:
|
|
220
|
+
governance:
|
|
221
|
+
runs-on: ubuntu-latest
|
|
222
|
+
steps:
|
|
223
|
+
- uses: actions/checkout@v4
|
|
224
|
+
- uses: actions/setup-go@v5
|
|
225
|
+
with:
|
|
226
|
+
go-version: "1.22"
|
|
227
|
+
- run: go install github.com/kgsaran/trackfw/cmd/trackfw@latest
|
|
228
|
+
- run: trackfw validate
|
|
229
|
+
`;
|
|
230
|
+
fs.writeFileSync(dest, content, 'utf8');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// helpers
|
|
234
|
+
function isDir(p) {
|
|
235
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isFile(p) {
|
|
239
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function countMD(dir) {
|
|
243
|
+
let n = 0;
|
|
244
|
+
function walk(d) {
|
|
245
|
+
let entries;
|
|
246
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
247
|
+
for (const e of entries) {
|
|
248
|
+
if (e.isDirectory()) walk(path.join(d, e.name));
|
|
249
|
+
else if (e.name.endsWith('.md')) n++;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
walk(dir);
|
|
253
|
+
return n;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function listSubDirs(dir) {
|
|
257
|
+
try {
|
|
258
|
+
return fs.readdirSync(dir).filter(f => {
|
|
259
|
+
try { return fs.statSync(path.join(dir, f)).isDirectory(); } catch { return false; }
|
|
260
|
+
});
|
|
261
|
+
} catch { return []; }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const cmd = new Command('discover');
|
|
265
|
+
cmd.description('Scan the repository and auto-detect the governance structure');
|
|
266
|
+
cmd.option('--init', 'generate trackfw.yaml calibrated for this project');
|
|
267
|
+
cmd.option('--bootstrap-log', 'create retroactive .trackfw-log from done/ files');
|
|
268
|
+
cmd.action((opts) => {
|
|
269
|
+
const cwd = process.cwd();
|
|
270
|
+
console.log(`trackfw discover — scanning ${cwd}\n`);
|
|
271
|
+
|
|
272
|
+
const r = scan(cwd);
|
|
273
|
+
|
|
274
|
+
// ADR dirs
|
|
275
|
+
if (r.adrCount > 0) {
|
|
276
|
+
const dirs = r.adrDirs.join(', ');
|
|
277
|
+
console.log(`✓ ADRs found: ${String(r.adrCount).padEnd(4)} (${dirs})`);
|
|
278
|
+
} else {
|
|
279
|
+
console.log('⚠ No ADRs found');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// REQ dir
|
|
283
|
+
if (r.reqCount > 0) {
|
|
284
|
+
console.log(`✓ REQs found: ${String(r.reqCount).padEnd(4)} (${r.reqDir})`);
|
|
285
|
+
} else {
|
|
286
|
+
console.log('⚠ No REQs found');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Roadmaps
|
|
290
|
+
if (r.roadmapCount > 0) {
|
|
291
|
+
const mode = r.roadmapNamespacing === 'by_agent' ? 'by_agent mode' : r.roadmapNamespacing;
|
|
292
|
+
console.log(`✓ Roadmaps found: ${String(r.roadmapCount).padEnd(4)} (${r.roadmapDir} — ${mode})`);
|
|
293
|
+
} else {
|
|
294
|
+
console.log('⚠ No roadmaps found');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Agents
|
|
298
|
+
if (r.agents.length > 0) {
|
|
299
|
+
console.log(`✓ Agents detected: ${r.agents.join(', ')}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// trackfw.yaml
|
|
303
|
+
if (!r.hasTrackfwYAML) {
|
|
304
|
+
console.log('⚠ No trackfw.yaml — run with --init to generate one');
|
|
305
|
+
} else {
|
|
306
|
+
console.log('✓ trackfw.yaml found');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// .trackfw-log
|
|
310
|
+
if (!r.hasTrackfwLog) {
|
|
311
|
+
console.log('⚠ No .trackfw-log — run with --bootstrap-log to create retroactive history');
|
|
312
|
+
} else {
|
|
313
|
+
console.log('✓ .trackfw-log found');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// hooks
|
|
317
|
+
if (r.hookFramework !== 'none') {
|
|
318
|
+
console.log(`✓ Hooks: ${r.hookFramework}`);
|
|
319
|
+
} else {
|
|
320
|
+
console.log('⚠ No hook framework detected');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// CI
|
|
324
|
+
if (r.ciSystem !== 'none') {
|
|
325
|
+
console.log(`✓ CI: ${r.ciSystem}`);
|
|
326
|
+
} else {
|
|
327
|
+
console.log('⚠ No CI system detected');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log(`\nGovernance Score: ${r.governanceScore}/100`);
|
|
331
|
+
|
|
332
|
+
if (opts.init) {
|
|
333
|
+
const yaml = generateYAML(r);
|
|
334
|
+
fs.writeFileSync('trackfw.yaml', yaml, 'utf8');
|
|
335
|
+
console.log('\n✓ trackfw.yaml generated');
|
|
336
|
+
try {
|
|
337
|
+
installGates(r, cwd);
|
|
338
|
+
console.log('✓ governance gates installed');
|
|
339
|
+
} catch (e) {
|
|
340
|
+
console.log(`⚠ gates install partial: ${e.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (opts.bootstrapLog) {
|
|
345
|
+
if (!r.roadmapDir) {
|
|
346
|
+
console.error('⚠ No roadmap dir detected — cannot bootstrap log');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const logContent = generateBootstrapLog(r, cwd);
|
|
350
|
+
const logPath = r.roadmapDir + '/.trackfw-log';
|
|
351
|
+
fs.appendFileSync(logPath, logContent, 'utf8');
|
|
352
|
+
console.log(`✓ bootstrap log written to ${logPath}`);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
module.exports = cmd;
|
|
357
|
+
module.exports.scan = scan;
|
|
358
|
+
module.exports.generateYAML = generateYAML;
|
|
359
|
+
module.exports.generateBootstrapLog = generateBootstrapLog;
|
package/src/commands/index.js
CHANGED
|
@@ -18,6 +18,7 @@ function createProgram() {
|
|
|
18
18
|
program.addCommand(require('./status'))
|
|
19
19
|
program.addCommand(require('./log'))
|
|
20
20
|
program.addCommand(require('./plugins'))
|
|
21
|
+
program.addCommand(require('./discover'))
|
|
21
22
|
|
|
22
23
|
// plugin dispatch — comandos desconhecidos tentam executar plugin
|
|
23
24
|
program.hook('preSubcommand', () => {})
|
package/src/commands/init.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const { Command } = require('commander')
|
|
3
|
+
const { t } = require('../i18n')
|
|
3
4
|
|
|
4
5
|
const cmd = new Command('init')
|
|
5
|
-
cmd.description('
|
|
6
|
+
cmd.description(t('init.description'))
|
|
6
7
|
cmd.action(async () => {
|
|
7
8
|
const path = require('path')
|
|
8
9
|
const generators = require('../generators/init')
|
|
@@ -19,27 +20,27 @@ cmd.action(async () => {
|
|
|
19
20
|
ci: 'none',
|
|
20
21
|
}
|
|
21
22
|
await generators.scaffold(cfg)
|
|
22
|
-
console.log(
|
|
23
|
+
console.log(`\n${t('init.success')}`)
|
|
23
24
|
return
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const { input, select, checkbox } = require('@inquirer/prompts')
|
|
27
28
|
|
|
28
|
-
let projectName, projectType, frontend, pkgManager, backend, hooks, ci, aiTools
|
|
29
|
+
let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, aiTools
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
32
|
projectName = await input({
|
|
32
|
-
message: '
|
|
33
|
+
message: t('init.prompt.projectName'),
|
|
33
34
|
default: path.basename(process.cwd()),
|
|
34
35
|
})
|
|
35
36
|
|
|
36
37
|
projectType = await select({
|
|
37
|
-
message: '
|
|
38
|
+
message: t('init.prompt.projectType'),
|
|
38
39
|
choices: [
|
|
39
|
-
{ name: '
|
|
40
|
-
{ name: '
|
|
41
|
-
{ name: '
|
|
42
|
-
{ name: '
|
|
40
|
+
{ name: t('init.prompt.projectType_fullstack'), value: 'fullstack' },
|
|
41
|
+
{ name: t('init.prompt.projectType_frontend'), value: 'frontend' },
|
|
42
|
+
{ name: t('init.prompt.projectType_backend'), value: 'backend' },
|
|
43
|
+
{ name: t('init.prompt.projectType_governance'), value: 'governance' },
|
|
43
44
|
],
|
|
44
45
|
})
|
|
45
46
|
|
|
@@ -47,7 +48,7 @@ cmd.action(async () => {
|
|
|
47
48
|
pkgManager = ''
|
|
48
49
|
if (projectType === 'fullstack' || projectType === 'frontend') {
|
|
49
50
|
frontend = await select({
|
|
50
|
-
message: '
|
|
51
|
+
message: t('init.prompt.frontendStack'),
|
|
51
52
|
choices: [
|
|
52
53
|
{ name: 'React / Next.js', value: 'react' },
|
|
53
54
|
{ name: 'Vue', value: 'vue' },
|
|
@@ -55,7 +56,7 @@ cmd.action(async () => {
|
|
|
55
56
|
],
|
|
56
57
|
})
|
|
57
58
|
pkgManager = await select({
|
|
58
|
-
message: '
|
|
59
|
+
message: t('init.prompt.pkgManager'),
|
|
59
60
|
choices: [
|
|
60
61
|
{ name: 'npm', value: 'npm' },
|
|
61
62
|
{ name: 'pnpm', value: 'pnpm' },
|
|
@@ -66,20 +67,50 @@ cmd.action(async () => {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
backend = ''
|
|
70
|
+
let backendFramework = ''
|
|
69
71
|
if (projectType === 'fullstack' || projectType === 'backend') {
|
|
70
72
|
backend = await select({
|
|
71
|
-
message: '
|
|
73
|
+
message: t('init.prompt.backendLang'),
|
|
72
74
|
choices: [
|
|
73
75
|
{ name: 'Go', value: 'go' },
|
|
74
|
-
{ name: 'Java
|
|
76
|
+
{ name: 'Java', value: 'java' },
|
|
75
77
|
{ name: 'Node.js', value: 'node' },
|
|
76
78
|
{ name: 'Python', value: 'python' },
|
|
77
79
|
],
|
|
78
80
|
})
|
|
81
|
+
|
|
82
|
+
const frameworkChoices = {
|
|
83
|
+
go: [
|
|
84
|
+
{ name: 'Gin', value: 'gin' },
|
|
85
|
+
{ name: 'Echo', value: 'echo' },
|
|
86
|
+
{ name: 'Fiber', value: 'fiber' },
|
|
87
|
+
{ name: 'Standard library (net/http)', value: 'stdlib' },
|
|
88
|
+
],
|
|
89
|
+
java: [
|
|
90
|
+
{ name: 'Spring Boot', value: 'spring-boot' },
|
|
91
|
+
{ name: 'Quarkus', value: 'quarkus' },
|
|
92
|
+
{ name: 'Micronaut', value: 'micronaut' },
|
|
93
|
+
],
|
|
94
|
+
node: [
|
|
95
|
+
{ name: 'Express', value: 'express' },
|
|
96
|
+
{ name: 'Fastify', value: 'fastify' },
|
|
97
|
+
{ name: 'NestJS', value: 'nestjs' },
|
|
98
|
+
{ name: 'Koa', value: 'koa' },
|
|
99
|
+
],
|
|
100
|
+
python: [
|
|
101
|
+
{ name: 'FastAPI', value: 'fastapi' },
|
|
102
|
+
{ name: 'Django', value: 'django' },
|
|
103
|
+
{ name: 'Flask', value: 'flask' },
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
backendFramework = await select({
|
|
107
|
+
message: t('init.prompt.backendFramework'),
|
|
108
|
+
choices: frameworkChoices[backend] || [],
|
|
109
|
+
})
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
hooks = await select({
|
|
82
|
-
message: '
|
|
113
|
+
message: t('init.prompt.gitHooks'),
|
|
83
114
|
choices: [
|
|
84
115
|
{ name: 'husky', value: 'husky' },
|
|
85
116
|
{ name: 'lefthook', value: 'lefthook' },
|
|
@@ -88,7 +119,7 @@ cmd.action(async () => {
|
|
|
88
119
|
})
|
|
89
120
|
|
|
90
121
|
ci = await select({
|
|
91
|
-
message: '
|
|
122
|
+
message: t('init.prompt.ci'),
|
|
92
123
|
choices: [
|
|
93
124
|
{ name: 'GitHub Actions', value: 'github-actions' },
|
|
94
125
|
{ name: 'GitLab CI', value: 'gitlab-ci' },
|
|
@@ -97,7 +128,7 @@ cmd.action(async () => {
|
|
|
97
128
|
})
|
|
98
129
|
|
|
99
130
|
aiTools = await checkbox({
|
|
100
|
-
message: '
|
|
131
|
+
message: t('init.prompt.aiTools'),
|
|
101
132
|
choices: [
|
|
102
133
|
{ name: 'Claude Code', value: 'claude' },
|
|
103
134
|
{ name: 'Gemini CLI', value: 'gemini' },
|
|
@@ -119,11 +150,11 @@ cmd.action(async () => {
|
|
|
119
150
|
ci: 'none',
|
|
120
151
|
}
|
|
121
152
|
await generators.scaffold(cfg)
|
|
122
|
-
console.log(
|
|
153
|
+
console.log(`\n${t('init.success')}`)
|
|
123
154
|
return
|
|
124
155
|
}
|
|
125
156
|
|
|
126
|
-
const cfg = { projectName, projectType, frontend, backend, pkgManager, hooks, ci }
|
|
157
|
+
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci }
|
|
127
158
|
await generators.scaffold(cfg)
|
|
128
159
|
|
|
129
160
|
for (const tool of (aiTools || [])) {
|
|
@@ -137,7 +168,7 @@ cmd.action(async () => {
|
|
|
137
168
|
}
|
|
138
169
|
}
|
|
139
170
|
|
|
140
|
-
console.log(
|
|
171
|
+
console.log(`\n${t('init.success')}`)
|
|
141
172
|
})
|
|
142
173
|
|
|
143
174
|
module.exports = cmd
|
package/src/commands/log.js
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
const { Command } = require('commander')
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
|
+
const { t } = require('../i18n')
|
|
5
6
|
|
|
6
7
|
const cmd = new Command('log')
|
|
7
|
-
cmd.description('
|
|
8
|
-
cmd.option('--tail <n>', '
|
|
8
|
+
cmd.description(t('log.description'))
|
|
9
|
+
cmd.option('--tail <n>', t('log.tail'), '20')
|
|
9
10
|
cmd.action(async (opts) => {
|
|
10
11
|
const tail = parseInt(opts.tail, 10)
|
|
11
12
|
const logPath = path.join('docs', 'roadmaps', '.trackfw-log')
|
|
12
13
|
|
|
13
14
|
if (!fs.existsSync(logPath)) {
|
|
14
|
-
console.log('
|
|
15
|
+
console.log(t('log.empty'))
|
|
15
16
|
return
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -22,7 +23,7 @@ cmd.action(async (opts) => {
|
|
|
22
23
|
const start = Math.max(0, lines.length - tail)
|
|
23
24
|
const visible = lines.slice(start)
|
|
24
25
|
|
|
25
|
-
console.log('
|
|
26
|
+
console.log(t('log.header'))
|
|
26
27
|
visible.forEach(l => console.log(l))
|
|
27
28
|
})
|
|
28
29
|
|