trackfw 6.5.0 → 6.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "6.5.0",
3
+ "version": "6.6.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",
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const path = require('path')
4
+ const os = require('os')
3
5
  const { Command } = require('commander')
4
6
  const { input } = require('@inquirer/prompts')
5
7
  const generators = require('../generators/adr')
@@ -8,9 +10,29 @@ const { t } = require('../i18n')
8
10
  const cmd = new Command('adr')
9
11
  cmd.description(t('adr.description'))
10
12
 
13
+ /**
14
+ * Resolve o diretório de ADRs conforme --scope.
15
+ * 'project' (default) preserva o comportamento atual (adrDirs[0] do trackfw.yaml).
16
+ * 'global' escreve/lista em ~/.trackfw/adr, sem exigir trackfw.yaml no cwd.
17
+ * @param {string} scope
18
+ * @returns {string}
19
+ */
20
+ function resolveAdrDir(scope) {
21
+ if (scope === 'global') {
22
+ return path.join(os.homedir(), '.trackfw', 'adr')
23
+ }
24
+ if (scope === 'project') {
25
+ return require('../config').load().adrDirs[0]
26
+ }
27
+ console.error(`Invalid --scope value: "${scope}". Expected "project" or "global".`)
28
+ process.exit(1)
29
+ }
30
+
11
31
  cmd.command('new <title>')
12
32
  .description(t('adr.new.description'))
