trackfw 3.1.0 → 5.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.
@@ -0,0 +1,208 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const os = require('os')
5
+ const path = require('path')
6
+ const identityStore = require('../identity')
7
+ const { catalog, buildPlans, IntegrationManager, globalGroupPath } = require('../integrations')
8
+ const { tildeify, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine')
9
+
10
+ // `trackfw update harness` is the global counterpart to `trackfw update` —
11
+ // see docs/cli-parity.md, "`trackfw update` vs `trackfw update harness`".
12
+ // It never requires trackfw.yaml or a project cwd, and its only job is to
13
+ // update rules/agents/skills ALREADY INSTALLED under the user's home
14
+ // directory (~/.claude and equivalents). It never installs a target that is
15
+ // not already present unless --install-missing is passed.
16
+ //
17
+ // Target universe (fixed declared order, mirrors internal/commands/
18
+ // update_harness.go and pypi/trackfw/commands/update_harness.py):
19
+ // 1. claude-skill — ~/.claude/skills/trackfw/SKILL.md (legacy
20
+ // governance meta-skill, previously written by the
21
+ // removed `update` step "skill global"; this is the
22
+ // exact target named in the frozen contract
23
+ // example in docs/cli-parity.md).
24
+ // 2..N. <tool>-agents / <tool>-skills — one target per (catalog target,
25
+ // kind) pair, in catalog.targets declared order,
26
+ // covering the project-independent, home-rooted
27
+ // agents/skills catalog (npm/src/integrations).
28
+ //
29
+ // Ambiguity (reported, not resolved unilaterally — see ML-6C handoff
30
+ // report): each <tool>-<kind> target aggregates potentially many catalog
31
+ // items (e.g. claude-agents bundles all 12 agent personas) into a single
32
+ // state. The contract does not specify item-level granularity for harness
33
+ // targets, so this implementation reports the bundle-level outcome:
34
+ // - every item not-installed -> missing
35
+ // - any item unmanaged/modified -> skipped (never overwritten)
36
+ // - otherwise, at least one item written -> updated
37
+ // - otherwise (all current) -> skipped
38
+
39
+ // claudeSkillContent — mirrors generators/init.js:installSkillsForce
40
+ // (npm/src/generators/init.js:1242) byte-for-byte. Duplicated here, not
41
+ // imported, because installSkillsForce() bakes os.homedir() internally with
42
+ // an unconditional overwrite and no dry-run mode — it cannot be reused
43
+ // without either mutating on --dry-run or forking its signature. Reported
44
+ // as a follow-up cleanup opportunity in the ML-6C handoff report.
45
+ function claudeSkillContent() {
46
+ return `---
47
+ name: trackfw
48
+ description: "trackfw — Governed Software Delivery: ADR → REQ → ROADMAP → kanban"
49
+ signature: "📦 trackfw - Governed Delivery"
50
+ ---
51
+
52
+ # trackfw — Modo de Operação
53
+
54
+ Este projeto usa **trackfw** para governança de entrega de software.
55
+ Cadeia: **ADR → REQ → ROADMAP** · Estados: \`backlog / wip / blocked / done / abandoned\`
56
+
57
+ ## Comandos principais
58
+
59
+ - \`trackfw context\` — contexto de trabalho atual (sempre execute primeiro)
60
+ - \`trackfw status\` — todos os artefatos e estados
61
+ - \`trackfw validate\` — valida consistência de governança
62
+ - \`trackfw roadmap move <nome> <estado>\` — transição de estado
63
+ - \`trackfw serve\` — board Kanban em http://localhost:4080
64
+
65
+ ## Protocolo de agente
66
+
67
+ 1. Antes de iniciar: \`trackfw context\` + ler \`docs/agents-working-context.md\`
68
+ 2. Após concluir: atualizar \`docs/agents-working-context.md\`
69
+ 3. Antes de PR: \`trackfw validate\` deve passar com zero violations
70
+ `
71
+ }
72
+
73
+ function claudeSkillTarget(homeRoot, { dryRun, installMissing }) {
74
+ const id = 'claude-skill'
75
+ const filePath = path.join(homeRoot, '.claude', 'skills', 'trackfw', 'SKILL.md')
76
+ const displayPath = tildeify(homeRoot, filePath)
77
+ const desired = claudeSkillContent()
78
+
79
+ try {
80
+ const exists = fs.existsSync(filePath)
81
+ const actual = exists ? fs.readFileSync(filePath, 'utf8') : null
82
+
83
+ if (!exists && !installMissing) return { id, state: 'missing', path: displayPath }
84
+ if (exists && actual === desired) return { id, state: 'skipped', path: displayPath }
85
+
86
+ if (dryRun) return { id, state: 'updated', path: displayPath }
87
+
88
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
89
+ fs.writeFileSync(filePath, desired, 'utf8')
90
+ return { id, state: 'updated', path: displayPath }
91
+ } catch (e) {
92
+ return { id, state: 'failed', path: displayPath, message: e.message }
93
+ }
94
+ }
95
+
96
+ // catalogBundleTarget — one target per (tool, kind) pair at global scope.
97
+ // Uses IntegrationManager.inspect (read-only) to classify every catalog
98
+ // item under that pair, then only calls manager.update() for the subset
99
+ // that needs a write — and never for --dry-run. displayPath is derived from
100
+ // the catalog itself (globalGroupPath), not from any individual plan's
101
+ // destination, so it never depends on catalog item iteration order (this is
102
+ // what previously caused the claude-skills path to diverge from the Python
103
+ // CLI — see docs/cli-parity.md, "Declared harness targets — pinned list").
104
+ function catalogBundleTarget(toolId, kind, homeRoot, identityConfig, { dryRun, installMissing }) {
105
+ const id = `${toolId}-${kind}`
106
+ let displayPath = `~/.${toolId}`
107
+ try {
108
+ displayPath = globalGroupPath(toolId, kind)
109
+ const plans = buildPlans(kind, { targets: [toolId], scope: 'global', identity: identityConfig })
110
+ if (!plans.length) return { id, state: 'missing', path: displayPath }
111
+
112
+ const manager = new IntegrationManager({ homeRoot })
113
+ const statuses = manager.inspect(plans)
114
+
115
+ const allNotInstalled = statuses.every((s) => s.state === 'not-installed')
116
+ const anyModified = statuses.some((s) => s.state === 'modified')
117
+
118
+ if (allNotInstalled && !installMissing) return { id, state: 'missing', path: displayPath }
119
+
120
+ const toWrite = plans.filter((_, index) => {
121
+ const state = statuses[index].state
122
+ if (state === 'outdated') return true
123
+ if (state === 'not-installed') return installMissing
124
+ return false
125
+ })
126
+
127
+ if (!toWrite.length) {
128
+ if (allNotInstalled) return { id, state: 'missing', path: displayPath }
129
+ return { id, state: 'skipped', path: displayPath }
130
+ }
131
+
132
+ if (!dryRun) manager.update(toWrite)
133
+ return { id, state: 'updated', path: displayPath }
134
+ } catch (e) {
135
+ return { id, state: 'failed', path: displayPath, message: e.message }
136
+ }
137
+ }
138
+
139
+ const HARNESS_TARGET_IDS = ['claude-skill']
140
+ for (const target of catalog.targets) {
141
+ HARNESS_TARGET_IDS.push(`${target.id}-agents`, `${target.id}-skills`)
142
+ }
143
+
144
+ // buildHarnessTargets — `wanted` (nullable) restricts which targets are
145
+ // even computed/applied. This must happen HERE, not as a post-hoc filter
146
+ // on the returned array: catalogBundleTarget/claudeSkillTarget call
147
+ // manager.update() as a side effect, so filtering after construction would
148
+ // still have mutated every unrequested target's files on disk.
149
+ function buildHarnessTargets(homeRoot, identityConfig, { dryRun, installMissing }, wanted) {
150
+ const include = (id) => !wanted || wanted.includes(id)
151
+ const targets = []
152
+ if (include('claude-skill')) targets.push(claudeSkillTarget(homeRoot, { dryRun, installMissing }))
153
+ for (const target of catalog.targets) {
154
+ const agentsId = `${target.id}-agents`
155
+ const skillsId = `${target.id}-skills`
156
+ if (include(agentsId)) targets.push(catalogBundleTarget(target.id, 'agents', homeRoot, identityConfig, { dryRun, installMissing }))
157
+ if (include(skillsId)) targets.push(catalogBundleTarget(target.id, 'skills', homeRoot, identityConfig, { dryRun, installMissing }))
158
+ }
159
+ return targets
160
+ }
161
+
162
+ // run — entry point invoked by `trackfw update harness`. Deliberately a
163
+ // plain function, not its own commander.Command: nesting a Command that
164
+ // redeclares the SAME flag names (--json, --dry-run, --targets,
165
+ // --install-missing) as its parent ('update') triggers a commander@12
166
+ // parsing quirk where the flag binds to the ANCESTOR command's opts()
167
+ // instead of the child's, silently producing `{}` in the child action no
168
+ // matter what was passed on the command line (reproduced and confirmed in
169
+ // isolation while building this ML — see the vault note this ML links).
170
+ // `update.js` instead parses `update harness --json` as ITS OWN single
171
+ // command with an optional positional `[mode]` argument, so there is only
172
+ // ever one Option object per flag name; it calls `run(options)` here when
173
+ // `mode === 'harness'`.
174
+ function run(options) {
175
+ let wanted
176
+ try {
177
+ const requested = options.targets ? String(options.targets).split(',').map((s) => s.trim()).filter(Boolean) : []
178
+ wanted = validateTargets(HARNESS_TARGET_IDS, requested)
179
+ } catch (e) {
180
+ console.error(`✗ ${e.message}`)
181
+ process.exit(1)
182
+ }
183
+
184
+ const homeRoot = os.homedir()
185
+ const dryRun = Boolean(options.dryRun)
186
+ const installMissing = Boolean(options.installMissing)
187
+
188
+ // Identidade resolvida do disco antes de buildPlans — pular esta etapa
189
+ // reverteria silenciosamente os nomes customizados para os defaults
190
+ // neutros (mesma justificativa de npm/src/integrations/index.js:execute).
191
+ const identityConfig = identityStore.load(homeRoot)
192
+
193
+ const targets = options.json
194
+ ? silenceConsole(() => buildHarnessTargets(homeRoot, identityConfig, { dryRun, installMissing }, wanted))
195
+ : buildHarnessTargets(homeRoot, identityConfig, { dryRun, installMissing }, wanted)
196
+
197
+ const doc = buildDocument('harness', dryRun, targets)
198
+
199
+ if (options.json) {
200
+ console.log(JSON.stringify(doc, null, 2))
201
+ } else {
202
+ console.log(humanReport('harness', dryRun, targets))
203
+ }
204
+
205
+ if (doc.summary.failed > 0) process.exitCode = 1
206
+ }
207
+
208
+ module.exports = { run, HARNESS_TARGET_IDS, buildHarnessTargets, claudeSkillContent }
@@ -5,6 +5,12 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
7
  const identityStore = require('../identity');
8
+ const { runFileTarget, validateTargets, buildDocument, humanReport, silenceConsole } = require('../lib/update-engine');
9
+
10
+ // `trackfw update` is project-scoped only — see docs/cli-parity.md,
11
+ // "`trackfw update` vs `trackfw update harness`". It must NEVER touch global
12
+ // state (~/.claude, ~/.codex, etc.). Global artifacts moved to
13
+ // `trackfw update harness` (update-harness.js).
8
14
 
9
15
  function readUpdateConfig(rootDir) {
10
16
  const yaml = path.join(rootDir, 'trackfw.yaml');
@@ -50,99 +56,227 @@ function updateHooksSurgical(cfg, rootDir) {
50
56
  }
51
57
  }
52
58
 
53
- const cmd = new Command('update');
54
- cmd.description('Update trackfw-managed artifacts to the current version');
55
- cmd.action(() => {
56
- const cwd = process.cwd();
57
- const yaml = path.join(cwd, 'trackfw.yaml');
58
- if (!fs.existsSync(yaml)) {
59
- console.error('✗ trackfw.yaml não encontrado — execute trackfw init primeiro');
60
- process.exit(1);
59
+ // codexProjectAgentsTarget — installs/reports the project-scoped Codex
60
+ // agents/skills bundle (catalog-based). Uses IntegrationManager.inspect,
61
+ // which is read-only, to compute state under --dry-run too; only the
62
+ // mutating manager.update() call is skipped when dryRun is true. Kept
63
+ // separate from runFileTarget because IntegrationManager already owns a
64
+ // correct, manifest-aware not-installed/current/outdated/modified state
65
+ // machine — re-deriving it via directory hashing would risk diverging from
66
+ // that source of truth (and would need the .trackfw manifest copied into
67
+ // the simulation, which is unnecessary complexity here).
68
+ function codexProjectAgentsTarget(cwd, identityConfig, { dryRun, installMissing }) {
69
+ const id = 'codex-project-agents';
70
+ const displayPath = '.codex/agents, .agents/skills';
71
+ const detected = fs.existsSync(path.join(cwd, 'AGENTS.md')) || fs.existsSync(path.join(cwd, '.codex'));
72
+ if (!detected) return { id, state: 'missing', path: displayPath };
73
+
74
+ try {
75
+ const { buildPlans, IntegrationManager } = require('../integrations');
76
+ const manager = new IntegrationManager({ projectRoot: cwd });
77
+ let wroteAny = false;
78
+ for (const kind of ['agents', 'skills']) {
79
+ const plans = buildPlans(kind, { targets: ['codex'], scope: 'project', identity: identityConfig });
80
+ const statuses = manager.inspect(plans);
81
+ const toWrite = plans.filter((_, index) => {
82
+ const state = statuses[index].state;
83
+ return state === 'outdated' || (installMissing && state === 'not-installed');
84
+ });
85
+ if (toWrite.length) {
86
+ wroteAny = true;
87
+ if (!dryRun) manager.update(toWrite);
88
+ }
89
+ }
90
+ return { id, state: wroteAny ? 'updated' : 'skipped', path: displayPath };
91
+ } catch (e) {
92
+ return { id, state: 'failed', path: displayPath, message: e.message };
61
93
  }
94
+ }
62
95
 
63
- const cfg = readUpdateConfig(cwd);
96
+ // PROJECT_TARGET_IDS — the fixed declared order for `trackfw update`
97
+ // targets. `ci-workflow` and `git-hooks` only appear when the project's
98
+ // trackfw.yaml opted into a CI system / hook framework — see ambiguity
99
+ // note in the ML-6C handoff report about config-conditional target lists.
100
+ const PROJECT_TARGET_IDS = [
101
+ 'agent-rules',
102
+ 'agent-hooks',
103
+ 'codex-project-agents',
104
+ 'validate-script',
105
+ 'ci-workflow',
106
+ 'git-hooks',
107
+ 'claude-commands',
108
+ ];
109
+
110
+ // buildProjectTargets — `wanted` (nullable) restricts which targets are
111
+ // even computed/applied. This must be enforced HERE, before any apply()
112
+ // runs, not as a post-hoc filter on the returned array: every target's
113
+ // apply() is a real filesystem side effect (outside --dry-run), so
114
+ // filtering afterwards would still have written every unrequested target.
115
+ function buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing }, wanted) {
64
116
  const generators = require('../generators/init');
65
117
  const discover = require('./discover');
118
+ const hooksGen = require('../generators/hooks');
119
+ const include = (id) => !wanted || wanted.includes(id);
66
120
 
67
- // A identidade é carregada uma única vez, fora de qualquer try/catch —
68
- // um identity.json corrompido deve abortar o comando inteiro, nunca cair
69
- // silenciosamente para os nomes neutros default.
70
- const identityConfig = identityStore.load(os.homedir());
121
+ const targets = [];
71
122
 
72
- console.log('trackfw update — re-aplicando templates atuais...\n');
123
+ if (include('agent-rules')) targets.push(runFileTarget({
124
+ id: 'agent-rules',
125
+ path: 'CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md, .windsurfrules, .amazonq/developer/guidelines.md, .cursor/rules/trackfw.mdc',
126
+ root: cwd,
127
+ relPaths: ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.github/copilot-instructions.md', '.windsurfrules', '.amazonq/developer/guidelines.md', '.cursor/rules/trackfw.mdc'],
128
+ apply: (root) => generators.injectRulesDetected(root),
129
+ dryRun,
130
+ installMissing,
131
+ }));
73
132
 
74
- // 1. Agent rules (marker-delimited, idempotent)
75
- try {
76
- generators.injectRulesDetected(cwd);
77
- console.log(' ✓ agent rules atualizadas');
78
- } catch (e) {
79
- console.log(` ⚠ agent rules: ${e.message}`);
80
- }
81
- if (fs.existsSync(path.join(cwd, 'AGENTS.md')) || fs.existsSync(path.join(cwd, '.codex'))) {
82
- try {
83
- const { buildPlans, IntegrationManager } = require('../integrations');
84
- const roots = { projectRoot: cwd };
85
- const manager = new IntegrationManager(roots);
86
- for (const kind of ['agents', 'skills']) {
87
- const plans = buildPlans(kind, { targets: ['codex'], scope: 'project', identity: identityConfig });
88
- const statuses = manager.inspect(plans);
89
- const existing = plans.filter((_, index) => statuses[index].state !== 'not-installed');
90
- manager.update(existing);
91
- }
92
- } catch (e) {
93
- console.warn(` ⚠ Codex integration: ${e.message}`);
94
- }
95
- }
133
+ if (include('agent-hooks')) targets.push(runFileTarget({
134
+ id: 'agent-hooks',
135
+ path: '.claude/settings.json, .codex/hooks.json, .gemini/settings.json, .kiro/hooks/trackfw-attention.json, .github/hooks/trackfw-attention.json, .cursor/hooks.json, scripts/trackfw-attention-*.sh',
136
+ root: cwd,
137
+ relPaths: [
138
+ '.claude/settings.json',
139
+ '.codex/hooks.json',
140
+ '.gemini/settings.json',
141
+ '.kiro/hooks/trackfw-attention.json',
142
+ '.github/hooks/trackfw-attention.json',
143
+ '.cursor/hooks.json',
144
+ 'scripts/trackfw-attention-signal.sh',
145
+ 'scripts/trackfw-attention-cleanup.sh',
146
+ ],
147
+ apply: (root) => {
148
+ hooksGen.injectHooksDetected(root);
149
+ hooksGen.generateAttentionScripts(cfg, root);
150
+ },
151
+ dryRun,
152
+ installMissing,
153
+ }));
96
154
 
97
- // 1b. Agent hooks (attention signal/cleanup)
98
- try {
99
- const { injectHooksDetected, generateAttentionScripts } = require('../generators/hooks');
100
- injectHooksDetected(cwd);
101
- generateAttentionScripts(cfg, cwd);
102
- console.log(' ✓ agent hooks atualizados');
103
- } catch (e) {
104
- console.warn(` ⚠ agent hooks: ${e.message}`);
155
+ if (include('codex-project-agents')) targets.push(codexProjectAgentsTarget(cwd, identityConfig, { dryRun, installMissing }));
156
+
157
+ if (include('validate-script')) targets.push(runFileTarget({
158
+ id: 'validate-script',
159
+ path: 'scripts/trackfw-validate.sh',
160
+ root: cwd,
161
+ relPaths: ['scripts/trackfw-validate.sh'],
162
+ // Reuses generators/init.js's generateValidateScript — the SAME
163
+ // generator `trackfw init` uses to write this file — not
164
+ // discover.js's writeValidateScript, which produces a different
165
+ // (simpler, non-per-backend) script and made every `update` re-run
166
+ // report "updated" against a project actually already current
167
+ // (ML-6H fix). readUpdateConfig returns raw trackfw.yaml keys
168
+ // (snake_case, e.g. "pkg_manager"); buildValidateScript expects the
169
+ // camelCase shape used by the rest of the init generators.
170
+ apply: (root) => generators.generateValidateScript({
171
+ backend: cfg.backend,
172
+ frontend: cfg.frontend,
173
+ pkgManager: cfg.pkg_manager,
174
+ }, root),
175
+ dryRun,
176
+ installMissing,
177
+ }));
178
+
179
+ if (include('ci-workflow') && (cfg.ci === 'github-actions' || cfg.ci === 'github_actions')) {
180
+ targets.push(runFileTarget({
181
+ id: 'ci-workflow',
182
+ path: '.github/workflows/trackfw-validate.yml',
183
+ root: cwd,
184
+ relPaths: ['.github/workflows/trackfw-validate.yml'],
185
+ apply: (root) => discover.writeCIWorkflowForce(root),
186
+ dryRun,
187
+ installMissing,
188
+ }));
105
189
  }
106
190
 
107
- // 2. Validate script (trackfw-owned, overwrite)
108
- try {
109
- discover.writeValidateScript(cwd);
110
- console.log(' ✓ scripts/trackfw-validate.sh atualizado');
111
- } catch (e) {
112
- console.log(` ⚠ validate script: ${e.message}`);
191
+ if (include('git-hooks') && (cfg.hooks === 'husky' || cfg.hooks === 'lefthook')) {
192
+ const relPath = cfg.hooks === 'husky' ? '.husky/pre-commit' : 'lefthook.yml';
193
+ targets.push(runFileTarget({
194
+ id: 'git-hooks',
195
+ path: relPath,
196
+ root: cwd,
197
+ relPaths: [relPath],
198
+ apply: (root) => updateHooksSurgical(cfg, root),
199
+ dryRun,
200
+ installMissing,
201
+ }));
113
202
  }
114
203
 
115
- // 3. CI workflow (trackfw-owned, overwrite)
116
- if (cfg.ci === 'github-actions' || cfg.ci === 'github_actions') {
117
- try {
118
- discover.writeCIWorkflowForce(cwd);
119
- console.log(' ✓ .github/workflows/trackfw-validate.yml atualizado');
120
- } catch (e) {
121
- console.log(` ⚠ CI workflow: ${e.message}`);
122
- }
204
+ if (include('claude-commands')) targets.push(runFileTarget({
205
+ id: 'claude-commands',
206
+ path: '.claude/commands/trackfw',
207
+ root: cwd,
208
+ relPaths: ['.claude/commands/trackfw'],
209
+ apply: (root) => generators.generateClaudeCommandsForce(root),
210
+ dryRun,
211
+ installMissing,
212
+ }));
213
+
214
+ return targets;
215
+ }
216
+
217
+ const cmd = new Command('update');
218
+ cmd.description('Update trackfw-managed artifacts. Bare form is project-scoped (never touches global state); `update harness` updates the global harness instead.');
219
+ // `[mode]` is a plain positional argument, not a nested commander.Command —
220
+ // see the long comment in update-harness.js:run for why: a real subcommand
221
+ // redeclaring the same --json/--dry-run/--targets/--install-missing flags
222
+ // as this parent silently drops them (commander@12 quirk, confirmed by
223
+ // reproduction). One Command, one Option per flag, branch on `mode` inside
224
+ // a single action — this is the only structure that parses correctly.
225
+ cmd.argument('[mode]', 'Pass "harness" to update the global harness instead of the current project');
226
+ cmd.option('--dry-run', 'Compute and report states without writing anything');
227
+ cmd.option('--json', 'Emit the result document as JSON');
228
+ cmd.option('--targets <ids>', 'Comma-separated subset of target ids');
229
+ cmd.option('--install-missing', 'Allow missing targets to be installed');
230
+ cmd.action((mode, options) => {
231
+ if (mode === 'harness') {
232
+ return require('./update-harness').run(options);
233
+ }
234
+ if (mode) {
235
+ console.error(`✗ Unknown update mode: ${mode} (expected "harness" or no argument)`);
236
+ process.exit(1);
123
237
  }
124
238
 
125
- // 4. Git hooks (surgical)
126
- updateHooksSurgical(cfg, cwd);
239
+ const cwd = process.cwd();
240
+ const yaml = path.join(cwd, 'trackfw.yaml');
241
+ if (!fs.existsSync(yaml)) {
242
+ console.error('✗ trackfw.yaml não encontrado — execute trackfw init primeiro');
243
+ process.exit(1);
244
+ }
127
245
 
128
- // 5. Claude commands (force overwrite)
246
+ let wanted;
129
247
  try {
130
- generators.generateClaudeCommandsForce(cwd);
131
- console.log(' ✓ .claude/commands/trackfw/ atualizado');
248
+ const requested = options.targets ? String(options.targets).split(',').map((s) => s.trim()).filter(Boolean) : [];
249
+ wanted = validateTargets(PROJECT_TARGET_IDS, requested);
132
250
  } catch (e) {
133
- console.log(` ⚠ Claude commands: ${e.message}`);
251
+ console.error(`✗ ${e.message}`);
252
+ process.exit(1);
134
253
  }
135
254
 
136
- // 6. Global skill (force overwrite)
137
- try {
138
- generators.installSkillsForce(cwd);
139
- console.log(' ✓ skill global atualizada');
140
- } catch (e) {
141
- console.log(` ⚠ skills: ${e.message}`);
255
+ const cfg = readUpdateConfig(cwd);
256
+ const dryRun = Boolean(options.dryRun);
257
+ const installMissing = Boolean(options.installMissing);
258
+
259
+ // A identidade é carregada uma única vez, fora de qualquer try/catch —
260
+ // um identity.json corrompido deve abortar o comando inteiro, nunca cair
261
+ // silenciosamente para os nomes neutros default.
262
+ const identityConfig = identityStore.load(os.homedir());
263
+
264
+ // With --json, stdout must carry only the result document — apply()
265
+ // functions log human progress lines as a side effect; silence them so a
266
+ // consumer parsing --json output never has to skip preamble noise.
267
+ const targets = options.json
268
+ ? silenceConsole(() => buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing }, wanted))
269
+ : buildProjectTargets(cwd, cfg, identityConfig, { dryRun, installMissing }, wanted);
270
+
271
+ const doc = buildDocument('project', dryRun, targets);
272
+
273
+ if (options.json) {
274
+ console.log(JSON.stringify(doc, null, 2));
275
+ } else {
276
+ console.log(humanReport('project', dryRun, targets));
142
277
  }
143
278
 
144
- console.log('\n✓ trackfw update concluído');
145
- require('../generators/init').printArchitectNextSteps(process.cwd())
279
+ if (doc.summary.failed > 0) process.exitCode = 1;
146
280
  });
147
281
 
148
282
  module.exports = cmd;