trackfw 3.1.0 → 4.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 +85 -1
- package/package.json +1 -1
- package/src/commands/barrier.js +358 -0
- package/src/commands/help.js +99 -9
- package/src/commands/index.js +1 -0
- package/src/commands/update-harness.js +208 -0
- package/src/commands/update.js +206 -72
- package/src/generators/init.js +124 -36
- package/src/integrations/assets/agents/architect.md +4 -1
- package/src/integrations/assets/agents/backend.md +3 -3
- package/src/integrations/assets/agents/code-quality.md +3 -0
- package/src/integrations/assets/agents/data.md +3 -3
- package/src/integrations/assets/agents/dba.md +3 -3
- package/src/integrations/assets/agents/frontend.md +3 -3
- package/src/integrations/assets/agents/iac.md +3 -3
- package/src/integrations/assets/agents/infra.md +3 -3
- package/src/integrations/assets/agents/qa.md +3 -3
- package/src/integrations/assets/agents/security.md +3 -0
- package/src/integrations/assets/agents/tooling.md +3 -3
- package/src/integrations/assets/agents/ux.md +3 -0
- package/src/integrations/catalog.js +25 -1
- package/src/integrations/index.js +20 -2
- package/src/lib/update-engine.js +217 -0
- package/src/validator/index.js +2 -0
|
@@ -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 }
|
package/src/commands/update.js
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
246
|
+
let wanted;
|
|
129
247
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
251
|
+
console.error(`✗ ${e.message}`);
|
|
252
|
+
process.exit(1);
|
|
134
253
|
}
|
|
135
254
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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;
|