trackfw 7.0.0 → 7.2.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.
@@ -3,14 +3,32 @@
3
3
  const os = require('node:os')
4
4
  const { Command } = require('commander')
5
5
  const { catalog, execute, parseSurfaces, buildPlans } = require('../integrations')
6
+ const { surfaceFor, readAsset } = require('../integrations/catalog')
7
+ const { markdownParts, resolveAgentModel, looksLikeSuspectModelValue } = require('../integrations/render')
6
8
  const identityStore = require('../identity')
7
9
  const identityWizard = require('./identity-wizard')
8
10
  const { t } = require('../i18n')
9
11
  const { createThirdPartyCommand } = require('./thirdparty')
12
+ const configModule = require('../config')
10
13
 
11
14
  const csv = value => String(value).split(',').map(entry => entry.trim()).filter(Boolean)
12
15
  const collect = (value, previous) => previous.concat(value)
13
16
 
17
+ // forceHelp returns the --force help text for a mutation subcommand. The
18
+ // three operations grant --force different powers, and a single shared
19
+ // string previously overstated update/uninstall's reach while never
20
+ // mentioning install's ability to adopt unmanaged bytes — that ambiguity is
21
+ // what sent a user straight into the "Unmanaged artifact does not match a
22
+ // trackfw template" error on `update --force` (see unmanagedArtifactError in
23
+ // src/integrations/manager.js for the matching remediation). Mirrors
24
+ // internal/commands/integrations_flags.go:forceHelp and
25
+ // pypi/trackfw/integrations/command.py.
26
+ function forceHelp(operation) {
27
+ if (operation === 'install') return 'Replace a modified managed artifact, or adopt/overwrite an unmanaged file already on disk'
28
+ if (operation === 'uninstall') return 'Remove a modified managed artifact'
29
+ return "Replace a modified managed artifact; never adopts unmanaged bytes — use 'install --force' for that"
30
+ }
31
+
14
32
  function human(result) {
15
33
  const lines = [`Available ${result.kind} (catalog ${result.catalog_version}):`]
16
34
  for (const item of result.items) lines.push(` ${item.id.padEnd(14)} ${item.name} — ${item.description}`)
@@ -109,6 +127,10 @@ function createLifecycleCommand(kind) {
109
127
  // internal/commands/integrations_flags.go registering `third-party` in
110
128
  // newIntegrationsLifecycleCmd.
111
129
  root.addCommand(createThirdPartyCommand(kind))
130
+ // "models" is agents-only: skills have no model field, and surfacing this
131
+ // under `trackfw skills models` would mislead users. Mirrors the identity
132
+ // flag gate and the Go/Python implementations.
133
+ if (kind === 'agents') root.addCommand(createAgentModelsCommand())
112
134
  for (const operation of ['list', 'install', 'uninstall', 'update']) {
113
135
  const mutation = operation !== 'list'
114
136
  const command = new Command(operation)
@@ -117,7 +139,7 @@ function createLifecycleCommand(kind) {
117
139
  .option('--scope <scope>', 'Installation scope: project or global (default: global; asks interactively)')
118
140
  .option('--surface <target=surface>', 'Surface selection (repeatable)', collect, [])
119
141
  .option('--json', 'Print deterministic JSON')
120
- .option('--force', 'Replace or remove modified artifacts')
142
+ .option('--force', forceHelp(operation))
121
143
 
122
144
  // Flags de identidade são exclusivas de agents (ADR D5): skills não têm
123
145
  // identidade, e createLifecycleCommand é compartilhado entre `agents` e
@@ -199,6 +221,7 @@ function createLifecycleCommand(kind) {
199
221
  }
200
222
  }
201
223
 
224
+ options.agentModels = configModule.load().agentModels || {}
202
225
  const output = execute(kind, operation, options)
203
226
  console.log(options.json ? JSON.stringify(output) : human(output))
204
227
  })
@@ -207,4 +230,58 @@ function createLifecycleCommand(kind) {
207
230
  return root
208
231
  }
209
232
 
210
- module.exports = { createLifecycleCommand, csv, human, promptSelection, promptAmbiguousSurfaces, resolveScope }
233
+ // createAgentModelsCommand returns the "models" subcommand of "trackfw agents".
234
+ // Lists, for each catalog agent × target pair, the model identifier that
235
+ // install/update would write to the generated artifact. Mirrors
236
+ // internal/commands/agents_models.go:newAgentModelsCmd and
237
+ // pypi/trackfw/integrations/command.py:_run_models.
238
+ //
239
+ // Column widths must match Go and Python for byte-identical output.
240
+ const MODELS_AGENT_WIDTH = 14
241
+ const MODELS_TIER_WIDTH = 8
242
+ const MODELS_TARGET_WIDTH = 12
243
+ const MODELS_NA = '—'
244
+
245
+ function pad(s, width) {
246
+ return String(s).padEnd(width)
247
+ }
248
+
249
+ function createAgentModelsCommand() {
250
+ const cmd = new Command('models')
251
+ .description('Show the resolved model each agent uses per target')
252
+ cmd.action(() => {
253
+ const agentModels = configModule.load().agentModels || {}
254
+
255
+ // Warnings: emit once per suspect tier, sorted, to stderr.
256
+ const suspectTiers = Object.keys(agentModels)
257
+ .filter(tier => looksLikeSuspectModelValue(agentModels[tier]))
258
+ .sort()
259
+ for (const tier of suspectTiers) {
260
+ process.stderr.write(
261
+ `WARN: agent_models.${tier} = ${JSON.stringify(agentModels[tier])} — not a version string and not a claude- model ID; will be written literally and may produce an invalid model identifier\n`
262
+ )
263
+ }
264
+
265
+ const lines = []
266
+ lines.push(`${pad('AGENT', MODELS_AGENT_WIDTH)} ${pad('TIER', MODELS_TIER_WIDTH)} ${pad('TARGET', MODELS_TARGET_WIDTH)} RESOLVED`)
267
+
268
+ for (const agent of catalog.agents) {
269
+ const source = readAsset(agent)
270
+ const parts = markdownParts(source)
271
+ const tier = parts.model || 'sonnet'
272
+
273
+ for (const target of catalog.targets) {
274
+ const surface = surfaceFor(target, null, 'agents')
275
+ const representation = surface.capabilities.agents.representation
276
+ const { resolved, present } = resolveAgentModel(tier, representation, target.id, agentModels)
277
+ const display = present ? resolved : MODELS_NA
278
+ lines.push(`${pad(agent.id, MODELS_AGENT_WIDTH)} ${pad(tier, MODELS_TIER_WIDTH)} ${pad(target.id, MODELS_TARGET_WIDTH)} ${display}`)
279
+ }
280
+ }
281
+
282
+ console.log(lines.join('\n'))
283
+ })
284
+ return cmd
285
+ }
286
+
287
+ module.exports = { createLifecycleCommand, createAgentModelsCommand, csv, human, promptSelection, promptAmbiguousSurfaces, resolveScope }
@@ -0,0 +1,51 @@
1
+ 'use strict'
2
+
3
+ const { Command } = require('commander')
4
+ const { runPush } = require('../push/runner')
5
+ const config = require('../config')
6
+
7
+ function createPushCommand() {
8
+ const cmd = new Command('push')
9
+ cmd
10
+ .description(
11
+ 'trackfw push pushes already-created commits without committing and without opening a PR/MR.\n\n' +
12
+ ' 1. Validates branch name — must match feat|fix|refactor|chore|docs/<slug>\n' +
13
+ ' 2. Validates governance — REQ + roadmap in wip/ must exist for feat/fix/refactor branches\n' +
14
+ ' (hard gate: not affected by lenient mode or per-rule severity); chore/docs branches skip\n' +
15
+ ' this check, mirroring \'trackfw commit\'\n' +
16
+ ' 3. Detects pending squash-merges in other branches (advisory only)\n' +
17
+ ' 4. Pushes to origin (adds -u if no upstream is configured yet)\n\n' +
18
+ 'push never commits and never opens a PR/MR.\n' +
19
+ 'Does not accept -m. If you have not committed yet, run \'trackfw commit -m "..."\' first.\n\n' +
20
+ 'Compositional vocabulary:\n' +
21
+ ' trackfw commit -m "..." commits\n' +
22
+ ' trackfw push pushes\n' +
23
+ ' trackfw ship -m "..." commit + push + PR (composition)\n\n' +
24
+ "--force-with-lease pushes with 'git push --force-with-lease' instead of a plain push — for\n" +
25
+ 'the post-rebase case, where a plain push is rejected. It only runs when the branch already\n' +
26
+ "has an open PR/MR (verified via the resolved forge CLI): the safe path is always to open the\n" +
27
+ 'PR first.'
28
+ )
29
+ .option('--dry-run', 'Print what would be done without executing write commands', false)
30
+ .option('--force-with-lease', 'Governed force-push (git push --force-with-lease); requires an open PR/MR on this branch', false)
31
+ .action((options) => {
32
+ const cfg = config.load()
33
+ const exitCode = runPush(
34
+ {
35
+ dryRun: options.dryRun || false,
36
+ forceWithLease: options.forceWithLease || false,
37
+ },
38
+ {
39
+ configForge: cfg.forge || '',
40
+ repoDir: '.',
41
+ }
42
+ )
43
+ if (exitCode !== 0) {
44
+ process.exit(exitCode)
45
+ }
46
+ })
47
+
48
+ return cmd
49
+ }
50
+
51
+ module.exports = createPushCommand()
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const { Command } = require('commander')
4
+ const { runReleaseTag } = require('../release/runner')
5
+ const config = require('../config')
6
+
7
+ function createReleaseCommand() {
8
+ const cmd = new Command('release')
9
+ cmd.description('Governed release operations')
10
+
11
+ const tagCmd = new Command('tag')
12
+ tagCmd
13
+ .description(
14
+ 'Create and publish an annotated release tag.\n\n' +
15
+ "It exists because 'trackfw ship' only pushes branches — tag is not a branch operation,\n" +
16
+ "and ship's governance gate (\"REQ + roadmap in wip/\") does not apply to release. See\n" +
17
+ 'ADR-2026-08-19-caminho-governado-para-push-forcado-e-tag-de-release.md.\n\n' +
18
+ 'Every precondition below refuses with a message naming what to fix — this command\n' +
19
+ 'never guesses:\n' +
20
+ ' 1. Working tree must be clean.\n' +
21
+ ' 2. The default branch (main/master), if checked out locally, must be up to date with origin.\n' +
22
+ ' 3. The 4 version files must all match <version> exactly.\n' +
23
+ ' 4. CHANGELOG.md must have a "## [<version>] - YYYY-MM-DD" section.\n' +
24
+ ' 5. The tag must not already exist, locally or on origin.\n' +
25
+ ' 6. The GitHub CLI (gh) must be available and authenticated — release tag currently\n' +
26
+ ' only supports GitHub; other forges are refused with instructions to push the tag\n' +
27
+ ' manually.\n\n' +
28
+ 'On success, it publishes the tag via two GitHub API calls (POST git/tags then POST\n' +
29
+ 'git/refs), preserving the annotation.'
30
+ )
31
+ .argument('<version>', 'Version to tag (with or without a leading "v")')
32
+ .action((version) => {
33
+ const cfg = config.load()
34
+ const exitCode = runReleaseTag(version, {
35
+ configForge: cfg.forge || '',
36
+ repoDir: '.',
37
+ })
38
+ if (exitCode !== 0) {
39
+ process.exit(exitCode)
40
+ }
41
+ })
42
+
43
+ cmd.addCommand(tagCmd)
44
+ return cmd
45
+ }
46
+
47
+ module.exports = createReleaseCommand()
@@ -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
- server.listen(port, '127.0.0.1', () => {
152
- console.log(`trackfw serve: http://localhost:${port}`)
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 }
@@ -15,16 +15,23 @@ function createShipCommand() {
15
15
  " this check, mirroring 'trackfw commit'\n" +
16
16
  ' 3. Detects pending squash-merges in other branches (advisory only)\n' +
17
17
  ' 4. Reviews what is staged (git status --short + git diff --cached --stat)\n' +
18
- ' 5. Commits with Conventional Commits format (-m is required)\n' +
18
+ ' 5. Commits with Conventional Commits format (-m is required, unless nothing is staged\n' +
19
+ ' and --force-with-lease is set — see below)\n' +
19
20
  ' 6. Pushes to origin (adds -u if no upstream is configured yet)\n' +
20
21
  ' 7. Opens PR/MR via the resolved forge CLI (or prints URL if CLI is absent)\n\n' +
21
22
  "Stage your files explicitly before running ship.\n" +
22
- "This command never executes 'git add .' or 'git add -A'."
23
+ "This command never executes 'git add .' or 'git add -A'.\n\n" +
24
+ "--force-with-lease pushes with 'git push --force-with-lease' instead of a plain push —\n" +
25
+ "for the post-rebase case, where a plain push is rejected. It only runs when the branch\n" +
26
+ "already has an open PR/MR (verified via the resolved forge CLI): the safe path is\n" +
27
+ "always to open the PR first. When nothing is staged, it pushes existing commits\n" +
28
+ "without requiring -m."
23
29
  )
24
30
  .option('-m, --message <msg>', 'Commit message (Conventional Commits format required)')
25
31
  .option('--dry-run', 'Print what would be done without executing write commands', false)
26
32
  .option('--no-pr', 'Skip PR/MR creation after push')
27
33
  .option('--forge <forge>', 'Override forge detection (github, gitlab, bitbucket, azure)', '')
34
+ .option('--force-with-lease', 'Governed force-push (git push --force-with-lease); requires an open PR/MR on this branch', false)
28
35
  .action((options) => {
29
36
  const cfg = config.load()
30
37
  const exitCode = runShip(
@@ -33,6 +40,7 @@ function createShipCommand() {
33
40
  dryRun: options.dryRun || false,
34
41
  noPR: options.pr === false,
35
42
  forge: options.forge || '',
43
+ forceWithLease: options.forceWithLease || false,
36
44
  },
37
45
  {
38
46
  configForge: cfg.forge || '',