trackfw 6.10.0 → 7.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 +0 -3
- package/package.json +1 -1
- package/src/branch/runner.js +33 -20
- package/src/changelog/index.js +114 -0
- package/src/commands/branch.js +12 -9
- package/src/commands/changelog.js +29 -0
- package/src/commands/commit.js +60 -0
- package/src/commands/discover.js +69 -0
- package/src/commands/index.js +22 -4
- package/src/commands/integrations.js +7 -0
- package/src/commands/ship.js +4 -3
- package/src/commands/thirdparty.js +384 -0
- package/src/commands/update-harness.js +3 -1
- package/src/commands/update.js +3 -1
- package/src/commit/runner.js +352 -0
- package/src/config/index.js +27 -0
- package/src/generators/hooks.js +479 -2
- package/src/generators/init.js +28 -6
- package/src/i18n/locales/en-US.json +1 -19
- package/src/i18n/locales/es-ES.json +1 -19
- package/src/i18n/locales/pt-BR.json +1 -19
- package/src/integrations/index.js +19 -2
- package/src/integrations/manager.js +15 -2
- package/src/lib/unknown-command.js +82 -0
- package/src/ship/runner.js +213 -26
- package/src/thirdparty/fetch.js +140 -0
- package/src/thirdparty/markers.js +207 -0
- package/src/thirdparty/provenance.js +114 -0
- package/src/thirdparty/quarantine.js +113 -0
- package/src/thirdparty/references.js +194 -0
- package/src/validator/index.js +715 -16
- package/src/commands/plugins.js +0 -184
package/README.md
CHANGED
|
@@ -157,9 +157,6 @@ trackfw ship -m "feat(auth): add login flow"
|
|
|
157
157
|
| `trackfw serve` | Start a local governance dashboard (no cloud, no accounts) |
|
|
158
158
|
| `trackfw status` | Show wip, blocked, REQs waiting on ADRs |
|
|
159
159
|
| `trackfw log [--tail N]` | Show roadmap state transition history |
|
|
160
|
-
| `trackfw plugins list` | List installed plugins |
|
|
161
|
-
| `trackfw plugins add <user/repo>` | Install a plugin from GitHub Releases |
|
|
162
|
-
| `trackfw plugins remove <name>` | Remove an installed plugin |
|
|
163
160
|
| `trackfw agents list` | List available agents and deployment state across AI CLIs |
|
|
164
161
|
| `trackfw agents install` | Install selected specialist agents |
|
|
165
162
|
| `trackfw agents update` | Safely update managed agents |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Governance CLI for AI-native software delivery and AI coding agents. Enforces traceability: ADR → REQ → ROADMAP → kanban. Native support for Codex, Claude Code, Gemini CLI, Cursor, Copilot, Windsurf, Amazon Q, OpenCode, and Kiro.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
package/src/branch/runner.js
CHANGED
|
@@ -16,12 +16,20 @@ const { spawnSync } = require('child_process')
|
|
|
16
16
|
const config = require('../config')
|
|
17
17
|
const validator = require('../validator')
|
|
18
18
|
|
|
19
|
-
// branchValidTypes
|
|
20
|
-
|
|
19
|
+
// branchValidTypes is the full vocabulary accepted by `trackfw branch new`. feat/fix/refactor are
|
|
20
|
+
// gated on a matching REQ + roadmap already in wip/ or done/ (branchGatedTypes below); chore/docs
|
|
21
|
+
// are housekeeping types — already treated as roadmap-exempt by `trackfw ship` and `trackfw
|
|
22
|
+
// commit` — and create the branch without that gate.
|
|
23
|
+
const branchValidTypes = new Set(['feat', 'fix', 'refactor', 'chore', 'docs'])
|
|
24
|
+
|
|
25
|
+
// branchGatedTypes is the subset of branchValidTypes that requires a matching REQ + roadmap
|
|
26
|
+
// already in wip/ or done/ before the branch is created. Keep this in sync with the pattern
|
|
27
|
+
// `trackfw ship`/`trackfw commit` use to decide when the branch_has_wip_roadmap gate applies.
|
|
28
|
+
const branchGatedTypes = new Set(['feat', 'fix', 'refactor'])
|
|
21
29
|
|
|
22
30
|
/**
|
|
23
31
|
* parseBranchSpec splits "<type>/<slug>" and validates both parts. type must be one of
|
|
24
|
-
* feat, fix, refactor (branchValidTypes); slug must be non-empty.
|
|
32
|
+
* feat, fix, refactor, chore, docs (branchValidTypes); slug must be non-empty.
|
|
25
33
|
* @param {string} spec
|
|
26
34
|
* @returns {{ branchType: string, slug: string }}
|
|
27
35
|
* @throws {Error} when spec is malformed
|
|
@@ -30,12 +38,12 @@ function parseBranchSpec(spec) {
|
|
|
30
38
|
const value = String(spec)
|
|
31
39
|
const idx = value.indexOf('/')
|
|
32
40
|
if (idx === -1 || value.slice(0, idx) === '') {
|
|
33
|
-
throw new Error(`invalid branch spec "${spec}" — expected <type>/<slug> with type in feat, fix, refactor`)
|
|
41
|
+
throw new Error(`invalid branch spec "${spec}" — expected <type>/<slug> with type in feat, fix, refactor, chore, docs`)
|
|
34
42
|
}
|
|
35
43
|
const branchType = value.slice(0, idx)
|
|
36
44
|
const slug = value.slice(idx + 1)
|
|
37
45
|
if (!branchValidTypes.has(branchType)) {
|
|
38
|
-
throw new Error(`invalid branch type "${branchType}" — must be one of feat, fix, refactor`)
|
|
46
|
+
throw new Error(`invalid branch type "${branchType}" — must be one of feat, fix, refactor, chore, docs`)
|
|
39
47
|
}
|
|
40
48
|
if (slug.trim() === '') {
|
|
41
49
|
throw new Error(`branch slug is required — expected <type>/<slug>, got "${spec}"`)
|
|
@@ -102,24 +110,28 @@ function runBranchNew(spec, dryRun, deps = {}) {
|
|
|
102
110
|
|
|
103
111
|
const branchName = `${branchType}/${slug}`
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
// chore/docs are housekeeping types — already treated as roadmap-exempt by `trackfw ship`
|
|
114
|
+
// and `trackfw commit` — so the branch_has_wip_roadmap gate below does not apply to them.
|
|
115
|
+
if (branchGatedTypes.has(branchType)) {
|
|
116
|
+
const cfg = loadConfig()
|
|
117
|
+
const wipDirs = resolveWIPDirs(cfg)
|
|
118
|
+
const doneDirs = resolveDoneDirs(cfg)
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
const normalizedSlug = validator.normalizeBranchSlug(slug)
|
|
121
|
+
const { matched, candidates } = matchSlug(normalizedSlug, wipDirs, doneDirs)
|
|
111
122
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
if (!matched) {
|
|
124
|
+
const msg = (!candidates || candidates.length === 0)
|
|
125
|
+
? validator.branchGovernanceOrientation(branchName)
|
|
126
|
+
: validator.branchNoMatchingRoadmapMessage(branchName, candidates)
|
|
127
|
+
if (dryRun) {
|
|
128
|
+
writeln(`[dry-run] would block: ${msg}`)
|
|
129
|
+
} else {
|
|
130
|
+
writeln(msg)
|
|
131
|
+
}
|
|
132
|
+
writeErr(`blocked: no matching roadmap in wip/ nor done/ for "${branchName}"`)
|
|
133
|
+
return 1
|
|
120
134
|
}
|
|
121
|
-
writeErr(`blocked: no matching roadmap in wip/ nor done/ for "${branchName}"`)
|
|
122
|
-
return 1
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
if (dryRun) {
|
|
@@ -134,6 +146,7 @@ function runBranchNew(spec, dryRun, deps = {}) {
|
|
|
134
146
|
|
|
135
147
|
module.exports = {
|
|
136
148
|
branchValidTypes,
|
|
149
|
+
branchGatedTypes,
|
|
137
150
|
parseBranchSpec,
|
|
138
151
|
defaultGitCheckout,
|
|
139
152
|
runBranchNew,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// changelog — parsing e extração de seções do CHANGELOG.md no formato
|
|
4
|
+
// Keep a Changelog (https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
// Port 1:1 de internal/changelog/changelog.go — manter em sincronia.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
// sectionHeaderRE casa cabeçalhos de seção no formato "## [x.y.z] - YYYY-MM-DD"
|
|
11
|
+
// ou "## [Unreleased]" (sem data).
|
|
12
|
+
const sectionHeaderRE = /^## \[([^\]]+)\](?: - (\d{4}-\d{2}-\d{2}))?/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* parseSections — separa o conteúdo de um CHANGELOG.md em seções, uma por
|
|
16
|
+
* cabeçalho "## [...]" encontrado. Texto antes da primeira seção (título do
|
|
17
|
+
* arquivo, preâmbulo) é descartado.
|
|
18
|
+
* @param {string} content
|
|
19
|
+
* @returns {Array<{version: string, date: string, body: string}>}
|
|
20
|
+
*/
|
|
21
|
+
function parseSections(content) {
|
|
22
|
+
const lines = content.split('\n')
|
|
23
|
+
|
|
24
|
+
const sections = []
|
|
25
|
+
let current = null
|
|
26
|
+
let bodyLines = []
|
|
27
|
+
|
|
28
|
+
const flush = () => {
|
|
29
|
+
if (current !== null) {
|
|
30
|
+
current.body = bodyLines.join('\n')
|
|
31
|
+
sections.push(current)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const m = sectionHeaderRE.exec(line)
|
|
37
|
+
if (m !== null) {
|
|
38
|
+
flush()
|
|
39
|
+
current = { version: m[1], date: m[2] || '', body: '' }
|
|
40
|
+
bodyLines = []
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
if (current !== null) {
|
|
44
|
+
bodyLines.push(line)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
flush()
|
|
48
|
+
|
|
49
|
+
return sections
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* firstSection — retorna a primeira seção da lista.
|
|
54
|
+
* Erro se a lista vier vazia.
|
|
55
|
+
* @param {Array<{version: string, date: string, body: string}>} sections
|
|
56
|
+
* @returns {{version: string, date: string, body: string}}
|
|
57
|
+
*/
|
|
58
|
+
function firstSection(sections) {
|
|
59
|
+
if (sections.length === 0) {
|
|
60
|
+
throw new Error('CHANGELOG.md has no version sections')
|
|
61
|
+
}
|
|
62
|
+
return sections[0]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* findVersion — busca a seção com version igual ao argumento, normalizando um
|
|
67
|
+
* prefixo "v"/"V" opcional no argumento do usuário antes de comparar.
|
|
68
|
+
* @param {Array<{version: string, date: string, body: string}>} sections
|
|
69
|
+
* @param {string} version
|
|
70
|
+
* @returns {{version: string, date: string, body: string}}
|
|
71
|
+
*/
|
|
72
|
+
function findVersion(sections, version) {
|
|
73
|
+
let normalized = version
|
|
74
|
+
if (normalized.length > 0 && (normalized[0] === 'v' || normalized[0] === 'V')) {
|
|
75
|
+
normalized = normalized.slice(1)
|
|
76
|
+
}
|
|
77
|
+
for (const s of sections) {
|
|
78
|
+
if (s.version === normalized || s.version === version) {
|
|
79
|
+
return s
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`version "${version}" not found in CHANGELOG.md`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* formatSection — reconstrói o texto formatado de uma seção, reproduzindo o
|
|
87
|
+
* cabeçalho original.
|
|
88
|
+
* @param {{version: string, date: string, body: string}} section
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
function formatSection(section) {
|
|
92
|
+
const dateSuffix = section.date !== '' ? ` - ${section.date}` : ''
|
|
93
|
+
const body = section.body.replace(/^\n+/, '')
|
|
94
|
+
return `## [${section.version}]${dateSuffix}\n\n${body.replace(/\n+$/, '') + '\n'}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* read — lê o CHANGELOG.md na raiz informada.
|
|
99
|
+
* @param {string} root
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function read(root) {
|
|
103
|
+
const filePath = path.join(root, 'CHANGELOG.md')
|
|
104
|
+
try {
|
|
105
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (err.code === 'ENOENT') {
|
|
108
|
+
throw new Error('CHANGELOG.md not found — nothing to show')
|
|
109
|
+
}
|
|
110
|
+
throw err
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { parseSections, firstSection, findVersion, formatSection, read }
|
package/src/commands/branch.js
CHANGED
|
@@ -15,22 +15,25 @@ function createBranchCommand() {
|
|
|
15
15
|
const newCmd = new Command('new')
|
|
16
16
|
newCmd
|
|
17
17
|
.description(
|
|
18
|
-
'Create a feat/fix/refactor branch
|
|
18
|
+
'Create a feat/fix/refactor/chore/docs branch; feat/fix/refactor gated on a matching REQ + roadmap already in wip/ or done/\n\n' +
|
|
19
19
|
'trackfw branch new moves the branch_has_wip_roadmap governance gate (already enforced\n' +
|
|
20
20
|
"by 'trackfw validate' and 'trackfw ship') to before branch creation, instead of after:\n\n" +
|
|
21
|
-
' 1. Validates <type> is one of feat, fix, refactor and <slug> is non-empty.\n' +
|
|
22
|
-
' 2.
|
|
23
|
-
" logic 'trackfw validate' already uses (normalized slug
|
|
24
|
-
"
|
|
25
|
-
" governance orientation message 'trackfw validate'
|
|
26
|
-
"
|
|
27
|
-
'
|
|
21
|
+
' 1. Validates <type> is one of feat, fix, refactor, chore, docs and <slug> is non-empty.\n' +
|
|
22
|
+
' 2. For feat, fix, refactor: checks whether a roadmap in wip/ or done/ matches the given\n' +
|
|
23
|
+
" slug — the exact matching logic 'trackfw validate' already uses (normalized slug,\n" +
|
|
24
|
+
" filename contains match). Without a match: blocks — 'git checkout -b' is never\n" +
|
|
25
|
+
" executed — and prints the same governance orientation message 'trackfw validate'\n" +
|
|
26
|
+
" already prints for this rule.\n" +
|
|
27
|
+
" 3. For chore, docs: housekeeping types already treated as roadmap-exempt by 'trackfw ship'\n" +
|
|
28
|
+
" and 'trackfw commit' — the branch is created without the roadmap gate.\n" +
|
|
29
|
+
" 4. With a match (or for chore/docs): runs 'git checkout -b <type>/<slug>', propagating\n" +
|
|
30
|
+
" Git's own output and exit status literally.\n\n" +
|
|
28
31
|
'Create the governance artifacts first if this blocks you:\n' +
|
|
29
32
|
' trackfw req new "title"\n' +
|
|
30
33
|
' trackfw roadmap new "title"\n' +
|
|
31
34
|
' trackfw roadmap move <name> wip'
|
|
32
35
|
)
|
|
33
|
-
.argument('<spec>', '<type>/<slug>, type in feat, fix, refactor')
|
|
36
|
+
.argument('<spec>', '<type>/<slug>, type in feat, fix, refactor, chore, docs')
|
|
34
37
|
.option('--dry-run', 'Report whether the branch would be created or blocked, without executing git', false)
|
|
35
38
|
.action((spec, options) => {
|
|
36
39
|
const exitCode = runBranchNew(spec, !!options.dryRun, {})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const { Command } = require('commander')
|
|
3
|
+
const { read, parseSections, firstSection, findVersion, formatSection } = require('../changelog')
|
|
4
|
+
|
|
5
|
+
const cmd = new Command('changelog')
|
|
6
|
+
cmd.description('Show entries from CHANGELOG.md')
|
|
7
|
+
// enablePositionalOptions escopa a flag local "--version" a este subcomando,
|
|
8
|
+
// evitando que o root a intercepte como a flag global "trackfw --version".
|
|
9
|
+
cmd.enablePositionalOptions()
|
|
10
|
+
cmd.option('--version <x.y.z>', 'Show a specific version section')
|
|
11
|
+
cmd.option('--all', 'Show the entire CHANGELOG.md')
|
|
12
|
+
cmd.action(async (opts) => {
|
|
13
|
+
try {
|
|
14
|
+
const root = process.cwd()
|
|
15
|
+
const content = read(root)
|
|
16
|
+
if (opts.all) {
|
|
17
|
+
process.stdout.write(content)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const sections = parseSections(content)
|
|
21
|
+
const section = opts.version ? findVersion(sections, opts.version) : firstSection(sections)
|
|
22
|
+
process.stdout.write(formatSection(section))
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(`Error: ${err.message}`)
|
|
25
|
+
process.exitCode = 1
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
module.exports = cmd
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// trackfw commit -m "<message>"
|
|
4
|
+
//
|
|
5
|
+
// CLI wiring for the `trackfw commit` command. The testable logic lives in
|
|
6
|
+
// ../commit/runner.js (mirrors the branch.js / branch/runner.js split already used in this CLI).
|
|
7
|
+
|
|
8
|
+
const { Command } = require('commander')
|
|
9
|
+
const { runCommit, buildSuggestedMessage } = require('../commit/runner')
|
|
10
|
+
|
|
11
|
+
function createCommitCommand() {
|
|
12
|
+
const cmd = new Command('commit')
|
|
13
|
+
cmd
|
|
14
|
+
.description(
|
|
15
|
+
"trackfw commit is the missing intermediate step between raw 'git commit' and\n" +
|
|
16
|
+
"'trackfw ship': it commits staged changes directly, but blocks the commit before it happens\n" +
|
|
17
|
+
'when governance is missing, instead of letting it land and only catching it later:\n\n' +
|
|
18
|
+
" 1. On 'main'/'master': always blocked — commit directly on the default branch is never\n" +
|
|
19
|
+
' permitted.\n' +
|
|
20
|
+
' 2. On a feat/fix/refactor branch: requires a roadmap matching the branch slug already in\n' +
|
|
21
|
+
" wip/ or done/ — the exact matching logic 'trackfw branch new' and 'trackfw validate'\n" +
|
|
22
|
+
' already use. Without a match, blocks with the same governance orientation message.\n' +
|
|
23
|
+
' 3. On any other branch (e.g. doc/housekeeping branches): allowed without requiring a\n' +
|
|
24
|
+
' roadmap — a warning is logged, but the commit proceeds.\n' +
|
|
25
|
+
" 4. When allowed: runs 'git commit -m <message>', propagating Git's own output and exit\n" +
|
|
26
|
+
' status literally.\n\n' +
|
|
27
|
+
"'--suggest' takes a completely separate path: it prints a heuristic Conventional Commits\n" +
|
|
28
|
+
"skeleton built from 'git diff --cached --name-status' (type + staged file list) and exits\n" +
|
|
29
|
+
'without ever committing — no LLM call, just a structural heuristic. It is not a\n' +
|
|
30
|
+
'ready-to-use message; review and edit before using it with -m. When \'--suggest\' is set,\n' +
|
|
31
|
+
"'-m' (if also passed) is ignored and no commit ever happens.\n\n" +
|
|
32
|
+
'Create the governance artifacts first if this blocks you:\n' +
|
|
33
|
+
' trackfw req new "title"\n' +
|
|
34
|
+
' trackfw roadmap new "title"\n' +
|
|
35
|
+
' trackfw roadmap move <name> wip'
|
|
36
|
+
)
|
|
37
|
+
.option('-m, --message <msg>', 'Commit message (required)')
|
|
38
|
+
.option('--suggest', 'Print a heuristic Conventional Commits message skeleton from staged files and exit without committing (ignores -m)')
|
|
39
|
+
.action((options) => {
|
|
40
|
+
if (options.suggest) {
|
|
41
|
+
try {
|
|
42
|
+
const suggestion = buildSuggestedMessage({})
|
|
43
|
+
process.stdout.write(suggestion + '\n')
|
|
44
|
+
return
|
|
45
|
+
} catch (err) {
|
|
46
|
+
process.stderr.write(err.message + '\n')
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const exitCode = runCommit(options.message || '', {})
|
|
52
|
+
if (exitCode !== 0) {
|
|
53
|
+
process.exit(exitCode)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return cmd
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = createCommitCommand()
|
package/src/commands/discover.js
CHANGED
|
@@ -22,6 +22,11 @@ function scan(rootDir) {
|
|
|
22
22
|
hookFramework: 'none',
|
|
23
23
|
ciSystem: 'none',
|
|
24
24
|
forge: '',
|
|
25
|
+
// suggestedTestFramework é uma sugestão best-effort (nunca erro, '' quando nada bate) baseada
|
|
26
|
+
// em arquivos de configuração presentes na raiz do projeto. É apenas impressa como sugestão
|
|
27
|
+
// pelo comando `discover` — nunca escrita automaticamente em trackfw.yaml (a convenção de
|
|
28
|
+
// agent_conventions deve ser sempre declarada pelo time, não inferida).
|
|
29
|
+
suggestedTestFramework: '',
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
// trackfw.yaml
|
|
@@ -110,6 +115,9 @@ function scan(rootDir) {
|
|
|
110
115
|
r.ciSystem = 'none';
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
// Suggested test framework — best-effort heuristic, never an error.
|
|
119
|
+
r.suggestedTestFramework = detectTestFramework(rootDir);
|
|
120
|
+
|
|
113
121
|
// Forge detection — reuse forge/resolve.js (no duplicate parse).
|
|
114
122
|
// gitRemoteURL returns '' on any error; CI detection is filesystem-based.
|
|
115
123
|
try {
|
|
@@ -338,6 +346,60 @@ function countMD(dir) {
|
|
|
338
346
|
return n;
|
|
339
347
|
}
|
|
340
348
|
|
|
349
|
+
// detectTestFramework é uma heurística best-effort para sugerir um framework de teste com base
|
|
350
|
+
// em arquivos de configuração presentes na raiz do projeto. Nunca retorna erro — retorna '' quando
|
|
351
|
+
// nenhum arquivo-gatilho é encontrado. Ordem de precedência: jest, vitest, pytest, go test.
|
|
352
|
+
function detectTestFramework(rootDir) {
|
|
353
|
+
if (isFile(path.join(rootDir, 'jest.config.js')) || isFile(path.join(rootDir, 'jest.config.ts'))) {
|
|
354
|
+
return 'jest';
|
|
355
|
+
}
|
|
356
|
+
if (isFile(path.join(rootDir, 'vitest.config.js')) || isFile(path.join(rootDir, 'vitest.config.ts'))) {
|
|
357
|
+
return 'vitest';
|
|
358
|
+
}
|
|
359
|
+
if (isFile(path.join(rootDir, 'pytest.ini'))) {
|
|
360
|
+
return 'pytest';
|
|
361
|
+
}
|
|
362
|
+
if (hasFileWithSubstring(path.join(rootDir, 'pyproject.toml'), '[tool.pytest')) {
|
|
363
|
+
return 'pytest';
|
|
364
|
+
}
|
|
365
|
+
if (hasFileWithSubstring(path.join(rootDir, 'setup.cfg'), '[tool:pytest]')) {
|
|
366
|
+
return 'pytest';
|
|
367
|
+
}
|
|
368
|
+
if (isFile(path.join(rootDir, 'go.mod')) && hasGoTestFile(rootDir)) {
|
|
369
|
+
return 'go test';
|
|
370
|
+
}
|
|
371
|
+
return '';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// hasFileWithSubstring lê path e retorna true se seu conteúdo contém sub. Retorna false
|
|
375
|
+
// silenciosamente se o arquivo não existir ou não puder ser lido (best-effort).
|
|
376
|
+
function hasFileWithSubstring(p, sub) {
|
|
377
|
+
let content;
|
|
378
|
+
try { content = fs.readFileSync(p, 'utf8'); } catch { return false; }
|
|
379
|
+
return content.includes(sub);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// hasGoTestFile percorre rootDir recursivamente procurando qualquer arquivo *_test.go.
|
|
383
|
+
function hasGoTestFile(rootDir) {
|
|
384
|
+
let found = false;
|
|
385
|
+
function walk(d) {
|
|
386
|
+
if (found) return;
|
|
387
|
+
let entries;
|
|
388
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
389
|
+
for (const e of entries) {
|
|
390
|
+
if (found) return;
|
|
391
|
+
if (e.isDirectory()) {
|
|
392
|
+
walk(path.join(d, e.name));
|
|
393
|
+
} else if (e.name.endsWith('_test.go')) {
|
|
394
|
+
found = true;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
walk(rootDir);
|
|
400
|
+
return found;
|
|
401
|
+
}
|
|
402
|
+
|
|
341
403
|
function listSubDirs(dir) {
|
|
342
404
|
try {
|
|
343
405
|
return fs.readdirSync(dir).filter(f => {
|
|
@@ -412,6 +474,12 @@ cmd.action((opts) => {
|
|
|
412
474
|
console.log('⚠ No CI system detected');
|
|
413
475
|
}
|
|
414
476
|
|
|
477
|
+
// suggested test framework — printed only, never written to trackfw.yaml automatically
|
|
478
|
+
// (agent_conventions must always be declared by the team).
|
|
479
|
+
if (r.suggestedTestFramework) {
|
|
480
|
+
console.log(`Suggested test framework: ${r.suggestedTestFramework} (add to trackfw.yaml as agent_conventions: if correct)`);
|
|
481
|
+
}
|
|
482
|
+
|
|
415
483
|
console.log(`\nGovernance Score: ${r.governanceScore}/100`);
|
|
416
484
|
|
|
417
485
|
if (opts.init) {
|
|
@@ -500,3 +568,4 @@ module.exports.generateBootstrapLog = generateBootstrapLog;
|
|
|
500
568
|
module.exports.writeValidateScript = writeValidateScript;
|
|
501
569
|
module.exports.writeCIWorkflow = writeCIWorkflow;
|
|
502
570
|
module.exports.writeCIWorkflowForce = writeCIWorkflowForce;
|
|
571
|
+
module.exports.detectTestFramework = detectTestFramework;
|
package/src/commands/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander')
|
|
4
4
|
const { version } = require('../../package.json')
|
|
5
|
+
const { formatUnknownCommandError } = require('../lib/unknown-command')
|
|
5
6
|
|
|
6
7
|
function createProgram() {
|
|
7
8
|
const program = new Command()
|
|
@@ -9,6 +10,25 @@ function createProgram() {
|
|
|
9
10
|
.name('trackfw')
|
|
10
11
|
.description('trackfw — governed software delivery framework\nADR → REQ → ROADMAP → kanban')
|
|
11
12
|
.version(`trackfw ${version}`)
|
|
13
|
+
// enablePositionalOptions: garante que uma flag de subcomando (ex.: o
|
|
14
|
+
// "changelog --version <x.y.z>") não seja capturada pela flag global
|
|
15
|
+
// "--version" do root — sem isto, commander casa "--version" contra o
|
|
16
|
+
// primeiro registro conhecido em toda a árvore de comandos.
|
|
17
|
+
program.enablePositionalOptions()
|
|
18
|
+
|
|
19
|
+
// Comando desconhecido — mensagem canônica compartilhada pelos 3 CLIs
|
|
20
|
+
// (ADR-2026-08-15-remocao-do-subsistema-de-plugins-em-vez-de-gate-de-binario-
|
|
21
|
+
// de-terceiro.md D3). Registrar um listener 'command:*' intercepta ANTES do
|
|
22
|
+
// commander chamar seu próprio Command.unknownCommand() — evita reformatar
|
|
23
|
+
// uma mensagem já impressa por ele (o padrão que causava dupla-impressão no
|
|
24
|
+
// Go, ver internal/commands/root.go) e evita depender do texto/threshold
|
|
25
|
+
// próprio de commander.suggestSimilar, que diverge de cobra/argparse.
|
|
26
|
+
program.on('command:*', (operands) => {
|
|
27
|
+
const typed = operands[0]
|
|
28
|
+
const candidates = program.commands.map((cmd) => cmd.name())
|
|
29
|
+
process.stderr.write(formatUnknownCommandError(typed, candidates, program.name()) + '\n')
|
|
30
|
+
process.exit(1)
|
|
31
|
+
})
|
|
12
32
|
|
|
13
33
|
program.addCommand(require('./init'))
|
|
14
34
|
program.addCommand(require('./adr'))
|
|
@@ -16,8 +36,8 @@ function createProgram() {
|
|
|
16
36
|
program.addCommand(require('./roadmap'))
|
|
17
37
|
program.addCommand(require('./validate'))
|
|
18
38
|
program.addCommand(require('./status'))
|
|
39
|
+
program.addCommand(require('./changelog'))
|
|
19
40
|
program.addCommand(require('./log'))
|
|
20
|
-
program.addCommand(require('./plugins'))
|
|
21
41
|
program.addCommand(require('./discover'))
|
|
22
42
|
program.addCommand(require('./update'))
|
|
23
43
|
program.addCommand(require('./metrics'))
|
|
@@ -33,13 +53,11 @@ function createProgram() {
|
|
|
33
53
|
program.addCommand(require('./ship'))
|
|
34
54
|
program.addCommand(require('./barrier'))
|
|
35
55
|
program.addCommand(require('./branch'))
|
|
56
|
+
program.addCommand(require('./commit'))
|
|
36
57
|
|
|
37
58
|
const { createServeCommand } = require('./serve')
|
|
38
59
|
program.addCommand(createServeCommand())
|
|
39
60
|
|
|
40
|
-
// plugin dispatch — comandos desconhecidos tentam executar plugin
|
|
41
|
-
program.hook('preSubcommand', () => {})
|
|
42
|
-
|
|
43
61
|
return program
|
|
44
62
|
}
|
|
45
63
|
|
|
@@ -6,6 +6,7 @@ const { catalog, execute, parseSurfaces, buildPlans } = require('../integrations
|
|
|
6
6
|
const identityStore = require('../identity')
|
|
7
7
|
const identityWizard = require('./identity-wizard')
|
|
8
8
|
const { t } = require('../i18n')
|
|
9
|
+
const { createThirdPartyCommand } = require('./thirdparty')
|
|
9
10
|
|
|
10
11
|
const csv = value => String(value).split(',').map(entry => entry.trim()).filter(Boolean)
|
|
11
12
|
const collect = (value, previous) => previous.concat(value)
|
|
@@ -102,6 +103,12 @@ async function promptAmbiguousSurfaces(kind, options, prompts = require('@inquir
|
|
|
102
103
|
|
|
103
104
|
function createLifecycleCommand(kind) {
|
|
104
105
|
const root = new Command(kind).description(`Manage trackfw ${kind}`)
|
|
106
|
+
// third-party fetch/install (D1): reachable from both `trackfw agents
|
|
107
|
+
// third-party` and `trackfw skills third-party`, under the same
|
|
108
|
+
// two-phase quarantine gate. Mirrors
|
|
109
|
+
// internal/commands/integrations_flags.go registering `third-party` in
|
|
110
|
+
// newIntegrationsLifecycleCmd.
|
|
111
|
+
root.addCommand(createThirdPartyCommand(kind))
|
|
105
112
|
for (const operation of ['list', 'install', 'uninstall', 'update']) {
|
|
106
113
|
const mutation = operation !== 'list'
|
|
107
114
|
const command = new Command(operation)
|
package/src/commands/ship.js
CHANGED
|
@@ -9,9 +9,10 @@ function createShipCommand() {
|
|
|
9
9
|
cmd
|
|
10
10
|
.description(
|
|
11
11
|
'trackfw ship runs a governed delivery sequence:\n\n' +
|
|
12
|
-
' 1. Validates branch name — must match feat|fix|refactor/<slug>\n' +
|
|
13
|
-
' 2. Validates governance — REQ + roadmap in wip/ must exist\n' +
|
|
14
|
-
' (hard gate: not affected by lenient mode or per-rule severity)\n' +
|
|
12
|
+
' 1. Validates branch name — must match feat|fix|refactor|chore|docs/<slug>\n' +
|
|
13
|
+
' 2. Validates governance — REQ + roadmap in wip/ must exist for feat/fix/refactor branches\n' +
|
|
14
|
+
' (hard gate: not affected by lenient mode or per-rule severity); chore/docs branches skip\n' +
|
|
15
|
+
" this check, mirroring 'trackfw commit'\n" +
|
|
15
16
|
' 3. Detects pending squash-merges in other branches (advisory only)\n' +
|
|
16
17
|
' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
|
|
17
18
|
' 5. Commits with Conventional Commits format (-m is required)\n' +
|