trackfw 1.0.3 → 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 ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ require('../src/commands/index').createProgram().parseAsync(process.argv)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Governed software delivery framework: ADR → REQ → ROADMAP → kanban",
5
5
  "keywords": [
6
6
  "cli",
@@ -18,19 +18,16 @@
18
18
  "bin": {
19
19
  "trackfw": "./bin/trackfw"
20
20
  },
21
+ "main": "./src/commands/index.js",
21
22
  "files": [
22
- "bin/"
23
+ "bin/",
24
+ "src/"
23
25
  ],
24
- "engines": {
25
- "node": ">=14"
26
+ "dependencies": {
27
+ "commander": "^12.0.0",
28
+ "@inquirer/prompts": "^5.0.0"
26
29
  },
27
- "os": [
28
- "linux",
29
- "darwin",
30
- "win32"
31
- ],
32
- "cpu": [
33
- "x64",
34
- "arm64"
35
- ]
30
+ "engines": {
31
+ "node": ">=18"
32
+ }
36
33
  }
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ const { Command } = require('commander')
4
+ const { input } = require('@inquirer/prompts')
5
+ const generators = require('../generators/adr')
6
+
7
+ const cmd = new Command('adr')
8
+ cmd.description('Manage Architecture Decision Records')
9
+
10
+ cmd.command('new <title>')
11
+ .description('Create a new ADR')
12
+ .action(async (title) => {
13
+ const content = { title }
14
+ // wizard interativo se TTY
15
+ if (process.stdin.isTTY) {
16
+ content.context = await input({ message: 'Context (what motivates this decision)?', default: '' })
17
+ content.decision = await input({ message: 'Decision (what was decided)?', default: '' })
18
+ content.consequences = await input({ message: 'Consequences (positive and negative)?', default: '' })
19
+ content.alternatives = await input({ message: 'Alternatives considered?', default: '' })
20
+ }
21
+ await generators.newADR(content)
22
+ })
23
+
24
+ cmd.command('list')
25
+ .description('List all ADRs in docs/adr/')
26
+ .action(async () => {
27
+ await generators.listADRs('docs/adr')
28
+ })
29
+
30
+ module.exports = cmd
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+
3
+ const { Command } = require('commander')
4
+ const { version } = require('../../package.json')
5
+
6
+ function createProgram() {
7
+ const program = new Command()
8
+ program
9
+ .name('trackfw')
10
+ .description('trackfw — governed software delivery framework\nADR → REQ → ROADMAP → kanban')
11
+ .version(version)
12
+
13
+ program.addCommand(require('./init'))
14
+ program.addCommand(require('./adr'))
15
+ program.addCommand(require('./req'))
16
+ program.addCommand(require('./roadmap'))
17
+ program.addCommand(require('./validate'))
18
+ program.addCommand(require('./status'))
19
+ program.addCommand(require('./log'))
20
+ program.addCommand(require('./plugins'))
21
+
22
+ // plugin dispatch — comandos desconhecidos tentam executar plugin
23
+ program.hook('preSubcommand', () => {})
24
+
25
+ return program
26
+ }
27
+
28
+ module.exports = { createProgram }
@@ -0,0 +1,143 @@
1
+ 'use strict'
2
+ const { Command } = require('commander')
3
+
4
+ const cmd = new Command('init')
5
+ cmd.description('Initialize trackfw governance in the current project')
6
+ cmd.action(async () => {
7
+ const path = require('path')
8
+ const generators = require('../generators/init')
9
+
10
+ // Modo não-TTY: usar defaults e chamar scaffold diretamente
11
+ if (!process.stdin.isTTY) {
12
+ const cfg = {
13
+ projectName: path.basename(process.cwd()),
14
+ projectType: 'governance',
15
+ frontend: '',
16
+ backend: '',
17
+ pkgManager: 'npm',
18
+ hooks: 'none',
19
+ ci: 'none',
20
+ }
21
+ await generators.scaffold(cfg)
22
+ console.log("\n✓ trackfw initialized — run 'trackfw status' to see your governance state.")
23
+ return
24
+ }
25
+
26
+ const { input, select, checkbox } = require('@inquirer/prompts')
27
+
28
+ let projectName, projectType, frontend, pkgManager, backend, hooks, ci, aiTools
29
+
30
+ try {
31
+ projectName = await input({
32
+ message: 'Project name?',
33
+ default: path.basename(process.cwd()),
34
+ })
35
+
36
+ projectType = await select({
37
+ message: 'Project type?',
38
+ choices: [
39
+ { name: 'Full-stack (frontend + backend)', value: 'fullstack' },
40
+ { name: 'Frontend only', value: 'frontend' },
41
+ { name: 'Backend only', value: 'backend' },
42
+ { name: 'Governance only (no build stack)', value: 'governance' },
43
+ ],
44
+ })
45
+
46
+ frontend = ''
47
+ pkgManager = ''
48
+ if (projectType === 'fullstack' || projectType === 'frontend') {
49
+ frontend = await select({
50
+ message: 'Frontend stack?',
51
+ choices: [
52
+ { name: 'React / Next.js', value: 'react' },
53
+ { name: 'Vue', value: 'vue' },
54
+ { name: 'Angular', value: 'angular' },
55
+ ],
56
+ })
57
+ pkgManager = await select({
58
+ message: 'Package manager?',
59
+ choices: [
60
+ { name: 'npm', value: 'npm' },
61
+ { name: 'pnpm', value: 'pnpm' },
62
+ { name: 'yarn', value: 'yarn' },
63
+ { name: 'bun', value: 'bun' },
64
+ ],
65
+ })
66
+ }
67
+
68
+ backend = ''
69
+ if (projectType === 'fullstack' || projectType === 'backend') {
70
+ backend = await select({
71
+ message: 'Backend stack?',
72
+ choices: [
73
+ { name: 'Go', value: 'go' },
74
+ { name: 'Java / Spring Boot', value: 'java' },
75
+ { name: 'Node.js', value: 'node' },
76
+ { name: 'Python', value: 'python' },
77
+ ],
78
+ })
79
+ }
80
+
81
+ hooks = await select({
82
+ message: 'Git hooks?',
83
+ choices: [
84
+ { name: 'husky', value: 'husky' },
85
+ { name: 'lefthook', value: 'lefthook' },
86
+ { name: 'None', value: 'none' },
87
+ ],
88
+ })
89
+
90
+ ci = await select({
91
+ message: 'CI system?',
92
+ choices: [
93
+ { name: 'GitHub Actions', value: 'github-actions' },
94
+ { name: 'GitLab CI', value: 'gitlab-ci' },
95
+ { name: 'None', value: 'none' },
96
+ ],
97
+ })
98
+
99
+ aiTools = await checkbox({
100
+ message: 'Which AI assistants do you use?',
101
+ choices: [
102
+ { name: 'Claude Code', value: 'claude' },
103
+ { name: 'Gemini CLI', value: 'gemini' },
104
+ { name: 'Cursor', value: 'cursor' },
105
+ { name: 'GitHub Copilot', value: 'copilot' },
106
+ { name: 'Windsurf', value: 'windsurf' },
107
+ { name: 'Amazon Q Developer', value: 'amazonq' },
108
+ ],
109
+ })
110
+ } catch (err) {
111
+ // Fallback quando stdin fecha inesperadamente (ex: pipe em TTY simulado)
112
+ const cfg = {
113
+ projectName: path.basename(process.cwd()),
114
+ projectType: 'governance',
115
+ frontend: '',
116
+ backend: '',
117
+ pkgManager: 'npm',
118
+ hooks: 'none',
119
+ ci: 'none',
120
+ }
121
+ await generators.scaffold(cfg)
122
+ console.log("\n✓ trackfw initialized — run 'trackfw status' to see your governance state.")
123
+ return
124
+ }
125
+
126
+ const cfg = { projectName, projectType, frontend, backend, pkgManager, hooks, ci }
127
+ await generators.scaffold(cfg)
128
+
129
+ for (const tool of (aiTools || [])) {
130
+ switch (tool) {
131
+ case 'claude': await generators.installAgents(); break
132
+ case 'gemini': await generators.installGemini(); break
133
+ case 'cursor': await generators.installCursor(); break
134
+ case 'copilot': await generators.installCopilot(); break
135
+ case 'windsurf': await generators.installWindsurf(); break
136
+ case 'amazonq': await generators.installAmazonQ(); break
137
+ }
138
+ }
139
+
140
+ console.log("\n✓ trackfw initialized — run 'trackfw status' to see your governance state.")
141
+ })
142
+
143
+ module.exports = cmd
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+ const { Command } = require('commander')
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ const cmd = new Command('log')
7
+ cmd.description('Show roadmap state transition history')
8
+ cmd.option('--tail <n>', 'Number of recent transitions to show', '20')
9
+ cmd.action(async (opts) => {
10
+ const tail = parseInt(opts.tail, 10)
11
+ const logPath = path.join('docs', 'roadmaps', '.trackfw-log')
12
+
13
+ if (!fs.existsSync(logPath)) {
14
+ console.log('No transitions recorded yet.')
15
+ return
16
+ }
17
+
18
+ const lines = fs.readFileSync(logPath, 'utf8')
19
+ .split('\n')
20
+ .filter(l => l.trim() !== '')
21
+
22
+ const start = Math.max(0, lines.length - tail)
23
+ const visible = lines.slice(start)
24
+
25
+ console.log('── trackfw log ─────────────────────────')
26
+ visible.forEach(l => console.log(l))
27
+ })
28
+
29
+ module.exports = cmd
@@ -0,0 +1,96 @@
1
+ 'use strict'
2
+ const { Command } = require('commander')
3
+ const os = require('os')
4
+ const path = require('path')
5
+ const fs = require('fs')
6
+
7
+ function pluginsDir() {
8
+ return path.join(os.homedir(), '.trackfw', 'plugins')
9
+ }
10
+
11
+ function platformOS() {
12
+ if (process.platform === 'win32') return 'windows'
13
+ if (process.platform === 'darwin') return 'darwin'
14
+ return 'linux'
15
+ }
16
+
17
+ function platformArch() {
18
+ if (process.arch === 'x64') return 'amd64'
19
+ return process.arch
20
+ }
21
+
22
+ function listPlugins() {
23
+ const dir = pluginsDir()
24
+ fs.mkdirSync(dir, { recursive: true })
25
+ return fs.readdirSync(dir).filter(f => fs.statSync(path.join(dir, f)).isFile())
26
+ }
27
+
28
+ async function installPlugin(repo) {
29
+ let base = repo
30
+ let tag = 'latest'
31
+ const atIdx = repo.indexOf('@')
32
+ if (atIdx !== -1) {
33
+ base = repo.slice(0, atIdx)
34
+ tag = repo.slice(atIdx + 1)
35
+ }
36
+ const pluginName = path.basename(base)
37
+ const assetName = `trackfw-plugin-${pluginName}-${platformOS()}-${platformArch()}`
38
+ const url = tag === 'latest'
39
+ ? `https://github.com/${base}/releases/latest/download/${assetName}`
40
+ : `https://github.com/${base}/releases/download/${tag}/${assetName}`
41
+
42
+ const res = await fetch(url)
43
+ if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`)
44
+
45
+ const dir = pluginsDir()
46
+ fs.mkdirSync(dir, { recursive: true })
47
+ fs.writeFileSync(path.join(dir, pluginName), Buffer.from(await res.arrayBuffer()), { mode: 0o755 })
48
+ }
49
+
50
+ function removePlugin(name) {
51
+ const filePath = path.join(pluginsDir(), name)
52
+ if (!fs.existsSync(filePath)) throw new Error(`plugin "${name}" not found`)
53
+ fs.unlinkSync(filePath)
54
+ }
55
+
56
+ const cmd = new Command('plugins')
57
+ cmd.description('Manage trackfw plugins')
58
+
59
+ cmd.command('list')
60
+ .description('List installed plugins')
61
+ .action(() => {
62
+ const plugins = listPlugins()
63
+ if (plugins.length === 0) {
64
+ console.log('No plugins installed. Use `trackfw plugins add <user/repo>` to install one.')
65
+ return
66
+ }
67
+ plugins.forEach(p => console.log(p))
68
+ })
69
+
70
+ cmd.command('add <repo>')
71
+ .description('Install a plugin from GitHub Releases (user/repo or user/repo@tag)')
72
+ .action(async (repo) => {
73
+ try {
74
+ console.log(`Installing plugin from ${repo}...`)
75
+ await installPlugin(repo)
76
+ const name = repo.split('@')[0].split('/').pop()
77
+ console.log(`Plugin "${name}" installed successfully.`)
78
+ } catch (err) {
79
+ console.error(`Error: ${err.message}`)
80
+ process.exit(1)
81
+ }
82
+ })
83
+
84
+ cmd.command('remove <name>')
85
+ .description('Remove an installed plugin')
86
+ .action((name) => {
87
+ try {
88
+ removePlugin(name)
89
+ console.log(`Plugin "${name}" removed.`)
90
+ } catch (err) {
91
+ console.error(`Error: ${err.message}`)
92
+ process.exit(1)
93
+ }
94
+ })
95
+
96
+ module.exports = cmd
@@ -0,0 +1,69 @@
1
+ 'use strict'
2
+ const { Command } = require('commander')
3
+ const { listREQs } = require('../generators/req')
4
+
5
+ const cmd = new Command('req')
6
+ cmd.description('Manage Requirements')
7
+
8
+ cmd.command('new <title>')
9
+ .description('Create a new REQ')
10
+ .action(async (title) => {
11
+ const { input, select } = require('@inquirer/prompts')
12
+ const generators = require('../generators/req')
13
+ const adrGenerators = require('../generators/adr')
14
+
15
+ const content = { title, motivation: '', criteria: '', dependsOnADRs: [] }
16
+
17
+ if (process.stdin.isTTY) {
18
+ // 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: '' })
21
+
22
+ // Detectar domínios com base em título + motivação
23
+ const probes = generators.detectDomains(content.title + ' ' + content.motivation)
24
+
25
+ // Form 2 — critérios de aceite
26
+ content.criteria = await input({ message: 'Acceptance Criteria (one per line)', default: '- [ ]\n- [ ]' })
27
+
28
+ // Perguntas dinâmicas por probe
29
+ const generatedADRs = []
30
+ for (const probe of probes) {
31
+ for (const question of probe.questions) {
32
+ const choices = question.options.map(opt => ({
33
+ name: opt.label,
34
+ value: opt.adrSlug || '',
35
+ }))
36
+ const answer = await select({
37
+ message: question.text,
38
+ choices,
39
+ })
40
+ if (answer) {
41
+ try {
42
+ const basename = await adrGenerators.newADRDraft(answer)
43
+ if (basename) generatedADRs.push(basename)
44
+ } catch (e) {
45
+ console.warn(`warning: could not create ADR draft for ${answer}: ${e.message}`)
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ content.dependsOnADRs = [...new Set(generatedADRs)]
52
+ }
53
+
54
+ await generators.newREQ(content)
55
+
56
+ if (content.dependsOnADRs.length > 0) {
57
+ console.log('\nADR drafts created:')
58
+ content.dependsOnADRs.forEach(adr => console.log(` -> ${adr}`))
59
+ console.log('\nResolve these ADRs (set Status: Accepted) before creating a roadmap.')
60
+ }
61
+ })
62
+
63
+ cmd.command('list')
64
+ .description('List all REQs in docs/req/')
65
+ .action(async () => {
66
+ listREQs('docs/req')
67
+ })
68
+
69
+ module.exports = cmd
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+ const { Command } = require('commander')
3
+ const { listRoadmaps, showRoadmap, moveRoadmap, newRoadmap } = require('../generators/roadmap')
4
+
5
+ const cmd = new Command('roadmap')
6
+ cmd.description('Manage Roadmaps')
7
+
8
+ cmd.command('new')
9
+ .description('Create a new roadmap from a REQ')
10
+ .option('-t, --title <title>', 'Roadmap title')
11
+ .option('-r, --req <path>', 'Path to the linked REQ')
12
+ .action(async (opts) => {
13
+ const title = opts.title || 'New Roadmap'
14
+ const reqPath = opts.req || ''
15
+ newRoadmap(title, reqPath)
16
+ })
17
+
18
+ cmd.command('list')
19
+ .description('List all roadmaps grouped by state')
20
+ .action(async () => {
21
+ listRoadmaps()
22
+ })
23
+
24
+ cmd.command('show <name>')
25
+ .description('Show a roadmap by name (partial match)')
26
+ .action(async (name) => {
27
+ showRoadmap(name)
28
+ })
29
+
30
+ cmd.command('move <name> <state>')
31
+ .description('Move a roadmap between states (backlog|wip|blocked|done|abandoned)')
32
+ .action(async (name, state) => {
33
+ moveRoadmap(name, state)
34
+ })
35
+
36
+ module.exports = cmd
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+ const { Command } = require('commander')
3
+ const { getStatus } = require('../validator')
4
+
5
+ const cmd = new Command('status')
6
+ cmd.description('Show project governance status')
7
+ cmd.action(async () => {
8
+ console.log(await getStatus())
9
+ })
10
+
11
+ module.exports = cmd
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+ const { Command } = require('commander')
3
+ const { validate } = require('../validator')
4
+
5
+ const cmd = new Command('validate')
6
+ cmd.description('Validate governance rules')
7
+ cmd.action(async () => {
8
+ const { violations, warnings } = await validate()
9
+
10
+ if (violations.length === 0 && warnings.length === 0) {
11
+ console.log('✓ No violations found.')
12
+ return
13
+ }
14
+
15
+ if (violations.length > 0) {
16
+ console.log(`\n✗ Violations (${violations.length}):`)
17
+ violations.forEach(v => console.log(` • ${v}`))
18
+ }
19
+
20
+ if (warnings.length > 0) {
21
+ console.log(`\n⚠ Warnings (${warnings.length}):`)
22
+ warnings.forEach(w => console.log(` • ${w}`))
23
+ }
24
+
25
+ if (violations.length > 0) process.exit(1)
26
+ })
27
+
28
+ module.exports = cmd
@@ -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 }