trackfw 3.0.0 → 3.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/README.md +9 -2
- package/package.json +1 -1
- package/src/commands/discover.js +18 -0
- package/src/commands/index.js +2 -0
- package/src/commands/init.js +40 -2
- package/src/commands/log.js +2 -1
- package/src/commands/note.js +24 -0
- package/src/commands/req.js +7 -1
- package/src/commands/roadmap.js +4 -3
- package/src/commands/ship.js +49 -0
- package/src/config/index.js +5 -2
- package/src/forge/adapter.js +189 -0
- package/src/forge/resolve.js +191 -0
- package/src/generators/adr.js +13 -4
- package/src/generators/date.js +23 -0
- package/src/generators/init.js +105 -3
- package/src/generators/note.js +154 -0
- package/src/generators/req.js +88 -5
- package/src/generators/roadmap.js +96 -20
- package/src/i18n/locales/en-US.json +2 -1
- package/src/i18n/locales/es-ES.json +2 -1
- package/src/i18n/locales/pt-BR.json +2 -1
- package/src/identity/preset.js +22 -0
- package/src/integrations/assets/agents/architect.md +32 -0
- package/src/integrations/assets/agents/backend.md +32 -0
- package/src/integrations/assets/agents/code-quality.md +29 -0
- package/src/integrations/assets/agents/data.md +32 -0
- package/src/integrations/assets/agents/dba.md +32 -0
- package/src/integrations/assets/agents/frontend.md +32 -0
- package/src/integrations/assets/agents/iac.md +46 -0
- package/src/integrations/assets/agents/infra.md +34 -0
- package/src/integrations/assets/agents/qa.md +32 -0
- package/src/integrations/assets/agents/security.md +29 -0
- package/src/integrations/assets/agents/tooling.md +47 -0
- package/src/integrations/assets/agents/ux.md +29 -0
- package/src/integrations/assets/catalog.json +16 -2
- package/src/integrations/assets/skills/architecture.md +66 -0
- package/src/integrations/assets/skills/backend.md +67 -0
- package/src/integrations/assets/skills/code-quality.md +78 -0
- package/src/integrations/assets/skills/data.md +75 -0
- package/src/integrations/assets/skills/dba.md +77 -0
- package/src/integrations/assets/skills/frontend.md +70 -0
- package/src/integrations/assets/skills/iac.md +90 -0
- package/src/integrations/assets/skills/infra.md +62 -0
- package/src/integrations/assets/skills/qa.md +63 -0
- package/src/integrations/assets/skills/security.md +74 -0
- package/src/integrations/assets/skills/tooling.md +80 -0
- package/src/integrations/assets/skills/ux.md +70 -0
- package/src/integrations/render.js +57 -6
- package/src/ship/runner.js +454 -0
- package/src/validator/index.js +321 -111
- package/src/validator/traceid.js +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,10 @@ trackfw validate
|
|
|
128
128
|
|
|
129
129
|
# 6. See what is in flight
|
|
130
130
|
trackfw status
|
|
131
|
+
|
|
132
|
+
# 7. Governed commit + push + open PR/MR (feat/fix/refactor branches only)
|
|
133
|
+
git add -p # stage your changes explicitly
|
|
134
|
+
trackfw ship -m "feat(auth): add login flow"
|
|
131
135
|
```
|
|
132
136
|
|
|
133
137
|
---
|
|
@@ -146,6 +150,7 @@ trackfw status
|
|
|
146
150
|
| `trackfw roadmap move <name> <state>` | Move roadmap between states |
|
|
147
151
|
| `trackfw roadmap list` | List all roadmaps grouped by state |
|
|
148
152
|
| `trackfw validate` | Check governance consistency (use as CI gate) |
|
|
153
|
+
| `trackfw ship -m "msg"` | Governed `git commit + push + open PR/MR` — enforces branch pattern and governance gate; resolves forge (GitHub/GitLab/Bitbucket/Azure) automatically or via `--forge`; falls back to a browser URL when the forge CLI is absent |
|
|
149
154
|
| `trackfw context` | Print a structured summary of the project's governance state (REQs, Roadmaps, ADRs with counts and statuses) |
|
|
150
155
|
| `trackfw serve` | Start a local governance dashboard (no cloud, no accounts) |
|
|
151
156
|
| `trackfw status` | Show wip, blocked, REQs waiting on ADRs |
|
|
@@ -372,14 +377,16 @@ operation could delete artifacts from the user's home directory. `list` never
|
|
|
372
377
|
prompts and always assumes `global` unless `--scope` is given, so it reports
|
|
373
378
|
the same destinations `install` actually wrote to.
|
|
374
379
|
|
|
375
|
-
The
|
|
380
|
+
The **12 roles** installed for each tool: **architect · backend · frontend · qa · infra · security · code-quality · dba · ux · data · iac · tooling**
|
|
381
|
+
|
|
382
|
+
The **17 skills** cover governance process (governance, implement, plan, release, review) and technical specialties (backend-skill, code-quality-skill, data-skill, dba-skill, frontend-skill, iac-skill, infra-skill, qa-skill, security-skill, tooling-skill, ux-skill, vault-skill).
|
|
376
383
|
|
|
377
384
|
---
|
|
378
385
|
|
|
379
386
|
## Agent identity — give your agents a name
|
|
380
387
|
|
|
381
388
|
By default the agents are functional and impersonal: `trackfw-architect`,
|
|
382
|
-
`trackfw-backend`, … Agent identity lets you name all
|
|
389
|
+
`trackfw-backend`, … Agent identity lets you name all twelve, pick how they
|
|
383
390
|
address you, and call them by name.
|
|
384
391
|
|
|
385
392
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.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, and Amazon Q.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
package/src/commands/discover.js
CHANGED
|
@@ -4,6 +4,7 @@ const { Command } = require('commander');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
|
+
const { resolve: resolveForge } = require('../forge/resolve');
|
|
7
8
|
|
|
8
9
|
function scan(rootDir) {
|
|
9
10
|
const r = {
|
|
@@ -20,6 +21,7 @@ function scan(rootDir) {
|
|
|
20
21
|
governanceScore: 0,
|
|
21
22
|
hookFramework: 'none',
|
|
22
23
|
ciSystem: 'none',
|
|
24
|
+
forge: '',
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
// trackfw.yaml
|
|
@@ -108,6 +110,18 @@ function scan(rootDir) {
|
|
|
108
110
|
r.ciSystem = 'none';
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
// Forge detection — reuse forge/resolve.js (no duplicate parse).
|
|
114
|
+
// gitRemoteURL returns '' on any error; CI detection is filesystem-based.
|
|
115
|
+
try {
|
|
116
|
+
const { resolveFromRepo } = require('../forge/resolve');
|
|
117
|
+
const res = resolveFromRepo('', '', rootDir);
|
|
118
|
+
if (res.source !== 'none') {
|
|
119
|
+
r.forge = res.forge;
|
|
120
|
+
}
|
|
121
|
+
} catch (_) {
|
|
122
|
+
// forge detection is best-effort; never block scan on it
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
r.governanceScore = calcScore(r);
|
|
112
126
|
return r;
|
|
113
127
|
}
|
|
@@ -146,6 +160,10 @@ function generateYAML(r) {
|
|
|
146
160
|
out += `hooks: ${r.hookFramework}\n`;
|
|
147
161
|
out += `ci: ${r.ciSystem}\n`;
|
|
148
162
|
|
|
163
|
+
if (r.forge) {
|
|
164
|
+
out += `forge: ${r.forge}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
return out;
|
|
150
168
|
}
|
|
151
169
|
|
package/src/commands/index.js
CHANGED
|
@@ -29,6 +29,8 @@ function createProgram() {
|
|
|
29
29
|
program.addCommand(require('./version'))
|
|
30
30
|
program.addCommand(require('./agents'))
|
|
31
31
|
program.addCommand(require('./skills'))
|
|
32
|
+
program.addCommand(require('./note'))
|
|
33
|
+
program.addCommand(require('./ship'))
|
|
32
34
|
|
|
33
35
|
const { createServeCommand } = require('./serve')
|
|
34
36
|
program.addCommand(createServeCommand())
|
package/src/commands/init.js
CHANGED
|
@@ -10,6 +10,7 @@ const cmd = new Command('init')
|
|
|
10
10
|
cmd.description(t('init.description'))
|
|
11
11
|
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
|
|
12
12
|
cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
|
|
13
|
+
cmd.option('--forge <value>', 'Forge platform: github, gitlab, bitbucket, azure', '')
|
|
13
14
|
cmd.action(async (options, command) => {
|
|
14
15
|
const os = require('os')
|
|
15
16
|
const path = require('path')
|
|
@@ -29,6 +30,14 @@ cmd.action(async (options, command) => {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// --forge: validate unconditionally above the non-TTY early return so that
|
|
34
|
+
// an invalid value fails loudly in CI instead of silently no-op'ing.
|
|
35
|
+
const forgeChanged = command.getOptionValueSource('forge') === 'cli'
|
|
36
|
+
if (forgeChanged && options.forge) {
|
|
37
|
+
const { validateForge } = require('../forge/resolve')
|
|
38
|
+
validateForge(options.forge) // throws on invalid value
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
// Pula o wizard de identidade inteiramente quando a flag foi passada
|
|
33
42
|
// explicitamente (já tratado acima) ou quando um arquivo de identidade já
|
|
34
43
|
// existe — re-executar init nunca deve sobrescrever silenciosamente uma
|
|
@@ -45,6 +54,7 @@ cmd.action(async (options, command) => {
|
|
|
45
54
|
pkgManager: 'npm',
|
|
46
55
|
hooks: 'none',
|
|
47
56
|
ci: 'none',
|
|
57
|
+
forge: forgeChanged ? options.forge : '',
|
|
48
58
|
}
|
|
49
59
|
await generators.scaffold(cfg)
|
|
50
60
|
const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
|
|
@@ -62,7 +72,17 @@ cmd.action(async (options, command) => {
|
|
|
62
72
|
|
|
63
73
|
const { input, select, checkbox } = require('@inquirer/prompts')
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
// Detect forge from the current dir to prefill the wizard default.
|
|
76
|
+
const { resolveFromRepo: resolveForgeFromRepo } = require('../forge/resolve')
|
|
77
|
+
let detectedForge = ''
|
|
78
|
+
if (!forgeChanged) {
|
|
79
|
+
try {
|
|
80
|
+
const res = resolveForgeFromRepo('', '', process.cwd())
|
|
81
|
+
if (res.source !== 'none') detectedForge = res.forge
|
|
82
|
+
} catch (_) {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, forgeValue, aiTools, requireReqInCommit, scope
|
|
66
86
|
|
|
67
87
|
try {
|
|
68
88
|
projectName = await input({
|
|
@@ -163,6 +183,22 @@ cmd.action(async (options, command) => {
|
|
|
163
183
|
],
|
|
164
184
|
})
|
|
165
185
|
|
|
186
|
+
if (!forgeChanged) {
|
|
187
|
+
forgeValue = await select({
|
|
188
|
+
message: t('init.prompt.forge'),
|
|
189
|
+
default: detectedForge,
|
|
190
|
+
choices: [
|
|
191
|
+
{ name: 'Auto-detect (omit key)', value: '' },
|
|
192
|
+
{ name: 'GitHub', value: 'github' },
|
|
193
|
+
{ name: 'GitLab', value: 'gitlab' },
|
|
194
|
+
{ name: 'Bitbucket', value: 'bitbucket' },
|
|
195
|
+
{ name: 'Azure DevOps', value: 'azure' },
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
} else {
|
|
199
|
+
forgeValue = options.forge
|
|
200
|
+
}
|
|
201
|
+
|
|
166
202
|
requireReqInCommit = false
|
|
167
203
|
if (hooks !== 'none') {
|
|
168
204
|
const { confirm: confirmPrompt } = require('@inquirer/prompts')
|
|
@@ -202,6 +238,7 @@ cmd.action(async (options, command) => {
|
|
|
202
238
|
pkgManager: 'npm',
|
|
203
239
|
hooks: 'none',
|
|
204
240
|
ci: 'none',
|
|
241
|
+
forge: forgeChanged ? options.forge : '',
|
|
205
242
|
}
|
|
206
243
|
await generators.scaffold(cfg)
|
|
207
244
|
console.log(`\n${t('init.success')}`)
|
|
@@ -229,6 +266,7 @@ cmd.action(async (options, command) => {
|
|
|
229
266
|
pkgManager: 'npm',
|
|
230
267
|
hooks: 'none',
|
|
231
268
|
ci: 'none',
|
|
269
|
+
forge: forgeChanged ? options.forge : '',
|
|
232
270
|
})
|
|
233
271
|
console.log(`\n${t('init.success')}`)
|
|
234
272
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
@@ -236,7 +274,7 @@ cmd.action(async (options, command) => {
|
|
|
236
274
|
}
|
|
237
275
|
}
|
|
238
276
|
|
|
239
|
-
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
|
|
277
|
+
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, forge: forgeValue || '', requireReqInCommit }
|
|
240
278
|
await generators.scaffold(cfg)
|
|
241
279
|
|
|
242
280
|
for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd(), scope)
|
package/src/commands/log.js
CHANGED
|
@@ -3,13 +3,14 @@ const { Command } = require('commander')
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const { t } = require('../i18n')
|
|
6
|
+
const config = require('../config')
|
|
6
7
|
|
|
7
8
|
const cmd = new Command('log')
|
|
8
9
|
cmd.description(t('log.description'))
|
|
9
10
|
cmd.option('--tail <n>', t('log.tail'), '20')
|
|
10
11
|
cmd.action(async (opts) => {
|
|
11
12
|
const tail = parseInt(opts.tail, 10)
|
|
12
|
-
const logPath = path.join(
|
|
13
|
+
const logPath = path.join(config.load().roadmapDir, '.trackfw-log')
|
|
13
14
|
|
|
14
15
|
if (!fs.existsSync(logPath)) {
|
|
15
16
|
console.log(t('log.empty'))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { newNote } = require('../generators/note')
|
|
5
|
+
|
|
6
|
+
function createNoteCommand() {
|
|
7
|
+
const note = new Command('note').description('Manage vault knowledge notes')
|
|
8
|
+
|
|
9
|
+
note
|
|
10
|
+
.command('new <title>')
|
|
11
|
+
.description('Create a new knowledge note in vault/notes/')
|
|
12
|
+
.action((title) => {
|
|
13
|
+
try {
|
|
14
|
+
newNote(title)
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error(`error: ${err.message}`)
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return note
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = createNoteCommand()
|
package/src/commands/req.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const { Command } = require('commander')
|
|
3
|
-
const { listREQs } = require('../generators/req')
|
|
3
|
+
const { listREQs, moveREQ } = require('../generators/req')
|
|
4
4
|
const { t } = require('../i18n')
|
|
5
5
|
|
|
6
6
|
const cmd = new Command('req')
|
|
@@ -67,4 +67,10 @@ cmd.command('list')
|
|
|
67
67
|
listREQs(require('../config').load().reqDir)
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
cmd.command('move <name> <status>')
|
|
71
|
+
.description('Update a REQ status in place')
|
|
72
|
+
.action(async (name, status) => {
|
|
73
|
+
moveREQ(name, status)
|
|
74
|
+
})
|
|
75
|
+
|
|
70
76
|
module.exports = cmd
|
package/src/commands/roadmap.js
CHANGED
|
@@ -8,15 +8,16 @@ cmd.description(t('roadmap.description'))
|
|
|
8
8
|
|
|
9
9
|
cmd.command('new')
|
|
10
10
|
.description(t('roadmap.new.description'))
|
|
11
|
-
.
|
|
11
|
+
.argument('[title]', 'Roadmap title')
|
|
12
|
+
.option('-t, --title <title>', 'Roadmap title (alias)')
|
|
12
13
|
.option('-r, --req <path>', 'Path to the linked REQ')
|
|
13
14
|
.option('--from-req <path>', 'Generate roadmap with ML stubs from REQ acceptance criteria')
|
|
14
|
-
.action(async (opts) => {
|
|
15
|
+
.action(async (titleArg, opts) => {
|
|
15
16
|
if (opts.fromReq) {
|
|
16
17
|
newRoadmapFromReq(opts.fromReq)
|
|
17
18
|
return
|
|
18
19
|
}
|
|
19
|
-
const title = opts.title || 'New Roadmap'
|
|
20
|
+
const title = titleArg || opts.title || 'New Roadmap'
|
|
20
21
|
const reqPath = opts.req || ''
|
|
21
22
|
newRoadmap(title, reqPath)
|
|
22
23
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander')
|
|
4
|
+
const { runShip } = require('../ship/runner')
|
|
5
|
+
const config = require('../config')
|
|
6
|
+
|
|
7
|
+
function createShipCommand() {
|
|
8
|
+
const cmd = new Command('ship')
|
|
9
|
+
cmd
|
|
10
|
+
.description(
|
|
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' +
|
|
15
|
+
' 3. Detects pending squash-merges in other branches (advisory only)\n' +
|
|
16
|
+
' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
|
|
17
|
+
' 5. Commits with Conventional Commits format (-m is required)\n' +
|
|
18
|
+
' 6. Pushes to origin (adds -u if no upstream is configured yet)\n' +
|
|
19
|
+
' 7. Opens PR/MR via the resolved forge CLI (or prints URL if CLI is absent)\n\n' +
|
|
20
|
+
"Stage your files explicitly before running ship.\n" +
|
|
21
|
+
"This command never executes 'git add .' or 'git add -A'."
|
|
22
|
+
)
|
|
23
|
+
.option('-m, --message <msg>', 'Commit message (Conventional Commits format required)')
|
|
24
|
+
.option('--dry-run', 'Print what would be done without executing write commands', false)
|
|
25
|
+
.option('--no-pr', 'Skip PR/MR creation after push')
|
|
26
|
+
.option('--forge <forge>', 'Override forge detection (github, gitlab, bitbucket, azure)', '')
|
|
27
|
+
.action((options) => {
|
|
28
|
+
const cfg = config.load()
|
|
29
|
+
const exitCode = runShip(
|
|
30
|
+
{
|
|
31
|
+
message: options.message || '',
|
|
32
|
+
dryRun: options.dryRun || false,
|
|
33
|
+
noPR: options.pr === false,
|
|
34
|
+
forge: options.forge || '',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
configForge: cfg.forge || '',
|
|
38
|
+
repoDir: '.',
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
if (exitCode !== 0) {
|
|
42
|
+
process.exit(exitCode)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return cmd
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = createShipCommand()
|
package/src/config/index.js
CHANGED
|
@@ -26,9 +26,11 @@ function defaults() {
|
|
|
26
26
|
lenientUntil: '',
|
|
27
27
|
wipLimit: 1,
|
|
28
28
|
wipBySquad: false,
|
|
29
|
+
staleWipDays: 7,
|
|
29
30
|
requireReqInCommit: false,
|
|
30
31
|
strictCiPaths: false,
|
|
31
32
|
traceIdField: '',
|
|
33
|
+
forge: '',
|
|
32
34
|
// NOVOS campos:
|
|
33
35
|
linkFields: {
|
|
34
36
|
req: ['REQ:'],
|
|
@@ -42,7 +44,7 @@ function defaults() {
|
|
|
42
44
|
wip_limit: 'error',
|
|
43
45
|
stale_wip: 'warning',
|
|
44
46
|
adr_orphan: 'warning',
|
|
45
|
-
ref_targets_exist: '
|
|
47
|
+
ref_targets_exist: 'error',
|
|
46
48
|
folder_status: 'warning',
|
|
47
49
|
filename_uniqueness: 'error',
|
|
48
50
|
blocked_by_draft_adr: 'error',
|
|
@@ -191,9 +193,11 @@ function parse(content, cfg) {
|
|
|
191
193
|
case 'lenient_until': cfg.lenientUntil = val; break;
|
|
192
194
|
case 'wip_limit': { const n = parseInt(val, 10); if (n > 0) cfg.wipLimit = n; break; }
|
|
193
195
|
case 'wip_by_squad': cfg.wipBySquad = val === 'true'; break;
|
|
196
|
+
case 'stale_wip_days': { const n = parseInt(val, 10); if (n > 0) cfg.staleWipDays = n; break; }
|
|
194
197
|
case 'require_req_in_commit': cfg.requireReqInCommit = val === 'true'; break;
|
|
195
198
|
case 'strict_ci_paths': cfg.strictCiPaths = val === 'true'; break;
|
|
196
199
|
case 'trace_id_field': cfg.traceIdField = val.replace(/^["']|["']$/g, ''); break;
|
|
200
|
+
case 'forge': cfg.forge = val.replace(/^["']|["']$/g, ''); break;
|
|
197
201
|
case 'link_fields': inLinkFields = true; break;
|
|
198
202
|
case 'acceptance_markers': inAcceptanceMarkers = true; acceptanceMarkers = []; break;
|
|
199
203
|
case 'rules': inRules = true; rules = {}; break;
|
|
@@ -208,4 +212,3 @@ const NAMESPACING_FLAT = 'flat';
|
|
|
208
212
|
const NAMESPACING_BY_AGENT = 'by_agent';
|
|
209
213
|
|
|
210
214
|
module.exports = { load, reset, defaults, expandPath, NAMESPACING_FLAT, NAMESPACING_BY_AGENT };
|
|
211
|
-
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Verificação de disponibilidade de CLI — scan puro no PATH sem subprocess
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Verifica se um executável está disponível no PATH, respeitando
|
|
12
|
+
* TRACKFW_DISABLE_EXTERNAL_COMMANDS (mesmo padrão de discover.go).
|
|
13
|
+
* @param {string} name
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
function _defaultAvailFn(name) {
|
|
17
|
+
if (!name) return false;
|
|
18
|
+
if (process.env.TRACKFW_DISABLE_EXTERNAL_COMMANDS === '1') return false;
|
|
19
|
+
|
|
20
|
+
const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
21
|
+
const isWindows = process.platform === 'win32';
|
|
22
|
+
const exts = isWindows
|
|
23
|
+
? (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')
|
|
24
|
+
: [''];
|
|
25
|
+
|
|
26
|
+
for (const dir of dirs) {
|
|
27
|
+
for (const ext of exts) {
|
|
28
|
+
try {
|
|
29
|
+
const candidate = path.join(dir, name + ext);
|
|
30
|
+
fs.accessSync(candidate, fs.constants.F_OK | fs.constants.X_OK);
|
|
31
|
+
return true;
|
|
32
|
+
} catch (_) {
|
|
33
|
+
// continua
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Conversão de remote URL → base HTTPS
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converte qualquer formato de remote git (git@, ssh://, https://, http://)
|
|
46
|
+
* para URL base HTTPS sem .git.
|
|
47
|
+
*
|
|
48
|
+
* Normalização especial para Azure SSH:
|
|
49
|
+
* git@ssh.dev.azure.com:v3/org/project/repo → https://dev.azure.com/org/project/_git/repo
|
|
50
|
+
*
|
|
51
|
+
* @param {string} rawURL
|
|
52
|
+
* @param {string} forge
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function remoteHTTPSBase(rawURL, forge) {
|
|
56
|
+
if (!rawURL) return '';
|
|
57
|
+
rawURL = rawURL.trim();
|
|
58
|
+
if (!rawURL) return '';
|
|
59
|
+
|
|
60
|
+
let host = '';
|
|
61
|
+
let pathStr = '';
|
|
62
|
+
|
|
63
|
+
if (rawURL.startsWith('git@')) {
|
|
64
|
+
// git@host:path
|
|
65
|
+
const rest = rawURL.slice(4);
|
|
66
|
+
const colonIdx = rest.indexOf(':');
|
|
67
|
+
if (colonIdx < 0) return '';
|
|
68
|
+
host = rest.slice(0, colonIdx).toLowerCase();
|
|
69
|
+
pathStr = rest.slice(colonIdx + 1);
|
|
70
|
+
|
|
71
|
+
} else if (rawURL.startsWith('ssh://')) {
|
|
72
|
+
// ssh://[user@]host/path
|
|
73
|
+
let rest = rawURL.slice(6);
|
|
74
|
+
const atIdx = rest.indexOf('@');
|
|
75
|
+
if (atIdx >= 0) rest = rest.slice(atIdx + 1);
|
|
76
|
+
const slashIdx = rest.indexOf('/');
|
|
77
|
+
if (slashIdx < 0) {
|
|
78
|
+
host = rest.toLowerCase();
|
|
79
|
+
} else {
|
|
80
|
+
host = rest.slice(0, slashIdx).toLowerCase();
|
|
81
|
+
pathStr = rest.slice(slashIdx + 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
} else if (rawURL.startsWith('https://') || rawURL.startsWith('http://')) {
|
|
85
|
+
let rest = rawURL.replace(/^https?:\/\//, '');
|
|
86
|
+
const atIdx = rest.indexOf('@');
|
|
87
|
+
if (atIdx >= 0) rest = rest.slice(atIdx + 1);
|
|
88
|
+
const slashIdx = rest.indexOf('/');
|
|
89
|
+
if (slashIdx < 0) {
|
|
90
|
+
host = rest.toLowerCase();
|
|
91
|
+
} else {
|
|
92
|
+
host = rest.slice(0, slashIdx).toLowerCase();
|
|
93
|
+
pathStr = rest.slice(slashIdx + 1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} else {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pathStr = pathStr.replace(/\.git$/, '').replace(/^\/|\/$/g, '');
|
|
101
|
+
|
|
102
|
+
// Normalização Azure SSH
|
|
103
|
+
if (forge === 'azure' && host === 'ssh.dev.azure.com') {
|
|
104
|
+
host = 'dev.azure.com';
|
|
105
|
+
pathStr = pathStr.replace(/^v3\//, '');
|
|
106
|
+
const parts = pathStr.split('/');
|
|
107
|
+
if (parts.length === 3) {
|
|
108
|
+
pathStr = `${parts[0]}/${parts[1]}/_git/${parts[2]}`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!pathStr) return `https://${host}`;
|
|
113
|
+
return `https://${host}/${pathStr}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Adapter
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @typedef {Object} Adapter
|
|
122
|
+
* @property {string} forge
|
|
123
|
+
* @property {string} noun - "Pull Request" ou "Merge Request"
|
|
124
|
+
* @property {string} cliName - nome do executável (vazio = sem CLI)
|
|
125
|
+
* @property {string[]} cliArgs - argumentos para criar PR/MR
|
|
126
|
+
* @property {boolean} available - false quando CLI ausente
|
|
127
|
+
* @property {function(string, string): string} fallbackURL
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Retorna o Adapter para o forge informado.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} forge
|
|
134
|
+
* @param {function(string): boolean} [availFn] - injeta verificação de CLI (para testes)
|
|
135
|
+
* @returns {Adapter}
|
|
136
|
+
*/
|
|
137
|
+
function forgeAdapter(forge, availFn) {
|
|
138
|
+
if (typeof availFn !== 'function') {
|
|
139
|
+
availFn = _defaultAvailFn;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let noun = 'Pull Request';
|
|
143
|
+
let cliName = '';
|
|
144
|
+
let cliArgs = [];
|
|
145
|
+
let available = false;
|
|
146
|
+
|
|
147
|
+
switch (forge) {
|
|
148
|
+
case 'github':
|
|
149
|
+
cliName = 'gh';
|
|
150
|
+
cliArgs = ['pr', 'create'];
|
|
151
|
+
available = availFn('gh');
|
|
152
|
+
break;
|
|
153
|
+
case 'gitlab':
|
|
154
|
+
noun = 'Merge Request';
|
|
155
|
+
cliName = 'glab';
|
|
156
|
+
cliArgs = ['mr', 'create'];
|
|
157
|
+
available = availFn('glab');
|
|
158
|
+
break;
|
|
159
|
+
case 'azure':
|
|
160
|
+
cliName = 'az';
|
|
161
|
+
cliArgs = ['repos', 'pr', 'create'];
|
|
162
|
+
available = availFn('az');
|
|
163
|
+
break;
|
|
164
|
+
case 'bitbucket':
|
|
165
|
+
// Bitbucket não possui CLI — nunca chama availFn
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
// forge desconhecido: sem CLI, sem disponibilidade
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const adapter = { forge, noun, cliName, cliArgs, available };
|
|
173
|
+
|
|
174
|
+
adapter.fallbackURL = function (remoteURL, branch) {
|
|
175
|
+
const base = remoteHTTPSBase(remoteURL, forge);
|
|
176
|
+
if (!base) return '';
|
|
177
|
+
switch (forge) {
|
|
178
|
+
case 'github': return `${base}/compare/${branch}?expand=1`;
|
|
179
|
+
case 'gitlab': return `${base}/-/merge_requests/new?merge_request[source_branch]=${branch}`;
|
|
180
|
+
case 'bitbucket': return `${base}/pull-requests/new?source=${branch}`;
|
|
181
|
+
case 'azure': return `${base}/pullrequestcreate?sourceRef=${branch}`;
|
|
182
|
+
default: return '';
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return adapter;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { forgeAdapter, remoteHTTPSBase };
|