13
- .action(async (title) => {
33
+ .option('--scope <scope>', 'ADR scope: project (default) or global (~/.trackfw/adr)', 'project')
34
+ .action(async (title, options) => {
35
+ const adrDir = resolveAdrDir(options.scope)
14
36
  const content = { title }
15
37
  // wizard interativo se TTY
16
38
  if (process.stdin.isTTY) {
@@ -19,13 +41,15 @@ cmd.command('new <title>')
19
41
  content.consequences = await input({ message: t('adr.new.prompt.consequences'), default: '' })
20
42
  content.alternatives = await input({ message: t('adr.new.prompt.alternatives'), default: '' })
21
43
  }
22
- await generators.newADR(content)
44
+ await generators.newADR(content, adrDir)
23
45
  })
24
46
 
25
47
  cmd.command('list')
26
48
  .description(t('adr.list.description'))
27
- .action(async () => {
28
- await generators.listADRs(require('../config').load().adrDirs[0])
49
+ .option('--scope <scope>', 'ADR scope: project (default) or global (~/.trackfw/adr)', 'project')
50
+ .action(async (options) => {
51
+ const adrDir = resolveAdrDir(options.scope)
52
+ await generators.listADRs(adrDir)
29
53
  })
30
54
 
31
55
  module.exports = cmd
@@ -1,4 +1,6 @@
1
1
  'use strict'
2
+ const path = require('path')
3
+ const os = require('os')
2
4
  const { Command } = require('commander')
3
5
  const { listREQs, moveREQ } = require('../generators/req')
4
6
  const { t } = require('../i18n')
@@ -26,6 +28,19 @@ cmd.command('new <title>')
26
28
  // Form 2 — critérios de aceite
27
29
  content.criteria = await input({ message: t('req.new.prompt.criteria'), default: '- [ ]\n- [ ]' })
28
30
 
31
+ // Escopo dos ADR drafts desta sessão (uma única pergunta, vale para todos)
32
+ const adrScope = await select({
33
+ message: t('req.new.prompt.adrScope'),
34
+ choices: [
35
+ { name: t('req.new.prompt.adrScopeLocal'), value: 'local' },
36
+ { name: t('req.new.prompt.adrScopeGlobal'), value: 'global' },
37
+ ],
38
+ default: 'local',
39
+ })
40
+ const adrDir = adrScope === 'global'
41
+ ? path.join(os.homedir(), '.trackfw', 'adr')
42
+ : require('../config').load().adrDirs[0]
43
+
29
44
  // Perguntas dinâmicas por probe
30
45
  const generatedADRs = []
31
46
  for (const probe of probes) {
@@ -40,7 +55,7 @@ cmd.command('new <title>')
40
55
  })
41
56
  if (answer) {
42
57
  try {
43
- const basename = await adrGenerators.newADRDraft(answer)
58
+ const basename = await adrGenerators.newADRDraft(answer, adrDir)
44
59
  if (basename) generatedADRs.push(basename)
45
60
  } catch (e) {
46
61
  console.warn(t('req.new.adrWarning', { slug: answer, message: e.message }))
@@ -6,7 +6,7 @@ const path = require('path')
6
6
  const identityStore = require('../identity')
7
7
  const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
8
8
  const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
9
- const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray } = require('../generators/hooks')
9
+ const { mergeClaudeHookArray, mergeSimpleCommandArray, mergeCopilotHookArray, generateGlobalCredentialGuardScript } = require('../generators/hooks')
10
10
 
11
11
  // `trackfw update harness` is the global counterpart to `trackfw update` —
12
12
  // see docs/cli-parity.md, "`trackfw update` vs `trackfw update harness`".
@@ -581,6 +581,19 @@ function run(options) {
581
581
  const dryRun = Boolean(options.dryRun)
582
582
  const installMissing = Boolean(options.installMissing)
583
583
 
584
+ // The per-CLI *-credential-guard targets below only wire hook entries that
585
+ // point at ~/.trackfw/scripts/trackfw-credential-guard.sh — none of them
586
+ // write the script itself (ADR-2026-08-06, decision #2/#3). Without this
587
+ // call the wiring is installed but every hook invocation fails with
588
+ // "No such file or directory" because the script never exists.
589
+ if (!dryRun) {
590
+ if (options.json) {
591
+ silenceConsole(() => generateGlobalCredentialGuardScript(homeRoot))
592
+ } else {
593
+ generateGlobalCredentialGuardScript(homeRoot)
594
+ }
595
+ }
596
+
584
597
  // Identidade resolvida do disco antes de buildPlans — pular esta etapa
585
598
  // reverteria silenciosamente os nomes customizados para os defaults
586
599
  // neutros (mesma justificativa de npm/src/integrations/index.js:execute).
@@ -29,6 +29,83 @@ function loadUpdateConfig(rootDir) {
29
29
  };
30
30
  }
31
31
 
32
+ // ensureGlobalAdrDirRegistered — mirrors internal/generators/update.go's
33
+ // ensureGlobalADRDirRegistered (Go implementation, same REQ, ML-1A). Registers
34
+ // ~/.trackfw/adr in the project's trackfw.yaml `adr_dirs` list, but ONLY when
35
+ // that directory exists AND contains at least one `ADR-*.md` file — an empty
36
+ // or absent global ADR dir is a no-op, never written. The edit is surgical
37
+ // (line-based text splice, not a YAML parse+reserialize) to preserve 100% of
38
+ // the user's existing trackfw.yaml content (comments, key order, formatting).
39
+ function ensureGlobalAdrDirRegistered(cwd) {
40
+ const home = os.homedir();
41
+ const globalDir = path.join(home, '.trackfw', 'adr');
42
+ if (!fs.existsSync(globalDir)) return;
43
+
44
+ let hasAdr = false;
45
+ try {
46
+ hasAdr = fs.readdirSync(globalDir).some((f) => /^ADR-.*\.md$/.test(f));
47
+ } catch (_) {
48
+ return;
49
+ }
50
+ if (!hasAdr) return;
51
+
52
+ const yamlPath = path.join(cwd, 'trackfw.yaml');
53
+ let content;
54
+ try {
55
+ content = fs.readFileSync(yamlPath, 'utf8');
56
+ } catch (_) {
57
+ return;
58
+ }
59
+
60
+ const resolvesToGlobal = (entry) => {
61
+ const trimmed = entry.trim();
62
+ if (trimmed === '~/.trackfw/adr') return true;
63
+ const expanded = trimmed.startsWith('~') ? path.join(home, trimmed.slice(1)) : trimmed;
64
+ return path.resolve(expanded) === path.resolve(globalDir);
65
+ };
66
+
67
+ const lines = content.split('\n');
68
+ let adrDirsIndex = -1;
69
+ for (let i = 0; i < lines.length; i++) {
70
+ if (/^adr_dirs:\s*$/.test(lines[i])) {
71
+ adrDirsIndex = i;
72
+ break;
73
+ }
74
+ }
75
+
76
+ if (adrDirsIndex === -1) {
77
+ // No adr_dirs key at all — the implicit default is `docs/adr`; append a
78
+ // new block preserving that default explicitly, plus the global entry.
79
+ let newContent = content;
80
+ if (newContent.length && !newContent.endsWith('\n')) newContent += '\n';
81
+ newContent += 'adr_dirs:\n - docs/adr\n - ~/.trackfw/adr\n';
82
+ fs.writeFileSync(yamlPath, newContent, 'utf8');
83
+ console.log(' ✓ adr_dirs: ~/.trackfw/adr registrado');
84
+ return;
85
+ }
86
+
87
+ const itemRe = /^(\s*)-\s*(.+?)\s*$/;
88
+ let end = adrDirsIndex + 1;
89
+ let lastItemIndex = -1;
90
+ let indent = ' ';
91
+ while (end < lines.length) {
92
+ const m = lines[end].match(itemRe);
93
+ if (!m) break;
94
+ indent = m[1] || indent;
95
+ if (resolvesToGlobal(m[2])) {
96
+ // Already registered under this or an equivalent path form — no-op.
97
+ return;
98
+ }
99
+ lastItemIndex = end;
100
+ end++;
101
+ }
102
+
103
+ const insertAt = lastItemIndex === -1 ? adrDirsIndex + 1 : lastItemIndex + 1;
104
+ lines.splice(insertAt, 0, `${indent}- ~/.trackfw/adr`);
105
+ fs.writeFileSync(yamlPath, lines.join('\n'), 'utf8');
106
+ console.log(' ✓ adr_dirs: ~/.trackfw/adr registrado');
107
+ }
108
+
32
109
  function updateHooksSurgical(cfg, rootDir) {
33
110
  const hooks = cfg.hooks || '';
34
111
  if (hooks === 'husky') {
@@ -243,6 +320,8 @@ cmd.action((mode, options) => {
243
320
  process.exit(1);
244
321
  }
245
322
 
323
+ ensureGlobalAdrDirRegistered(cwd);
324
+
246
325
  let wanted;
247
326
  try {
248
327
  const requested = options.targets ? String(options.targets).split(',').map((s) => s.trim()).filter(Boolean) : [];
@@ -43,13 +43,13 @@ function today() {
43
43
  }
44
44
 
45
45
  /**
46
- * Cria um novo ADR em docs/adr/ADR-YYYY-MM-DD-<slug>.md.
46
+ * Cria um novo ADR em <adrDir>/ADR-YYYY-MM-DD-<slug>.md.
47
47
  * Campos vazios recebem placeholder HTML.
48
48
  * @param {{ title: string, context?: string, decision?: string, consequences?: string, alternatives?: string }} content
49
+ * @param {string} adrDir Diretório de destino (resolvido pelo chamador conforme --scope)
49
50
  * @returns {Promise<void>}
50
51
  */
51
- async function newADR(content) {
52
- const adrDir = require('../config').load().adrDirs[0]
52
+ async function newADR(content, adrDir) {
53
53
  fs.mkdirSync(adrDir, { recursive: true })
54
54
 
55
55
  const slug = toSlug(content.title)
@@ -142,10 +142,14 @@ function parseADRStatus(filepath) {
142
142
  * Cria um ADR com Status: Draft a partir de um slug.
143
143
  * Idempotente: se já existe ADR-*-<slug>.md, pula e imprime mensagem.
144
144
  * @param {string} slug
145
+ * @param {string} [adrDir] Diretório de destino (resolvido pelo chamador conforme escopo).
146
+ * Se omitido, preserva o comportamento anterior (adrDirs[0] do trackfw.yaml).
145
147
  * @returns {Promise<string>} basename do arquivo criado
146
148
  */
147
- async function newADRDraft(slug) {
148
- const adrDir = require('../config').load().adrDirs[0]
149
+ async function newADRDraft(slug, adrDir) {
150
+ if (!adrDir) {
151
+ adrDir = require('../config').load().adrDirs[0]
152
+ }
149
153
  fs.mkdirSync(adrDir, { recursive: true })
150
154
 
151
155
  // Verificar idempotência: buscar arquivo existente com o mesmo slug
@@ -1,10 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const fs = require('fs')
4
- const path = require('path')
5
- const { injectCodexHooks } = require('./hooks')
6
- const { injectRulesForTool } = require('./init')
7
-
8
3
  const skills = {
9
4
  'trackfw-governance': `---
10
5
  name: trackfw-governance
@@ -81,23 +76,4 @@ Review the branch as an owner. Return findings first, ordered by severity, with
81
76
  `,
82
77
  }
83
78
 
84
- function installCodex(cwd) {
85
- const root = cwd || process.cwd()
86
- const { execute } = require('../integrations')
87
- execute('agents', 'install', { targets: ['codex'], scope: 'project' }, { projectRoot: root })
88
- execute('skills', 'install', { targets: ['codex'], scope: 'project' }, { projectRoot: root })
89
- injectRulesForTool('codex', root)
90
-
91
- const configPath = path.join(root, '.codex', 'config.toml')
92
- fs.mkdirSync(path.dirname(configPath), { recursive: true })
93
- let config = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : ''
94
- if (!/^\[agents\]\s*$/m.test(config)) {
95
- config = config.replace(/\s*$/, '') + '\n\n[agents]\nmax_threads = 6\nmax_depth = 1\n'
96
- fs.writeFileSync(configPath, config, 'utf8')
97
- }
98
-
99
- injectCodexHooks(root)
100
- console.log(' ✓ Codex: AGENTS.md, skills, custom agents and hooks')
101
- }
102
-
103
- module.exports = { installCodex, legacyCodexFixtures: { skills, agents } }
79
+ module.exports = { legacyCodexFixtures: { skills, agents } }
@@ -63,7 +63,10 @@
63
63
  "domainQuestion_persistence": "Which database engine will be used?",
64
64
  "domainQuestion_api": "Which API protocol will be used?",
65
65
  "domainQuestion_deploy": "What is the deployment target?",
66
- "domainQuestion_events": "Which event broker will be used?"
66
+ "domainQuestion_events": "Which event broker will be used?",
67
+ "adrScope": "Scope of ADRs for this REQ",
68
+ "adrScopeLocal": "Local (default)",
69
+ "adrScopeGlobal": "Global (~/.trackfw/adr, cross-project)"
67
70
  },
68
71
  "detectedDomains": "Detected domains: {{domains}}",
69
72
  "created": "✓ REQ created: {{path}}",
@@ -63,7 +63,10 @@
63
63
  "domainQuestion_persistence": "¿Qué motor de base de datos se usará?",
64
64
  "domainQuestion_api": "¿Qué protocolo de API se usará?",
65
65
  "domainQuestion_deploy": "¿Cuál es el destino de despliegue?",
66
- "domainQuestion_events": "¿Qué message broker se usará?"
66
+ "domainQuestion_events": "¿Qué message broker se usará?",
67
+ "adrScope": "Alcance de los ADRs de este REQ",
68
+ "adrScopeLocal": "Local (predeterminado)",
69
+ "adrScopeGlobal": "Global (~/.trackfw/adr, entre proyectos)"
67
70
  },
68
71
  "detectedDomains": "Dominios detectados: {{domains}}",
69
72
  "created": "✓ REQ creado: {{path}}",
@@ -63,7 +63,10 @@
63
63
  "domainQuestion_persistence": "Qual engine de banco de dados será usada?",
64
64
  "domainQuestion_api": "Qual protocolo de API será usado?",
65
65
  "domainQuestion_deploy": "Qual é o destino de deploy?",
66
- "domainQuestion_events": "Qual message broker será usado?"
66
+ "domainQuestion_events": "Qual message broker será usado?",
67
+ "adrScope": "Escopo dos ADRs desta REQ",
68
+ "adrScopeLocal": "Local (padrão)",
69
+ "adrScopeGlobal": "Global (~/.trackfw/adr, entre projetos)"
67
70
  },
68
71
  "detectedDomains": "Domínios detectados: {{domains}}",
69
72
  "created": "✓ REQ criado: {{path}}",