trackfw 2.16.0 → 3.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 CHANGED
@@ -363,6 +363,15 @@ are never replaced or removed unless `--force` is explicit, and unmanaged files
363
363
  are never removed. Known historical templates are adopted without overwriting;
364
364
  unknown unmanaged content cannot be adopted by `update`, even with `--force`.
365
365
 
366
+ Without `--scope`, `install`/`update` default to `global` (`~/.claude/...`)
367
+ when stdin is not a TTY, and otherwise prompt interactively with `global`
368
+ pre-selected — the resolved destination paths are printed before anything is
369
+ written. `uninstall` is the one exception: without `--scope` and without a
370
+ TTY it fails instead of guessing, since silently defaulting a destructive
371
+ operation could delete artifacts from the user's home directory. `list` never
372
+ prompts and always assumes `global` unless `--scope` is given, so it reports
373
+ the same destinations `install` actually wrote to.
374
+
366
375
  The 10 roles installed for each tool: **architect · backend · frontend · qa · infra · security · code-quality · dba · ux · data**
367
376
 
368
377
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "2.16.0",
3
+ "version": "3.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, and Amazon Q.",
5
5
  "keywords": [
6
6
  "cli",
@@ -4,6 +4,7 @@ const { t } = require('../i18n')
4
4
  const identityStore = require('../identity')
5
5
  const identityWizard = require('./identity-wizard')
6
6
  const { resolveIdentityPreset, identityFileExists } = identityWizard
7
+ const { resolveScope } = require('./integrations')
7
8
 
8
9
  const cmd = new Command('init')
9
10
  cmd.description(t('init.description'))
@@ -48,9 +49,11 @@ cmd.action(async (options, command) => {
48
49
  await generators.scaffold(cfg)
49
50
  const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
50
51
  const supported = new Set(['claude', 'codex', 'gemini', 'antigravity', 'cursor', 'copilot', 'windsurf', 'amazonq', 'kiro'])
52
+ // Sem TTY, o escopo nunca é perguntado: default `global` (ADR D1/D4).
53
+ const scope = await resolveScope({}, { interactive: false })
51
54
  for (const tool of aiTools) {
52
55
  if (!supported.has(tool)) throw new Error(`Unsupported AI tool: ${tool}`)
53
- await generators.installIntegrationTarget(tool, process.cwd())
56
+ await generators.installIntegrationTarget(tool, process.cwd(), scope)
54
57
  }
55
58
  console.log(`\n${t('init.success')}`)
56
59
  require('../generators/init').printArchitectNextSteps(process.cwd())
@@ -59,7 +62,7 @@ cmd.action(async (options, command) => {
59
62
 
60
63
  const { input, select, checkbox } = require('@inquirer/prompts')
61
64
 
62
- let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, aiTools, requireReqInCommit
65
+ let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, aiTools, requireReqInCommit, scope
63
66
 
64
67
  try {
65
68
  projectName = await input({
@@ -184,6 +187,11 @@ cmd.action(async (options, command) => {
184
187
  ],
185
188
  })
186
189
 
190
+ // Escopo de instalação (ADR D4): só pergunta quando há de fato algo para
191
+ // instalar. `resolveScope` é a mesma função (mesmas strings/mecanismo de
192
+ // prompt) usada por `agents install` / `skills install`.
193
+ scope = aiTools.length ? await resolveScope({}, { interactive: true }) : 'global'
194
+
187
195
  } catch (err) {
188
196
  // Fallback quando stdin fecha inesperadamente (ex: pipe em TTY simulado)
189
197
  const cfg = {
@@ -231,7 +239,7 @@ cmd.action(async (options, command) => {
231
239
  const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
232
240
  await generators.scaffold(cfg)
233
241
 
234
- for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd())
242
+ for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd(), scope)
235
243
 
236
244
  console.log(`\n${t('init.success')}`)
237
245
  require('../generators/init').printArchitectNextSteps(process.cwd())
@@ -2,7 +2,7 @@
2
2
 
3
3
  const os = require('node:os')
4
4
  const { Command } = require('commander')
5
- const { catalog, execute, parseSurfaces } = require('../integrations')
5
+ 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')
@@ -27,6 +27,66 @@ async function promptSelection(kind, options, prompts = require('@inquirer/promp
27
27
  options.items = await checkbox({ message: `${kind} to manage`, choices: catalog[kind].map(item => ({ name: item.name, value: item.id })), required: true })
28
28
  }
29
29
 
30
+ // resolveScope decide o escopo de instalação (ADR-2026-07-25-escopo-de-
31
+ // instalacao-selecionavel-para-agents-e-skills):
32
+ //
33
+ // - `--scope` explícito (`options.scope !== undefined`) é sempre respeitado
34
+ // e nunca dispara prompt — apenas validado contra os valores aceitos. A
35
+ // detecção é por *flag-set* (undefined), nunca por comparação de valor,
36
+ // para não confundir um `--scope project` explícito com o default.
37
+ // - Sem flag e `interactive` desligado (comando `list`, D6): default
38
+ // `global`, nunca lança erro (list nunca é destrutivo).
39
+ // - Sem flag e stdin não é um TTY (D1): default `global` — exceto para
40
+ // `uninstall` (ADR D8), que falha exigindo `--scope` explícito. D1 foi
41
+ // aprovado no enquadramento "onde instalar"; aplicá-lo a uninstall
42
+ // permitiria que um script de CI que hoje limpa `.claude/agents/` do
43
+ // repositório passasse a apagar arquivos do home do usuário.
44
+ // - Sem flag, `interactive` ligado e TTY (D2): pergunta o escopo, com
45
+ // `global` pré-selecionado, para toda operação incluindo uninstall — o
46
+ // usuário vê a escolha antes de destruir. Usa o mesmo mecanismo de prompt
47
+ // (`@inquirer/prompts`) já empregado por `identity-wizard.js`, sem
48
+ // dependência nova.
49
+ //
50
+ // `interactive` é passado pelo chamador como `false` para `list` — comando de
51
+ // leitura que nunca deve bloquear em prompt, mas que ainda assim precisa do
52
+ // mesmo default `global` para não reportar deployments divergentes dos que o
53
+ // `install` gravou (D6). `operation` identifica install/update/uninstall/list
54
+ // para o branch de D8. Espelha
55
+ // internal/commands/integrations_flags.go:resolveScope.
56
+ async function resolveScope(options, { interactive = true, operation, prompts = require('@inquirer/prompts') } = {}) {
57
+ if (options.scope !== undefined) {
58
+ if (options.scope !== 'project' && options.scope !== 'global') throw new Error(`Unsupported scope: ${options.scope}`)
59
+ return options.scope
60
+ }
61
+ if (!interactive) return 'global'
62
+ if (!process.stdin.isTTY) {
63
+ if (operation === 'uninstall') throw new Error('uninstall requires --scope in non-interactive mode')
64
+ return 'global'
65
+ }
66
+ const { select } = prompts
67
+ return select({
68
+ message: 'Onde instalar os artefatos?',
69
+ default: 'global',
70
+ choices: [
71
+ { name: 'Pasta do usuário (~/.claude) — vale para todos os projetos', value: 'global' },
72
+ { name: 'Este projeto (.claude) — apenas neste repositório', value: 'project' },
73
+ ],
74
+ })
75
+ }
76
+
77
+ // printResolvedDestinations imprime, antes da gravação, o escopo escolhido e
78
+ // os caminhos de destino que serão afetados (ADR D5) — nunca em modo --json,
79
+ // que já é o canal determinístico consumido por scripts. Recalcula os planos
80
+ // via buildPlans (sem side effect) só para enumerar destinos; a gravação em
81
+ // si acontece depois, em execute().
82
+ function printResolvedDestinations(kind, options) {
83
+ if (options.json) return
84
+ const plans = buildPlans(kind, options)
85
+ const destinations = [...new Set(plans.map(plan => plan.destination))].sort()
86
+ console.log(`Destino (${options.scope}):`)
87
+ for (const destination of destinations) console.log(` ${destination}`)
88
+ }
89
+
30
90
  async function promptAmbiguousSurfaces(kind, options, prompts = require('@inquirer/prompts')) {
31
91
  const { select } = prompts
32
92
  const selected = parseSurfaces(options.surfaces)
@@ -47,7 +107,7 @@ function createLifecycleCommand(kind) {
47
107
  const command = new Command(operation)
48
108
  .option('--targets <targets>', 'Comma-separated target CLIs', csv)
49
109
  .option('--items <items>', `Comma-separated ${kind} IDs`, csv)
50
- .option('--scope <scope>', 'Installation scope: project or global', 'project')
110
+ .option('--scope <scope>', 'Installation scope: project or global (default: global; asks interactively)')
51
111
  .option('--surface <target=surface>', 'Surface selection (repeatable)', collect, [])
52
112
  .option('--json', 'Print deterministic JSON')
53
113
  .option('--force', 'Replace or remove modified artifacts')
@@ -65,7 +125,14 @@ function createLifecycleCommand(kind) {
65
125
 
66
126
  command.action(async (options, cmd) => {
67
127
  options.surfaces = options.surface || []
68
- if (options.scope !== 'project' && options.scope !== 'global') throw new Error(`Unsupported scope: ${options.scope}`)
128
+
129
+ // Gate de escopo (ADR D1-D3, D6): resolvido incondicionalmente aqui,
130
+ // antes de qualquer seleção de targets/surfaces ou construção de
131
+ // planos, e independentemente de --targets já ter sido informado —
132
+ // caso contrário o caso mais comum (`agents install --targets claude`)
133
+ // nunca passaria por prompt algum. `list` (mutation === false) nunca
134
+ // pergunta (comando de leitura), apenas adota o default `global`.
135
+ options.scope = await resolveScope(options, { interactive: mutation, operation })
69
136
 
70
137
  // O booleano de --identity nunca deve chegar a execute()/buildPlans()
71
138
  // sob a chave "identity" — essa chave ali é reservada para uma Config
@@ -112,6 +179,10 @@ function createLifecycleCommand(kind) {
112
179
  }
113
180
  }
114
181
 
182
+ // D5: caminhos de destino impressos antes da gravação, apenas para
183
+ // operações de mutação (install/update/uninstall) e fora de --json.
184
+ if (mutation) printResolvedDestinations(kind, options)
185
+
115
186
  const output = execute(kind, operation, options)
116
187
  console.log(options.json ? JSON.stringify(output) : human(output))
117
188
  })
@@ -120,4 +191,4 @@ function createLifecycleCommand(kind) {
120
191
  return root
121
192
  }
122
193
 
123
- module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces }
194
+ module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces, resolveScope }
@@ -1006,12 +1006,35 @@ async function installAmazonQ(cwd = process.cwd()) {
1006
1006
  }
1007
1007
  }
1008
1008
 
1009
- /** Install the canonical agents and skills for one AI target. */
1010
- async function installIntegrationTarget(target, cwd = process.cwd()) {
1011
- const { execute } = require('../integrations')
1009
+ /**
1010
+ * Install the canonical agents and skills for one AI target.
1011
+ *
1012
+ * `scope` defaults to "project" for backward compatibility with the
1013
+ * compatibility entrypoints below (installAgents, installGemini, etc.), that
1014
+ * historical tests exercise directly without going through the `init`
1015
+ * wizard's scope prompt. `trackfw init` itself always passes the scope it
1016
+ * resolved (ADR-2026-07-25-escopo-de-instalacao-selecionavel-para-agents-e-
1017
+ * skills, D4).
1018
+ */
1019
+ async function installIntegrationTarget(target, cwd = process.cwd(), scope = 'project') {
1020
+ const { execute, buildPlans } = require('../integrations')
1012
1021
  const roots = { projectRoot: cwd }
1013
- execute('agents', 'install', { targets: [target], scope: 'project' }, roots)
1014
- execute('skills', 'install', { targets: [target], scope: 'project' }, roots)
1022
+ const options = { targets: [target], scope }
1023
+ // D5 — transparency: print resolved destinations before writing anything.
1024
+ // buildPlans has no side effects, so it is safe to call here purely to
1025
+ // enumerate destinations; the actual write happens below via execute().
1026
+ // Mirrors printResolvedDestinations in commands/integrations.js and Go's
1027
+ // installAITools (internal/commands/init.go) — this call site (init's AI
1028
+ // tools install) was the one place that stayed silent (divergence #2,
1029
+ // ROADMAP-2026-07-25-escopo-de-instalacao-selecionavel-para-agents-e-skills).
1030
+ const destinations = [
1031
+ ...buildPlans('agents', options).map(plan => plan.destination),
1032
+ ...buildPlans('skills', options).map(plan => plan.destination),
1033
+ ]
1034
+ console.log(`Destino (${scope}):`)
1035
+ for (const destination of [...new Set(destinations)].sort()) console.log(` ${destination}`)
1036
+ execute('agents', 'install', options, roots)
1037
+ execute('skills', 'install', options, roots)
1015
1038
  console.log(` ✓ ${target} agents and skills`)
1016
1039
  }
1017
1040