trackfw 7.0.0 → 7.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/bin/trackfw +38 -1
- package/package.json +1 -1
- package/src/branch/prune.js +450 -0
- package/src/commands/branch.js +47 -0
- package/src/commands/discover.js +6 -0
- package/src/commands/doctor.js +32 -0
- package/src/commands/index.js +1 -0
- package/src/commands/integrations.js +16 -1
- package/src/commands/serve.js +52 -4
- package/src/commands/update-harness.js +317 -11
- package/src/generators/hooks.js +478 -36
- package/src/i18n/locales/en-US.json +0 -3
- package/src/i18n/locales/es-ES.json +0 -3
- package/src/i18n/locales/pt-BR.json +0 -3
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/manager.js +118 -17
- package/src/lib/fatal-error.js +64 -0
- package/src/ship/runner.js +52 -75
- package/src/validator/index.js +426 -92
package/src/commands/serve.js
CHANGED
|
@@ -13,6 +13,48 @@ const { handleAttention } = require('../serve/api_attention')
|
|
|
13
13
|
|
|
14
14
|
const STATIC_DIR = path.join(__dirname, '..', 'serve', 'static')
|
|
15
15
|
|
|
16
|
+
// Aviso pinado, byte-idêntico entre os 3 runtimes (Go, Node.js, Python) — ver
|
|
17
|
+
// docs/cli-parity.md "`trackfw serve` — endereço de escuta, `--host` e
|
|
18
|
+
// aviso de exposição". Emitido quando --host resolve para uma interface
|
|
19
|
+
// diferente de loopback.
|
|
20
|
+
function exposureWarning(host, port) {
|
|
21
|
+
return `WARNING: trackfw serve is binding to ${host}:${port} — the governance chain (ADRs, REQs, roadmaps) will be readable without authentication by any device that can reach it.`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Texto de --help pinado, byte-idêntico entre os 3 runtimes.
|
|
25
|
+
const SERVE_HOST_FLAG_HELP = 'Host to bind to (loopback only by default; use 0.0.0.0 to expose on the network)'
|
|
26
|
+
|
|
27
|
+
// Espelha internal/serve/serve.go IsLoopbackHost — 'localhost' ou IP loopback
|
|
28
|
+
// (IPv4 127.0.0.0/8 inteiro, ou IPv6 ::1), igual a net.IP.IsLoopback() do Go
|
|
29
|
+
// e ipaddress.ip_address(...).is_loopback do Python.
|
|
30
|
+
function isLoopbackHost(host) {
|
|
31
|
+
const net = require('net')
|
|
32
|
+
if (host === 'localhost') return true
|
|
33
|
+
if (net.isIPv4(host)) {
|
|
34
|
+
return host.split('.')[0] === '127'
|
|
35
|
+
}
|
|
36
|
+
if (net.isIPv6(host)) {
|
|
37
|
+
return host === '::1' || host === '0:0:0:0:0:0:0:1'
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Espelha internal/serve/serve.go DisplayURL — URL a imprimir e a abrir no
|
|
43
|
+
// browser. 'localhost' é mantido só para 'localhost' ou IPv4 loopback
|
|
44
|
+
// (127.0.0.0/8), para não mudar a saída do caso comum; hosts IPv6 usam
|
|
45
|
+
// colchetes; qualquer outro host é impresso como está.
|
|
46
|
+
function displayUrl(host, port) {
|
|
47
|
+
const net = require('net')
|
|
48
|
+
if (host === 'localhost') return `http://localhost:${port}`
|
|
49
|
+
if (net.isIPv4(host)) {
|
|
50
|
+
return host.split('.')[0] === '127' ? `http://localhost:${port}` : `http://${host}:${port}`
|
|
51
|
+
}
|
|
52
|
+
if (net.isIPv6(host)) {
|
|
53
|
+
return `http://[${host}]:${port}`
|
|
54
|
+
}
|
|
55
|
+
return `http://${host}:${port}`
|
|
56
|
+
}
|
|
57
|
+
|
|
16
58
|
// Mapa de extensão → Content-Type
|
|
17
59
|
const MIME = {
|
|
18
60
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -141,19 +183,25 @@ function createServeCommand() {
|
|
|
141
183
|
cmd
|
|
142
184
|
.description('Inicia o servidor HTTP do trackfw dashboard (kanban + chain + metrics)')
|
|
143
185
|
.option('--port <port>', 'Porta do servidor', '8080')
|
|
186
|
+
.option('--host <host>', SERVE_HOST_FLAG_HELP, '127.0.0.1')
|
|
144
187
|
.option('--no-open', 'Não abrir o browser automaticamente')
|
|
145
188
|
.action((opts) => {
|
|
146
189
|
const cfg = config.load()
|
|
147
190
|
const port = parseInt(opts.port, 10) || 8080
|
|
191
|
+
const host = opts.host
|
|
148
192
|
|
|
149
193
|
const server = createServer(cfg, port)
|
|
150
194
|
|
|
151
|
-
|
|
152
|
-
console.
|
|
195
|
+
if (!isLoopbackHost(host)) {
|
|
196
|
+
console.error(exposureWarning(host, port))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
server.listen(port, host, () => {
|
|
200
|
+
const url = displayUrl(host, port)
|
|
201
|
+
console.log(`trackfw serve: ${url}`)
|
|
153
202
|
|
|
154
203
|
if (opts.open !== false) {
|
|
155
204
|
// Tentar abrir o browser
|
|
156
|
-
const url = `http://localhost:${port}`
|
|
157
205
|
const { exec } = require('child_process')
|
|
158
206
|
const platform = process.platform
|
|
159
207
|
let openCmd
|
|
@@ -187,4 +235,4 @@ function createServeCommand() {
|
|
|
187
235
|
return cmd
|
|
188
236
|
}
|
|
189
237
|
|
|
190
|
-
module.exports = { createServeCommand, createServer }
|
|
238
|
+
module.exports = { createServeCommand, createServer, isLoopbackHost, displayUrl }
|
|
@@ -462,6 +462,288 @@ function credentialGuardTargetKiro(homeRoot, { dryRun, installMissing }) {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
// git-branch-guard global-scope wiring (ROADMAP-2026-08-17 Wave 2/ML-2A).
|
|
467
|
+
// Mirrors the six credentialGuardTarget<Tool> functions above, entry-for-
|
|
468
|
+
// entry — same 4-state contract, same displayPath per tool, same reuse of
|
|
469
|
+
// mergeClaudeHookArray/mergeSimpleCommandArray/mergeCopilotHookArray (those
|
|
470
|
+
// helpers only need a scriptPath, which here is trackfw-git-branch-guard.sh
|
|
471
|
+
// instead of trackfw-credential-guard.sh). Mirrors
|
|
472
|
+
// internal/generators/update.go:harnessGitBranchGuardTarget<Tool>.
|
|
473
|
+
//
|
|
474
|
+
// Kiro is the one structural exception: credentialGuardTargetKiro rewrites
|
|
475
|
+
// ~/.kiro/hooks/trackfw-credential-guard.json WHOLESALE every run (never
|
|
476
|
+
// merges), so sharing that file with a second wholesale writer would make
|
|
477
|
+
// the two targets flap between each other's desired 2-entry document forever
|
|
478
|
+
// (idempotency failure). gitBranchGuardTargetKiro therefore writes its OWN
|
|
479
|
+
// dedicated file, ~/.kiro/hooks/trackfw-git-branch-guard.json, same
|
|
480
|
+
// {"version":"v1","hooks":[pre,post]} schema, hook names
|
|
481
|
+
// "trackfw-git-branch-guard-global-pre"/"-global-post".
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
function gitBranchGuardTargetClaude(homeRoot, { dryRun, installMissing }) {
|
|
485
|
+
const id = 'claude-git-branch-guard'
|
|
486
|
+
const filePath = path.join(homeRoot, '.claude', 'settings.json')
|
|
487
|
+
const displayPath = tildeify(homeRoot, filePath)
|
|
488
|
+
const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
|
|
489
|
+
|
|
490
|
+
let root
|
|
491
|
+
let raw = null
|
|
492
|
+
try {
|
|
493
|
+
if (fs.existsSync(filePath)) {
|
|
494
|
+
raw = fs.readFileSync(filePath, 'utf8')
|
|
495
|
+
root = raw.length ? JSON.parse(raw) : {}
|
|
496
|
+
} else {
|
|
497
|
+
if (!installMissing) return { id, state: 'missing', path: displayPath }
|
|
498
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
499
|
+
root = {}
|
|
500
|
+
if (!root.hooks) root.hooks = {}
|
|
501
|
+
root.hooks.PreToolUse = mergeClaudeHookArray(root.hooks.PreToolUse, 'Bash', scriptPath)
|
|
502
|
+
root.hooks.PostToolUse = mergeClaudeHookArray(root.hooks.PostToolUse, 'Bash', scriptPath)
|
|
503
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
504
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
505
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
506
|
+
return { id, state: 'updated', path: displayPath }
|
|
507
|
+
}
|
|
508
|
+
} catch (e) {
|
|
509
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
if (!root.hooks) root.hooks = {}
|
|
514
|
+
root.hooks.PreToolUse = mergeClaudeHookArray(root.hooks.PreToolUse, 'Bash', scriptPath)
|
|
515
|
+
root.hooks.PostToolUse = mergeClaudeHookArray(root.hooks.PostToolUse, 'Bash', scriptPath)
|
|
516
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
517
|
+
if (desired === raw) return { id, state: 'skipped', path: displayPath }
|
|
518
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
519
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
520
|
+
return { id, state: 'updated', path: displayPath }
|
|
521
|
+
} catch (e) {
|
|
522
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function gitBranchGuardTargetCodex(homeRoot, { dryRun, installMissing }) {
|
|
527
|
+
const id = 'codex-git-branch-guard'
|
|
528
|
+
const filePath = path.join(homeRoot, '.codex', 'hooks.json')
|
|
529
|
+
const displayPath = tildeify(homeRoot, filePath)
|
|
530
|
+
const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
|
|
531
|
+
|
|
532
|
+
let root
|
|
533
|
+
let raw = null
|
|
534
|
+
try {
|
|
535
|
+
if (fs.existsSync(filePath)) {
|
|
536
|
+
raw = fs.readFileSync(filePath, 'utf8')
|
|
537
|
+
root = raw.length ? JSON.parse(raw) : {}
|
|
538
|
+
} else {
|
|
539
|
+
if (!installMissing) return { id, state: 'missing', path: displayPath }
|
|
540
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
541
|
+
root = {}
|
|
542
|
+
if (!root.hooks) root.hooks = {}
|
|
543
|
+
root.hooks.PreToolUse = mergeClaudeHookArray(root.hooks.PreToolUse, 'Bash', scriptPath)
|
|
544
|
+
root.hooks.PostToolUse = mergeClaudeHookArray(root.hooks.PostToolUse, 'Bash', scriptPath)
|
|
545
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
546
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
547
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
548
|
+
return { id, state: 'updated', path: displayPath }
|
|
549
|
+
}
|
|
550
|
+
} catch (e) {
|
|
551
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
if (!root.hooks) root.hooks = {}
|
|
556
|
+
root.hooks.PreToolUse = mergeClaudeHookArray(root.hooks.PreToolUse, 'Bash', scriptPath)
|
|
557
|
+
root.hooks.PostToolUse = mergeClaudeHookArray(root.hooks.PostToolUse, 'Bash', scriptPath)
|
|
558
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
559
|
+
if (desired === raw) return { id, state: 'skipped', path: displayPath }
|
|
560
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
561
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
562
|
+
return { id, state: 'updated', path: displayPath }
|
|
563
|
+
} catch (e) {
|
|
564
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function gitBranchGuardTargetGemini(homeRoot, { dryRun, installMissing }) {
|
|
569
|
+
const id = 'gemini-git-branch-guard'
|
|
570
|
+
const filePath = path.join(homeRoot, '.gemini', 'settings.json')
|
|
571
|
+
const displayPath = tildeify(homeRoot, filePath)
|
|
572
|
+
const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
|
|
573
|
+
|
|
574
|
+
let root
|
|
575
|
+
let raw = null
|
|
576
|
+
try {
|
|
577
|
+
if (fs.existsSync(filePath)) {
|
|
578
|
+
raw = fs.readFileSync(filePath, 'utf8')
|
|
579
|
+
root = raw.length ? JSON.parse(raw) : {}
|
|
580
|
+
} else {
|
|
581
|
+
if (!installMissing) return { id, state: 'missing', path: displayPath }
|
|
582
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
583
|
+
root = {}
|
|
584
|
+
if (!root.hooks) root.hooks = {}
|
|
585
|
+
root.hooks.BeforeTool = mergeClaudeHookArray(root.hooks.BeforeTool, 'run_shell_command', scriptPath)
|
|
586
|
+
root.hooks.AfterTool = mergeClaudeHookArray(root.hooks.AfterTool, 'run_shell_command', scriptPath)
|
|
587
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
588
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
589
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
590
|
+
return { id, state: 'updated', path: displayPath }
|
|
591
|
+
}
|
|
592
|
+
} catch (e) {
|
|
593
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
if (!root.hooks) root.hooks = {}
|
|
598
|
+
root.hooks.BeforeTool = mergeClaudeHookArray(root.hooks.BeforeTool, 'run_shell_command', scriptPath)
|
|
599
|
+
root.hooks.AfterTool = mergeClaudeHookArray(root.hooks.AfterTool, 'run_shell_command', scriptPath)
|
|
600
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
601
|
+
if (desired === raw) return { id, state: 'skipped', path: displayPath }
|
|
602
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
603
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
604
|
+
return { id, state: 'updated', path: displayPath }
|
|
605
|
+
} catch (e) {
|
|
606
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function gitBranchGuardTargetCursor(homeRoot, { dryRun, installMissing }) {
|
|
611
|
+
const id = 'cursor-git-branch-guard'
|
|
612
|
+
const filePath = path.join(homeRoot, '.cursor', 'hooks.json')
|
|
613
|
+
const displayPath = tildeify(homeRoot, filePath)
|
|
614
|
+
const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
|
|
615
|
+
|
|
616
|
+
let root
|
|
617
|
+
let raw = null
|
|
618
|
+
try {
|
|
619
|
+
if (fs.existsSync(filePath)) {
|
|
620
|
+
raw = fs.readFileSync(filePath, 'utf8')
|
|
621
|
+
root = raw.length ? JSON.parse(raw) : {}
|
|
622
|
+
} else {
|
|
623
|
+
if (!installMissing) return { id, state: 'missing', path: displayPath }
|
|
624
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
625
|
+
root = {}
|
|
626
|
+
if (typeof root.version === 'undefined') root.version = 1
|
|
627
|
+
if (!root.hooks) root.hooks = {}
|
|
628
|
+
root.hooks.beforeShellExecution = mergeSimpleCommandArray(root.hooks.beforeShellExecution, scriptPath)
|
|
629
|
+
root.hooks.afterShellExecution = mergeSimpleCommandArray(root.hooks.afterShellExecution, scriptPath)
|
|
630
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
631
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
632
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
633
|
+
return { id, state: 'updated', path: displayPath }
|
|
634
|
+
}
|
|
635
|
+
} catch (e) {
|
|
636
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
try {
|
|
640
|
+
if (typeof root.version === 'undefined') root.version = 1
|
|
641
|
+
if (!root.hooks) root.hooks = {}
|
|
642
|
+
root.hooks.beforeShellExecution = mergeSimpleCommandArray(root.hooks.beforeShellExecution, scriptPath)
|
|
643
|
+
root.hooks.afterShellExecution = mergeSimpleCommandArray(root.hooks.afterShellExecution, scriptPath)
|
|
644
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
645
|
+
if (desired === raw) return { id, state: 'skipped', path: displayPath }
|
|
646
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
647
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
648
|
+
return { id, state: 'updated', path: displayPath }
|
|
649
|
+
} catch (e) {
|
|
650
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function gitBranchGuardTargetCopilot(homeRoot, { dryRun, installMissing }) {
|
|
655
|
+
const id = 'copilot-git-branch-guard'
|
|
656
|
+
const filePath = path.join(homeRoot, '.copilot', 'settings.json')
|
|
657
|
+
const displayPath = tildeify(homeRoot, filePath)
|
|
658
|
+
const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
|
|
659
|
+
|
|
660
|
+
let root
|
|
661
|
+
let raw = null
|
|
662
|
+
try {
|
|
663
|
+
if (fs.existsSync(filePath)) {
|
|
664
|
+
raw = fs.readFileSync(filePath, 'utf8')
|
|
665
|
+
root = raw.length ? JSON.parse(raw) : {}
|
|
666
|
+
} else {
|
|
667
|
+
if (!installMissing) return { id, state: 'missing', path: displayPath }
|
|
668
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
669
|
+
root = {}
|
|
670
|
+
if (!root.hooks) root.hooks = {}
|
|
671
|
+
root.hooks.preToolUse = mergeCopilotHookArray(root.hooks.preToolUse, scriptPath)
|
|
672
|
+
root.hooks.postToolUse = mergeCopilotHookArray(root.hooks.postToolUse, scriptPath)
|
|
673
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
674
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
675
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
676
|
+
return { id, state: 'updated', path: displayPath }
|
|
677
|
+
}
|
|
678
|
+
} catch (e) {
|
|
679
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
if (!root.hooks) root.hooks = {}
|
|
684
|
+
root.hooks.preToolUse = mergeCopilotHookArray(root.hooks.preToolUse, scriptPath)
|
|
685
|
+
root.hooks.postToolUse = mergeCopilotHookArray(root.hooks.postToolUse, scriptPath)
|
|
686
|
+
const desired = JSON.stringify(root, null, 2) + '\n'
|
|
687
|
+
if (desired === raw) return { id, state: 'skipped', path: displayPath }
|
|
688
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
689
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
690
|
+
return { id, state: 'updated', path: displayPath }
|
|
691
|
+
} catch (e) {
|
|
692
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// gitBranchGuardTargetKiro writes a DEDICATED file,
|
|
697
|
+
// ~/.kiro/hooks/trackfw-git-branch-guard.json — deliberately NOT
|
|
698
|
+
// credentialGuardTargetKiro's file. See this section's header comment for
|
|
699
|
+
// why sharing would break idempotency.
|
|
700
|
+
function gitBranchGuardTargetKiro(homeRoot, { dryRun, installMissing }) {
|
|
701
|
+
const id = 'kiro-git-branch-guard'
|
|
702
|
+
const filePath = path.join(homeRoot, '.kiro', 'hooks', 'trackfw-git-branch-guard.json')
|
|
703
|
+
const displayPath = tildeify(homeRoot, filePath)
|
|
704
|
+
const scriptPath = path.join(homeRoot, '.trackfw', 'scripts', 'trackfw-git-branch-guard.sh')
|
|
705
|
+
|
|
706
|
+
const desired = JSON.stringify(
|
|
707
|
+
{
|
|
708
|
+
version: 'v1',
|
|
709
|
+
hooks: [
|
|
710
|
+
{
|
|
711
|
+
name: 'trackfw-git-branch-guard-global-pre',
|
|
712
|
+
description: 'Blocks branch-creation git subcommands issued outside trackfw branch new (global, all trackfw projects)',
|
|
713
|
+
trigger: 'PreToolUse',
|
|
714
|
+
matcher: 'shell',
|
|
715
|
+
action: { type: 'command', command: scriptPath },
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
name: 'trackfw-git-branch-guard-global-post',
|
|
719
|
+
description: 'Warns on branch-creation git subcommands issued outside trackfw branch new (global, all trackfw projects)',
|
|
720
|
+
trigger: 'PostToolUse',
|
|
721
|
+
matcher: 'shell',
|
|
722
|
+
action: { type: 'command', command: scriptPath },
|
|
723
|
+
},
|
|
724
|
+
],
|
|
725
|
+
},
|
|
726
|
+
null,
|
|
727
|
+
2
|
|
728
|
+
) + '\n'
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
const exists = fs.existsSync(filePath)
|
|
732
|
+
const actual = exists ? fs.readFileSync(filePath, 'utf8') : null
|
|
733
|
+
|
|
734
|
+
if (!exists && !installMissing) return { id, state: 'missing', path: displayPath }
|
|
735
|
+
if (exists && actual === desired) return { id, state: 'skipped', path: displayPath }
|
|
736
|
+
|
|
737
|
+
if (dryRun) return { id, state: 'updated', path: displayPath }
|
|
738
|
+
|
|
739
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
740
|
+
fs.writeFileSync(filePath, desired, 'utf8')
|
|
741
|
+
return { id, state: 'updated', path: displayPath }
|
|
742
|
+
} catch (e) {
|
|
743
|
+
return { id, state: 'failed', path: displayPath, message: e.message }
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
465
747
|
// catalogBundleTarget — one target per (tool, kind) pair at global scope.
|
|
466
748
|
// Uses IntegrationManager.inspect (read-only) to classify every catalog
|
|
467
749
|
// item under that pair, then only calls manager.update() for the subset
|
|
@@ -506,18 +788,20 @@ function catalogBundleTarget(toolId, kind, homeRoot, identityConfig, { dryRun, i
|
|
|
506
788
|
}
|
|
507
789
|
|
|
508
790
|
// HARNESS_TARGET_IDS — mirrors internal/generators/update.go:HarnessTargetIDs.
|
|
509
|
-
// "codex-credential-guard"/"
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
|
|
791
|
+
// "codex-credential-guard"/"codex-git-branch-guard",
|
|
792
|
+
// "gemini-credential-guard"/"gemini-git-branch-guard", etc. are each
|
|
793
|
+
// inserted immediately BEFORE their tool's "-agents"/"-skills" pair (same
|
|
794
|
+
// relative position as claude-credential-guard/claude-git-branch-guard,
|
|
795
|
+
// which precede claude-agents/claude-skills), with credential-guard always
|
|
796
|
+
// preceding git-branch-guard within a tool — see buildHarnessTargetIDs's
|
|
797
|
+
// comment in update.go for the full rationale.
|
|
798
|
+
const HARNESS_TARGET_IDS = ['claude-skill', 'claude-credential-guard', 'claude-git-branch-guard']
|
|
515
799
|
for (const target of catalog.targets) {
|
|
516
|
-
if (target.id === 'codex') HARNESS_TARGET_IDS.push('codex-credential-guard')
|
|
517
|
-
if (target.id === 'gemini') HARNESS_TARGET_IDS.push('gemini-credential-guard')
|
|
518
|
-
if (target.id === 'cursor') HARNESS_TARGET_IDS.push('cursor-credential-guard')
|
|
519
|
-
if (target.id === 'copilot') HARNESS_TARGET_IDS.push('copilot-credential-guard')
|
|
520
|
-
if (target.id === 'kiro') HARNESS_TARGET_IDS.push('kiro-credential-guard')
|
|
800
|
+
if (target.id === 'codex') HARNESS_TARGET_IDS.push('codex-credential-guard', 'codex-git-branch-guard')
|
|
801
|
+
if (target.id === 'gemini') HARNESS_TARGET_IDS.push('gemini-credential-guard', 'gemini-git-branch-guard')
|
|
802
|
+
if (target.id === 'cursor') HARNESS_TARGET_IDS.push('cursor-credential-guard', 'cursor-git-branch-guard')
|
|
803
|
+
if (target.id === 'copilot') HARNESS_TARGET_IDS.push('copilot-credential-guard', 'copilot-git-branch-guard')
|
|
804
|
+
if (target.id === 'kiro') HARNESS_TARGET_IDS.push('kiro-credential-guard', 'kiro-git-branch-guard')
|
|
521
805
|
HARNESS_TARGET_IDS.push(`${target.id}-agents`, `${target.id}-skills`)
|
|
522
806
|
}
|
|
523
807
|
|
|
@@ -531,22 +815,38 @@ function buildHarnessTargets(homeRoot, identityConfig, { dryRun, installMissing
|
|
|
531
815
|
const targets = []
|
|
532
816
|
if (include('claude-skill')) targets.push(claudeSkillTarget(homeRoot, { dryRun, installMissing }))
|
|
533
817
|
if (include('claude-credential-guard')) targets.push(credentialGuardTargetClaude(homeRoot, { dryRun, installMissing }))
|
|
818
|
+
if (include('claude-git-branch-guard')) targets.push(gitBranchGuardTargetClaude(homeRoot, { dryRun, installMissing }))
|
|
534
819
|
for (const target of catalog.targets) {
|
|
535
820
|
if (target.id === 'codex' && include('codex-credential-guard')) {
|
|
536
821
|
targets.push(credentialGuardTargetCodex(homeRoot, { dryRun, installMissing }))
|
|
537
822
|
}
|
|
823
|
+
if (target.id === 'codex' && include('codex-git-branch-guard')) {
|
|
824
|
+
targets.push(gitBranchGuardTargetCodex(homeRoot, { dryRun, installMissing }))
|
|
825
|
+
}
|
|
538
826
|
if (target.id === 'gemini' && include('gemini-credential-guard')) {
|
|
539
827
|
targets.push(credentialGuardTargetGemini(homeRoot, { dryRun, installMissing }))
|
|
540
828
|
}
|
|
829
|
+
if (target.id === 'gemini' && include('gemini-git-branch-guard')) {
|
|
830
|
+
targets.push(gitBranchGuardTargetGemini(homeRoot, { dryRun, installMissing }))
|
|
831
|
+
}
|
|
541
832
|
if (target.id === 'cursor' && include('cursor-credential-guard')) {
|
|
542
833
|
targets.push(credentialGuardTargetCursor(homeRoot, { dryRun, installMissing }))
|
|
543
834
|
}
|
|
835
|
+
if (target.id === 'cursor' && include('cursor-git-branch-guard')) {
|
|
836
|
+
targets.push(gitBranchGuardTargetCursor(homeRoot, { dryRun, installMissing }))
|
|
837
|
+
}
|
|
544
838
|
if (target.id === 'copilot' && include('copilot-credential-guard')) {
|
|
545
839
|
targets.push(credentialGuardTargetCopilot(homeRoot, { dryRun, installMissing }))
|
|
546
840
|
}
|
|
841
|
+
if (target.id === 'copilot' && include('copilot-git-branch-guard')) {
|
|
842
|
+
targets.push(gitBranchGuardTargetCopilot(homeRoot, { dryRun, installMissing }))
|
|
843
|
+
}
|
|
547
844
|
if (target.id === 'kiro' && include('kiro-credential-guard')) {
|
|
548
845
|
targets.push(credentialGuardTargetKiro(homeRoot, { dryRun, installMissing }))
|
|
549
846
|
}
|
|
847
|
+
if (target.id === 'kiro' && include('kiro-git-branch-guard')) {
|
|
848
|
+
targets.push(gitBranchGuardTargetKiro(homeRoot, { dryRun, installMissing }))
|
|
849
|
+
}
|
|
550
850
|
const agentsId = `${target.id}-agents`
|
|
551
851
|
const skillsId = `${target.id}-skills`
|
|
552
852
|
if (include(agentsId)) targets.push(catalogBundleTarget(target.id, 'agents', homeRoot, identityConfig, { dryRun, installMissing }))
|
|
@@ -627,4 +927,10 @@ module.exports = {
|
|
|
627
927
|
credentialGuardTargetCursor,
|
|
628
928
|
credentialGuardTargetCopilot,
|
|
629
929
|
credentialGuardTargetKiro,
|
|
930
|
+
gitBranchGuardTargetClaude,
|
|
931
|
+
gitBranchGuardTargetCodex,
|
|
932
|
+
gitBranchGuardTargetGemini,
|
|
933
|
+
gitBranchGuardTargetCursor,
|
|
934
|
+
gitBranchGuardTargetCopilot,
|
|
935
|
+
gitBranchGuardTargetKiro,
|
|
630
936
|
}
|