trackfw 1.1.0 → 2.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/package.json +1 -1
- package/src/commands/adr.js +1 -1
- package/src/commands/context.js +189 -0
- package/src/commands/discover.js +359 -0
- package/src/commands/index.js +4 -0
- package/src/commands/init.js +11 -2
- package/src/commands/metrics.js +235 -0
- package/src/commands/plugins.js +73 -0
- package/src/commands/req.js +1 -1
- package/src/commands/roadmap.js +6 -1
- package/src/commands/sync.js +362 -0
- package/src/commands/validate.js +9 -1
- package/src/config/index.js +92 -0
- package/src/generators/adr.js +20 -7
- package/src/generators/init.js +51 -1
- package/src/generators/req.js +12 -3
- package/src/generators/roadmap.js +292 -58
- package/src/i18n/locales/en-US.json +9 -1
- package/src/i18n/locales/es-ES.json +9 -1
- package/src/i18n/locales/pt-BR.json +9 -1
- package/src/validator/index.js +369 -97
package/package.json
CHANGED
package/src/commands/adr.js
CHANGED
|
@@ -25,7 +25,7 @@ cmd.command('new <title>')
|
|
|
25
25
|
cmd.command('list')
|
|
26
26
|
.description(t('adr.list.description'))
|
|
27
27
|
.action(async () => {
|
|
28
|
-
await generators.listADRs('
|
|
28
|
+
await generators.listADRs(require('../config').load().adrDirs[0])
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
module.exports = cmd
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const config = require('../config')
|
|
7
|
+
const { validate } = require('../validator')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* extractFrontmatterField — extrai valor de campo YAML dentro de bloco --- ... ---.
|
|
11
|
+
* Retorna string vazia se não encontrado ou valor '""'.
|
|
12
|
+
* @param {string} content
|
|
13
|
+
* @param {string} field
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function extractFrontmatterField(content, field) {
|
|
17
|
+
const lines = content.split('\n')
|
|
18
|
+
let started = false
|
|
19
|
+
let inFrontmatter = false
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const trimmed = line.trim()
|
|
22
|
+
if (trimmed === '---') {
|
|
23
|
+
if (!started) {
|
|
24
|
+
started = true
|
|
25
|
+
inFrontmatter = true
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
break // segundo --- fecha o bloco
|
|
29
|
+
}
|
|
30
|
+
if (!inFrontmatter) break
|
|
31
|
+
const key = field + ':'
|
|
32
|
+
if (trimmed.startsWith(key)) {
|
|
33
|
+
let val = trimmed.slice(key.length).trim()
|
|
34
|
+
val = val.replace(/^["']|["']$/g, '') // remover aspas
|
|
35
|
+
return val
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* extractInlineStatus — extrai status da linha "| Status: ..." do markdown.
|
|
43
|
+
* @param {string} content
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function extractInlineStatus(content) {
|
|
47
|
+
for (const line of content.split('\n')) {
|
|
48
|
+
const idx = line.indexOf('| Status: ')
|
|
49
|
+
if (idx >= 0) {
|
|
50
|
+
let rest = line.slice(idx + '| Status: '.length)
|
|
51
|
+
const pipeIdx = rest.indexOf(' |')
|
|
52
|
+
if (pipeIdx >= 0) rest = rest.slice(0, pipeIdx)
|
|
53
|
+
rest = rest.replace(/[\s>|]+$/, '').trim()
|
|
54
|
+
return rest || 'unknown'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 'unknown'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* collectEntries — lê diretório e retorna lista de entradas com type, file, status, state.
|
|
62
|
+
* @param {string} dir
|
|
63
|
+
* @param {string} type - 'ADR' | 'REQ' | 'ROADMAP'
|
|
64
|
+
* @param {string} [state] - estado kanban (somente ROADMAP)
|
|
65
|
+
* @returns {Array<{type: string, file: string, status: string, state?: string}>}
|
|
66
|
+
*/
|
|
67
|
+
function collectEntries(dir, type, state) {
|
|
68
|
+
const entries = []
|
|
69
|
+
let files = []
|
|
70
|
+
try {
|
|
71
|
+
files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && !fs.statSync(path.join(dir, f)).isDirectory())
|
|
72
|
+
} catch (_) {
|
|
73
|
+
return entries
|
|
74
|
+
}
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
let content = ''
|
|
77
|
+
try { content = fs.readFileSync(path.join(dir, file), 'utf8') } catch (_) {}
|
|
78
|
+
let status = extractFrontmatterField(content, 'status')
|
|
79
|
+
if (!status) status = extractInlineStatus(content)
|
|
80
|
+
if (!status) status = state || 'unknown'
|
|
81
|
+
const entry = { type, file, status }
|
|
82
|
+
if (state) entry.state = state
|
|
83
|
+
entries.push(entry)
|
|
84
|
+
}
|
|
85
|
+
return entries
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* getContext — coleta governança e imprime em md ou json.
|
|
90
|
+
* @param {string} format - 'md' | 'json'
|
|
91
|
+
*/
|
|
92
|
+
function getContext(format) {
|
|
93
|
+
const cfg = config.load()
|
|
94
|
+
|
|
95
|
+
// ADRs
|
|
96
|
+
const adrs = []
|
|
97
|
+
for (const adrDir of (cfg.adrDirs || ['docs/adr'])) {
|
|
98
|
+
adrs.push(...collectEntries(adrDir, 'ADR'))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// REQs
|
|
102
|
+
const reqs = collectEntries(cfg.reqDir || 'docs/req', 'REQ')
|
|
103
|
+
|
|
104
|
+
// Roadmaps
|
|
105
|
+
const roadmaps = []
|
|
106
|
+
const states = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
107
|
+
if (cfg.roadmapNamespacing === 'by_agent') {
|
|
108
|
+
let agents = cfg.agents || []
|
|
109
|
+
if (agents.length === 0) {
|
|
110
|
+
try {
|
|
111
|
+
agents = fs.readdirSync(cfg.roadmapDir).filter(f => {
|
|
112
|
+
try { return fs.statSync(path.join(cfg.roadmapDir, f)).isDirectory() } catch (_) { return false }
|
|
113
|
+
})
|
|
114
|
+
} catch (_) { agents = [] }
|
|
115
|
+
}
|
|
116
|
+
for (const agent of agents) {
|
|
117
|
+
for (const state of states) {
|
|
118
|
+
const dir = path.join(cfg.roadmapDir, agent, state)
|
|
119
|
+
roadmaps.push(...collectEntries(dir, 'ROADMAP', state))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
for (const state of states) {
|
|
124
|
+
const dir = path.join(cfg.roadmapDir, state)
|
|
125
|
+
roadmaps.push(...collectEntries(dir, 'ROADMAP', state))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Validate
|
|
130
|
+
const { violations, warnings } = validate()
|
|
131
|
+
|
|
132
|
+
// Score
|
|
133
|
+
let score = 0
|
|
134
|
+
if (adrs.length > 0) score += 20
|
|
135
|
+
if (reqs.length > 0) score += 20
|
|
136
|
+
if (roadmaps.length > 0) score += 20
|
|
137
|
+
if (violations.length === 0) score += 40
|
|
138
|
+
|
|
139
|
+
if (format === 'json') {
|
|
140
|
+
console.log(JSON.stringify({ score, violations, warnings, adrs, reqs, roadmaps }, null, 2))
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Markdown
|
|
145
|
+
console.log('# trackfw governance context\n')
|
|
146
|
+
console.log(`**Governance score:** ${score}/100\n`)
|
|
147
|
+
|
|
148
|
+
console.log(`## ADRs (${adrs.length})`)
|
|
149
|
+
if (adrs.length === 0) {
|
|
150
|
+
console.log('- (none)')
|
|
151
|
+
} else {
|
|
152
|
+
for (const a of adrs) console.log(`- ${a.file} [${a.status}]`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`\n## REQs (${reqs.length})`)
|
|
156
|
+
if (reqs.length === 0) {
|
|
157
|
+
console.log('- (none)')
|
|
158
|
+
} else {
|
|
159
|
+
for (const r of reqs) console.log(`- ${r.file} [${r.status}]`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`\n## Roadmaps (${roadmaps.length})`)
|
|
163
|
+
if (roadmaps.length === 0) {
|
|
164
|
+
console.log('- (none)')
|
|
165
|
+
} else {
|
|
166
|
+
for (const r of roadmaps) console.log(`- ${r.file} [${r.state}]`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (violations.length > 0) {
|
|
170
|
+
console.log(`\n## Violations (${violations.length})`)
|
|
171
|
+
for (const v of violations) console.log(`- ${v}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (warnings.length > 0) {
|
|
175
|
+
console.log(`\n## Warnings (${warnings.length})`)
|
|
176
|
+
for (const w of warnings) console.log(`- ${w}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = (function () {
|
|
181
|
+
const cmd = new Command('context')
|
|
182
|
+
cmd
|
|
183
|
+
.description('Print governance context for LLM consumption')
|
|
184
|
+
.option('--format <fmt>', 'Output format: md or json', 'md')
|
|
185
|
+
.action((opts) => {
|
|
186
|
+
getContext(opts.format)
|
|
187
|
+
})
|
|
188
|
+
return cmd
|
|
189
|
+
})()
|
|
@@ -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,10 @@ 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'))
|
|
22
|
+
program.addCommand(require('./metrics'))
|
|
23
|
+
program.addCommand(require('./sync'))
|
|
24
|
+
program.addCommand(require('./context'))
|
|
21
25
|
|
|
22
26
|
// plugin dispatch — comandos desconhecidos tentam executar plugin
|
|
23
27
|
program.hook('preSubcommand', () => {})
|
package/src/commands/init.js
CHANGED
|
@@ -26,7 +26,7 @@ cmd.action(async () => {
|
|
|
26
26
|
|
|
27
27
|
const { input, select, checkbox } = require('@inquirer/prompts')
|
|
28
28
|
|
|
29
|
-
let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, aiTools
|
|
29
|
+
let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, aiTools, requireReqInCommit
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
32
|
projectName = await input({
|
|
@@ -127,6 +127,15 @@ cmd.action(async () => {
|
|
|
127
127
|
],
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
+
requireReqInCommit = false
|
|
131
|
+
if (hooks !== 'none') {
|
|
132
|
+
const { confirm: confirmPrompt } = require('@inquirer/prompts')
|
|
133
|
+
requireReqInCommit = await confirmPrompt({
|
|
134
|
+
message: t('init.prompt.require_req_in_commit'),
|
|
135
|
+
default: false,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
aiTools = await checkbox({
|
|
131
140
|
message: t('init.prompt.aiTools'),
|
|
132
141
|
choices: [
|
|
@@ -154,7 +163,7 @@ cmd.action(async () => {
|
|
|
154
163
|
return
|
|
155
164
|
}
|
|
156
165
|
|
|
157
|
-
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci }
|
|
166
|
+
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
|
|
158
167
|
await generators.scaffold(cfg)
|
|
159
168
|
|
|
160
169
|
for (const tool of (aiTools || [])) {
|