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.
@@ -3,6 +3,7 @@ const { Command } = require('commander')
3
3
  const os = require('os')
4
4
  const path = require('path')
5
5
  const fs = require('fs')
6
+ const { t } = require('../i18n')
6
7
 
7
8
  function pluginsDir() {
8
9
  return path.join(os.homedir(), '.trackfw', 'plugins')
@@ -40,7 +41,7 @@ async function installPlugin(repo) {
40
41
  : `https://github.com/${base}/releases/download/${tag}/${assetName}`
41
42
 
42
43
  const res = await fetch(url)
43
- if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`)
44
+ if (!res.ok) throw new Error(t('errors.downloadFailed', { status: res.status, url }))
44
45
 
45
46
  const dir = pluginsDir()
46
47
  fs.mkdirSync(dir, { recursive: true })
@@ -49,32 +50,32 @@ async function installPlugin(repo) {
49
50
 
50
51
  function removePlugin(name) {
51
52
  const filePath = path.join(pluginsDir(), name)
52
- if (!fs.existsSync(filePath)) throw new Error(`plugin "${name}" not found`)
53
+ if (!fs.existsSync(filePath)) throw new Error(t('errors.pluginNotFound', { name }))
53
54
  fs.unlinkSync(filePath)
54
55
  }
55
56
 
56
57
  const cmd = new Command('plugins')
57
- cmd.description('Manage trackfw plugins')
58
+ cmd.description(t('plugins.description'))
58
59
 
59
60
  cmd.command('list')
60
- .description('List installed plugins')
61
+ .description(t('plugins.list.description'))
61
62
  .action(() => {
62
63
  const plugins = listPlugins()
63
64
  if (plugins.length === 0) {
64
- console.log('No plugins installed. Use `trackfw plugins add <user/repo>` to install one.')
65
+ console.log(t('plugins.list.empty'))
65
66
  return
66
67
  }
67
68
  plugins.forEach(p => console.log(p))
68
69
  })
69
70
 
70
71
  cmd.command('add <repo>')
71
- .description('Install a plugin from GitHub Releases (user/repo or user/repo@tag)')
72
+ .description(t('plugins.add.description'))
72
73
  .action(async (repo) => {
73
74
  try {
74
- console.log(`Installing plugin from ${repo}...`)
75
+ console.log(t('plugins.add.installing', { repo }))
75
76
  await installPlugin(repo)
76
77
  const name = repo.split('@')[0].split('/').pop()
77
- console.log(`Plugin "${name}" installed successfully.`)
78
+ console.log(t('plugins.add.success', { name }))
78
79
  } catch (err) {
79
80
  console.error(`Error: ${err.message}`)
80
81
  process.exit(1)
@@ -82,11 +83,11 @@ cmd.command('add <repo>')
82
83
  })
83
84
 
84
85
  cmd.command('remove <name>')
85
- .description('Remove an installed plugin')
86
+ .description(t('plugins.remove.description'))
86
87
  .action((name) => {
87
88
  try {
88
89
  removePlugin(name)
89
- console.log(`Plugin "${name}" removed.`)
90
+ console.log(t('plugins.remove.success', { name }))
90
91
  } catch (err) {
91
92
  console.error(`Error: ${err.message}`)
92
93
  process.exit(1)
@@ -1,12 +1,13 @@
1
1
  'use strict'
2
2
  const { Command } = require('commander')
3
3
  const { listREQs } = require('../generators/req')
4
+ const { t } = require('../i18n')
4
5
 
5
6
  const cmd = new Command('req')
6
- cmd.description('Manage Requirements')
7
+ cmd.description(t('req.description'))
7
8
 
8
9
  cmd.command('new <title>')
9
- .description('Create a new REQ')
10
+ .description(t('req.new.description'))
10
11
  .action(async (title) => {
11
12
  const { input, select } = require('@inquirer/prompts')
12
13
  const generators = require('../generators/req')
@@ -16,14 +17,14 @@ cmd.command('new <title>')
16
17
 
17
18
  if (process.stdin.isTTY) {
18
19
  // Form 1 — título + motivação
19
- content.title = await input({ message: 'Project requirement', default: title })
20
- content.motivation = await input({ message: 'Motivation (why is this needed?)', default: '' })
20
+ content.title = await input({ message: t('req.new.prompt.title'), default: title })
21
+ content.motivation = await input({ message: t('req.new.prompt.motivation'), default: '' })
21
22
 
22
23
  // Detectar domínios com base em título + motivação
23
24
  const probes = generators.detectDomains(content.title + ' ' + content.motivation)
24
25
 
25
26
  // Form 2 — critérios de aceite
26
- content.criteria = await input({ message: 'Acceptance Criteria (one per line)', default: '- [ ]\n- [ ]' })
27
+ content.criteria = await input({ message: t('req.new.prompt.criteria'), default: '- [ ]\n- [ ]' })
27
28
 
28
29
  // Perguntas dinâmicas por probe
29
30
  const generatedADRs = []
@@ -42,7 +43,7 @@ cmd.command('new <title>')
42
43
  const basename = await adrGenerators.newADRDraft(answer)
43
44
  if (basename) generatedADRs.push(basename)
44
45
  } catch (e) {
45
- console.warn(`warning: could not create ADR draft for ${answer}: ${e.message}`)
46
+ console.warn(t('req.new.adrWarning', { slug: answer, message: e.message }))
46
47
  }
47
48
  }
48
49
  }
@@ -54,16 +55,16 @@ cmd.command('new <title>')
54
55
  await generators.newREQ(content)
55
56
 
56
57
  if (content.dependsOnADRs.length > 0) {
57
- console.log('\nADR drafts created:')
58
+ console.log(`\n${t('req.new.adrDraftsCreated')}`)
58
59
  content.dependsOnADRs.forEach(adr => console.log(` -> ${adr}`))
59
- console.log('\nResolve these ADRs (set Status: Accepted) before creating a roadmap.')
60
+ console.log(`\n${t('req.new.resolveADRs')}`)
60
61
  }
61
62
  })
62
63
 
63
64
  cmd.command('list')
64
- .description('List all REQs in docs/req/')
65
+ .description(t('req.list.description'))
65
66
  .action(async () => {
66
- listREQs('docs/req')
67
+ listREQs(require('../config').load().reqDir)
67
68
  })
68
69
 
69
70
  module.exports = cmd
@@ -1,12 +1,13 @@
1
1
  'use strict'
2
2
  const { Command } = require('commander')
3
3
  const { listRoadmaps, showRoadmap, moveRoadmap, newRoadmap } = require('../generators/roadmap')
4
+ const { t } = require('../i18n')
4
5
 
5
6
  const cmd = new Command('roadmap')
6
- cmd.description('Manage Roadmaps')
7
+ cmd.description(t('roadmap.description'))
7
8
 
8
9
  cmd.command('new')
9
- .description('Create a new roadmap from a REQ')
10
+ .description(t('roadmap.new.description'))
10
11
  .option('-t, --title <title>', 'Roadmap title')
11
12
  .option('-r, --req <path>', 'Path to the linked REQ')
12
13
  .action(async (opts) => {
@@ -16,19 +17,19 @@ cmd.command('new')
16
17
  })
17
18
 
18
19
  cmd.command('list')
19
- .description('List all roadmaps grouped by state')
20
+ .description(t('roadmap.list.description'))
20
21
  .action(async () => {
21
22
  listRoadmaps()
22
23
  })
23
24
 
24
25
  cmd.command('show <name>')
25
- .description('Show a roadmap by name (partial match)')
26
+ .description(t('roadmap.show.description'))
26
27
  .action(async (name) => {
27
28
  showRoadmap(name)
28
29
  })
29
30
 
30
31
  cmd.command('move <name> <state>')
31
- .description('Move a roadmap between states (backlog|wip|blocked|done|abandoned)')
32
+ .description(t('roadmap.move.description'))
32
33
  .action(async (name, state) => {
33
34
  moveRoadmap(name, state)
34
35
  })
@@ -1,9 +1,10 @@
1
1
  'use strict'
2
2
  const { Command } = require('commander')
3
3
  const { getStatus } = require('../validator')
4
+ const { t } = require('../i18n')
4
5
 
5
6
  const cmd = new Command('status')
6
- cmd.description('Show project governance status')
7
+ cmd.description(t('status.description'))
7
8
  cmd.action(async () => {
8
9
  console.log(await getStatus())
9
10
  })
@@ -1,24 +1,25 @@
1
1
  'use strict'
2
2
  const { Command } = require('commander')
3
3
  const { validate } = require('../validator')
4
+ const { t } = require('../i18n')
4
5
 
5
6
  const cmd = new Command('validate')
6
- cmd.description('Validate governance rules')
7
+ cmd.description(t('validate.description'))
7
8
  cmd.action(async () => {
8
9
  const { violations, warnings } = await validate()
9
10
 
10
11
  if (violations.length === 0 && warnings.length === 0) {
11
- console.log('✓ No violations found.')
12
+ console.log(t('validate.ok'))
12
13
  return
13
14
  }
14
15
 
15
16
  if (violations.length > 0) {
16
- console.log(`\n✗ Violations (${violations.length}):`)
17
+ console.log(`\n${t('validate.violations', { count: violations.length })}`)
17
18
  violations.forEach(v => console.log(` • ${v}`))
18
19
  }
19
20
 
20
21
  if (warnings.length > 0) {
21
- console.log(`\n⚠ Warnings (${warnings.length}):`)
22
+ console.log(`\n${t('validate.warnings', { count: warnings.length })}`)
22
23
  warnings.forEach(w => console.log(` • ${w}`))
23
24
  }
24
25
 
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function defaults() {
7
+ return {
8
+ adrDirs: ['docs/adr'],
9
+ reqDir: 'docs/req',
10
+ roadmapDir: 'docs/roadmaps',
11
+ roadmapNamespacing: 'flat',
12
+ agents: [],
13
+ governanceMode: '',
14
+ lenientUntil: '',
15
+ wipLimit: 1,
16
+ wipBySquad: false,
17
+ requireReqInCommit: false,
18
+ };
19
+ }
20
+
21
+ let _instance = null;
22
+
23
+ function load(cwd) {
24
+ if (_instance) return _instance;
25
+ _instance = defaults();
26
+ const yamlPath = path.join(cwd || process.cwd(), 'trackfw.yaml');
27
+ if (!fs.existsSync(yamlPath)) return _instance;
28
+ const content = fs.readFileSync(yamlPath, 'utf8');
29
+ parse(content, _instance);
30
+ return _instance;
31
+ }
32
+
33
+ function reset() {
34
+ _instance = null;
35
+ }
36
+
37
+ function parse(content, cfg) {
38
+ const lines = content.split('\n');
39
+ let inAdrDirs = false;
40
+ let inAgents = false;
41
+ let adrDirs = [];
42
+ let agents = [];
43
+
44
+ for (const rawLine of lines) {
45
+ const line = rawLine.trim();
46
+
47
+ if (inAdrDirs) {
48
+ if (line.startsWith('- ')) {
49
+ adrDirs.push(line.slice(2).trim());
50
+ continue;
51
+ }
52
+ inAdrDirs = false;
53
+ if (adrDirs.length) cfg.adrDirs = adrDirs;
54
+ }
55
+ if (inAgents) {
56
+ if (line.startsWith('- ')) {
57
+ agents.push(line.slice(2).trim());
58
+ continue;
59
+ }
60
+ inAgents = false;
61
+ if (agents.length) cfg.agents = agents;
62
+ }
63
+
64
+ const colonIdx = line.indexOf(':');
65
+ if (colonIdx < 0) continue;
66
+ const key = line.slice(0, colonIdx).trim();
67
+ const val = line.slice(colonIdx + 1).trim();
68
+ if (!key) continue;
69
+
70
+ switch (key) {
71
+ case 'adr_dirs': inAdrDirs = true; adrDirs = []; break;
72
+ case 'req_dir': cfg.reqDir = val; break;
73
+ case 'roadmap_dir': cfg.roadmapDir = val; break;
74
+ case 'roadmap_namespacing': cfg.roadmapNamespacing = val; break;
75
+ case 'agents': inAgents = true; agents = []; break;
76
+ case 'governance_mode': cfg.governanceMode = val; break;
77
+ case 'lenient_until': cfg.lenientUntil = val; break;
78
+ case 'wip_limit': { const n = parseInt(val, 10); if (n > 0) cfg.wipLimit = n; break; }
79
+ case 'wip_by_squad': cfg.wipBySquad = val === 'true'; break;
80
+ case 'require_req_in_commit': cfg.requireReqInCommit = val === 'true'; break;
81
+ }
82
+ }
83
+
84
+ // flush pending lists at EOF
85
+ if (inAdrDirs && adrDirs.length) cfg.adrDirs = adrDirs;
86
+ if (inAgents && agents.length) cfg.agents = agents;
87
+ }
88
+
89
+ const NAMESPACING_FLAT = 'flat'
90
+ const NAMESPACING_BY_AGENT = 'by_agent'
91
+
92
+ module.exports = { load, reset, defaults, NAMESPACING_FLAT, NAMESPACING_BY_AGENT };
@@ -40,11 +40,12 @@ function today() {
40
40
  * @returns {Promise<void>}
41
41
  */
42
42
  async function newADR(content) {
43
- fs.mkdirSync('docs/adr', { recursive: true })
43
+ const adrDir = require('../config').load().adrDirs[0]
44
+ fs.mkdirSync(adrDir, { recursive: true })
44
45
 
45
46
  const slug = toSlug(content.title)
46
47
  const date = today()
47
- const filename = `docs/adr/ADR-${date}-${slug}.md`
48
+ const filename = `${adrDir}/ADR-${date}-${slug}.md`
48
49
 
49
50
  const contextSection = content.context || '<!-- What is the situation that motivates this decision? -->'
50
51
  const decisionSection = content.decision || '<!-- What was decided? -->'
@@ -129,10 +130,10 @@ function parseADRStatus(filepath) {
129
130
  * @returns {Promise<string>} basename do arquivo criado
130
131
  */
131
132
  async function newADRDraft(slug) {
132
- fs.mkdirSync('docs/adr', { recursive: true })
133
+ const adrDir = require('../config').load().adrDirs[0]
134
+ fs.mkdirSync(adrDir, { recursive: true })
133
135
 
134
136
  // Verificar idempotência: buscar arquivo existente com o mesmo slug
135
- const adrDir = 'docs/adr'
136
137
  const existing = fs.existsSync(adrDir)
137
138
  ? fs.readdirSync(adrDir).find((f) => f.match(new RegExp(`^ADR-.*-${slug}\\.md$`)))
138
139
  : null
@@ -144,7 +145,7 @@ async function newADRDraft(slug) {
144
145
 
145
146
  const date = today()
146
147
  const filename = `ADR-${date}-${slug}.md`
147
- const filepath = path.join('docs/adr', filename)
148
+ const filepath = path.join(adrDir, filename)
148
149
  const title = slugToTitle(slug)
149
150
 
150
151
  const body = `# ADR: ${title}
@@ -29,6 +29,7 @@ async function scaffold(cfg) {
29
29
  generateCIWorkflow(cfg)
30
30
  generateGitHooks(cfg)
31
31
  generateClaudeMD(cfg)
32
+ if (cfg.backend === 'java') generatePomXml(cfg)
32
33
  generateClaudeCommands()
33
34
  }
34
35
 
@@ -43,9 +44,17 @@ function writeTrackfwConfig(cfg) {
43
44
 
44
45
  frontend: ${cfg.frontend || ''}
45
46
  backend: ${cfg.backend || ''}
47
+ backend_framework: ${cfg.backendFramework || ''}
46
48
  pkg_manager: ${cfg.pkgManager || ''}
47
49
  hooks: ${cfg.hooks || ''}
48
50
  ci: ${cfg.ci || ''}
51
+
52
+ # governance paths (edit to match your project structure)
53
+ adr_dirs:
54
+ - docs/adr
55
+ req_dir: docs/req
56
+ roadmap_dir: docs/roadmaps
57
+ roadmap_namespacing: flat
49
58
  `
50
59
  fs.writeFileSync('trackfw.yaml', content, 'utf8')
51
60
  console.log(' ✓ trackfw.yaml')
@@ -193,6 +202,63 @@ function generateLefthookHook() {
193
202
  console.log(' ✓ lefthook.yml')
194
203
  }
195
204
 
205
+ // ---------------------------------------------------------------------------
206
+ // pom.xml (Java / Spring Boot)
207
+ // ---------------------------------------------------------------------------
208
+
209
+ function generatePomXml(cfg) {
210
+ const slug = cfg.projectName
211
+ ? cfg.projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
212
+ : 'my-app'
213
+ const name = cfg.projectName || 'My App'
214
+ const content = `<?xml version="1.0" encoding="UTF-8"?>
215
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
216
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
217
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
218
+ <modelVersion>4.0.0</modelVersion>
219
+ <parent>
220
+ <groupId>org.springframework.boot</groupId>
221
+ <artifactId>spring-boot-starter-parent</artifactId>
222
+ <version>3.3.0</version>
223
+ <relativePath/>
224
+ </parent>
225
+ <groupId>com.example</groupId>
226
+ <artifactId>${slug}</artifactId>
227
+ <version>0.0.1-SNAPSHOT</version>
228
+ <name>${name}</name>
229
+ <description>${name} — generated by trackfw</description>
230
+ <properties>
231
+ <java.version>21</java.version>
232
+ </properties>
233
+ <dependencies>
234
+ <dependency>
235
+ <groupId>org.springframework.boot</groupId>
236
+ <artifactId>spring-boot-starter-web</artifactId>
237
+ </dependency>
238
+ <dependency>
239
+ <groupId>org.springframework.boot</groupId>
240
+ <artifactId>spring-boot-starter-actuator</artifactId>
241
+ </dependency>
242
+ <dependency>
243
+ <groupId>org.springframework.boot</groupId>
244
+ <artifactId>spring-boot-starter-test</artifactId>
245
+ <scope>test</scope>
246
+ </dependency>
247
+ </dependencies>
248
+ <build>
249
+ <plugins>
250
+ <plugin>
251
+ <groupId>org.springframework.boot</groupId>
252
+ <artifactId>spring-boot-maven-plugin</artifactId>
253
+ </plugin>
254
+ </plugins>
255
+ </build>
256
+ </project>
257
+ `
258
+ fs.writeFileSync('pom.xml', content, 'utf8')
259
+ console.log(' ✓ pom.xml')
260
+ }
261
+
196
262
  // ---------------------------------------------------------------------------
197
263
  // CLAUDE.md
198
264
  // ---------------------------------------------------------------------------
@@ -69,11 +69,12 @@ function toSlug(s) {
69
69
  * @returns {Promise<void>}
70
70
  */
71
71
  async function newREQ(content) {
72
- fs.mkdirSync('docs/req', { recursive: true })
72
+ const reqDir = require('../config').load().reqDir
73
+ fs.mkdirSync(reqDir, { recursive: true })
73
74
 
74
75
  const slug = toSlug(content.title)
75
76
  const date = new Date().toISOString().slice(0, 10)
76
- const filename = `docs/req/REQ-${date}-${slug}.md`
77
+ const filename = `${reqDir}/REQ-${date}-${slug}.md`
77
78
 
78
79
  const motivationSection = content.motivation || '<!-- Why is this requirement needed? What problem does it solve? -->'
79
80
  const criteriaSection = content.criteria || '- [ ]\n- [ ]'