trackfw 2.15.1 → 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 +183 -0
- package/package.json +1 -1
- package/src/commands/identity-wizard.js +258 -0
- package/src/commands/init.js +65 -4
- package/src/commands/integrations.js +133 -6
- package/src/commands/update.js +8 -1
- package/src/generators/init.js +28 -5
- package/src/i18n/locales/en-US.json +12 -1
- package/src/i18n/locales/es-ES.json +12 -1
- package/src/i18n/locales/pt-BR.json +12 -1
- package/src/identity/config.js +134 -0
- package/src/identity/index.js +19 -0
- package/src/identity/preset.js +197 -0
- package/src/identity/slug.js +62 -0
- package/src/integrations/index.js +12 -2
- package/src/integrations/manager.js +50 -0
- package/src/integrations/render.js +149 -14
package/README.md
CHANGED
|
@@ -363,10 +363,192 @@ 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
|
---
|
|
369
378
|
|
|
379
|
+
## Agent identity — give your agents a name
|
|
380
|
+
|
|
381
|
+
By default the agents are functional and impersonal: `trackfw-architect`,
|
|
382
|
+
`trackfw-backend`, … Agent identity lets you name all ten, pick how they
|
|
383
|
+
address you, and call them by name.
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
# Non-interactive: pick a themed preset
|
|
387
|
+
trackfw init --identity-preset greek
|
|
388
|
+
|
|
389
|
+
# Or answer the wizard, which also offers "name them one by one"
|
|
390
|
+
trackfw init
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`--identity-preset` accepts ten themed presets plus two opt-outs:
|
|
394
|
+
|
|
395
|
+
`greek` · `norse` · `potter` · `thrones` · `chaves` · `pioneers` · `starwars` · `tolkien` · `turma` · `egyptian` · `neutral` · `none`
|
|
396
|
+
|
|
397
|
+
`neutral` and `none` write nothing and keep the current behavior.
|
|
398
|
+
|
|
399
|
+
Sample mapping (three of the ten presets):
|
|
400
|
+
|
|
401
|
+
| Agent | `greek` | `pioneers` | `tolkien` |
|
|
402
|
+
|---|---|---|---|
|
|
403
|
+
| architect | Zeus | Turing | Gandalf |
|
|
404
|
+
| backend | Apolo | Ritchie | Aragorn |
|
|
405
|
+
| frontend | Afrodite | Berners-Lee | Arwen |
|
|
406
|
+
| qa | Ártemis | Hamilton | Legolas |
|
|
407
|
+
| infra | Ares | Torvalds | Gimli |
|
|
408
|
+
| security | Hades | Diffie | Boromir |
|
|
409
|
+
| dba | Poseidon | Codd | Elrond |
|
|
410
|
+
| ux | Atena | Norman | Galadriel |
|
|
411
|
+
| code-quality | Hefesto | Knuth | Faramir |
|
|
412
|
+
| data | Métis | Hopper | Bilbo |
|
|
413
|
+
|
|
414
|
+
The remaining presets are `norse` (Odin, Thor, Freya…), `potter` (Dumbledore,
|
|
415
|
+
Snape, Luna…), `thrones` (Tyrion, Jon, Arya…), `chaves` (Girafales, Madruga,
|
|
416
|
+
Chiquinha…), `starwars` (Yoda, Han, Leia…), `turma` (Franjinha, Cebolinha,
|
|
417
|
+
Magali…), and `egyptian` (Thoth, Rá, Ísis…).
|
|
418
|
+
|
|
419
|
+
### Custom mode and your nickname
|
|
420
|
+
|
|
421
|
+
The interactive wizard also offers **name them one by one**: you type all ten
|
|
422
|
+
display names yourself. Each entry is validated as you go — an invalid name is
|
|
423
|
+
rejected with an inline error, never silently corrected, and two names that
|
|
424
|
+
resolve to the same identifier are rejected too.
|
|
425
|
+
|
|
426
|
+
The wizard then asks for an optional **nickname for you**, which is how the
|
|
427
|
+
agents will address you.
|
|
428
|
+
|
|
429
|
+
### `agents install` also runs the wizard
|
|
430
|
+
|
|
431
|
+
`trackfw init` is not the only entry point: `trackfw agents install`, the
|
|
432
|
+
natural path in a project that is already governed, offers the same wizard.
|
|
433
|
+
The rule is identical across the three CLIs — it appears **only** when
|
|
434
|
+
**all** of the following hold:
|
|
435
|
+
|
|
436
|
+
- the command is `agents` (never `skills`: skills have no identity);
|
|
437
|
+
- stdin is a TTY (a non-interactive run never blocks on a prompt);
|
|
438
|
+
- and either no `~/.trackfw/identity.json` exists yet, or `--identity` was
|
|
439
|
+
passed to force reconfiguration.
|
|
440
|
+
|
|
441
|
+
With an identity already configured and no `--identity`, the command asks
|
|
442
|
+
nothing — it prints `identity: N custom agent(s)` and installs directly.
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
# First run on this machine: offers the wizard, then installs
|
|
446
|
+
trackfw agents install --targets claude
|
|
447
|
+
|
|
448
|
+
# Identity already configured: no prompt, installs directly
|
|
449
|
+
trackfw agents install --targets claude
|
|
450
|
+
|
|
451
|
+
# Force reconfiguration
|
|
452
|
+
trackfw agents install --targets claude --identity
|
|
453
|
+
|
|
454
|
+
# Non-interactive, same semantics as init --identity-preset
|
|
455
|
+
trackfw agents install --targets claude --identity-preset chaves
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Two new flags exist only on `agents install` (`skills install` never
|
|
459
|
+
registers them): `--identity` (bool, forces reconfiguration even if a file
|
|
460
|
+
already exists) and `--identity-preset <preset>` (same ten themed presets
|
|
461
|
+
plus `neutral` and `none`; an invalid value errors out listing the valid
|
|
462
|
+
ones).
|
|
463
|
+
|
|
464
|
+
In **name them one by one** mode, each field is now labeled by the agent's
|
|
465
|
+
specialty, taken from the catalog, never by its technical id:
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
Architect — Architecture, ADRs and governed coordination
|
|
469
|
+
> _
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Before anything is written to disk — for a themed preset **or** for custom
|
|
473
|
+
names — a confirmation screen lists all ten `specialty → name` pairs plus
|
|
474
|
+
your nickname:
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
── Confirmation ──────────────────────────────
|
|
478
|
+
Architecture, ADRs and governed coordination → Girafales
|
|
479
|
+
Backend APIs, domain logic and integrations → Madruga
|
|
480
|
+
...
|
|
481
|
+
What we'll call you: chefe
|
|
482
|
+
|
|
483
|
+
? Confirm?
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Answering no returns to preset selection; nothing is written until you
|
|
487
|
+
confirm.
|
|
488
|
+
|
|
489
|
+
Everything is stored in a single global file, shared by the Go, npm, and PyPI
|
|
490
|
+
distributions:
|
|
491
|
+
|
|
492
|
+
```json
|
|
493
|
+
// ~/.trackfw/identity.json
|
|
494
|
+
{
|
|
495
|
+
"schema_version": 1,
|
|
496
|
+
"user_nickname": "Kleber",
|
|
497
|
+
"agents": {
|
|
498
|
+
"architect": { "display_name": "Zeus", "slug": "zeus" }
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The generated artifact — still installed at the unchanged path
|
|
504
|
+
`~/.claude/agents/trackfw-architect.md`:
|
|
505
|
+
|
|
506
|
+
```markdown
|
|
507
|
+
---
|
|
508
|
+
name: zeus-tf
|
|
509
|
+
description: Zeus — Principal software architect for system design, ADRs and governed multi-agent coordination.
|
|
510
|
+
model: opus
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
Você é Zeus. Trate o usuário como Kleber.
|
|
514
|
+
|
|
515
|
+
# Architect
|
|
516
|
+
...
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Why the `-tf` suffix
|
|
520
|
+
|
|
521
|
+
The `name` always ends in `-tf`. Two agents sharing the same `name` in the same
|
|
522
|
+
directory make Claude Code load *"only one of them, chosen by filesystem read
|
|
523
|
+
order rather than a documented precedence"* — a silent, non-deterministic
|
|
524
|
+
shadowing that the user cannot detect. If you already keep a personal `zeus.md`
|
|
525
|
+
agent, `zeus-tf` guarantees both survive.
|
|
526
|
+
|
|
527
|
+
The suffix belongs to the technical identifier only. It never appears in how
|
|
528
|
+
the agent presents itself: the `description` and the body both say **Zeus**.
|
|
529
|
+
|
|
530
|
+
### How to invoke it
|
|
531
|
+
|
|
532
|
+
| You type | What happens |
|
|
533
|
+
|---|---|
|
|
534
|
+
| `@agent-zeus-tf` | Works — explicit mention resolves against `name` |
|
|
535
|
+
| "chame o Zeus" / "ask Zeus to…" | Works — natural-language routing reads `description` |
|
|
536
|
+
| "quem é você?" | Answers "Sou Zeus" — the body is loaded after selection |
|
|
537
|
+
|
|
538
|
+
### Cost and non-regression
|
|
539
|
+
|
|
540
|
+
The agent **never reads the configuration at runtime**. Identity is
|
|
541
|
+
materialized into the artifact at install time, so the per-interaction cost is
|
|
542
|
+
essentially zero: the `description` is substituted rather than extended, and
|
|
543
|
+
the body grows by tens of tokens that are loaded only after the agent has
|
|
544
|
+
already been selected. No tool call, no file read, no permanent instruction.
|
|
545
|
+
|
|
546
|
+
Without `~/.trackfw/identity.json`, the generated artifacts are **byte for byte
|
|
547
|
+
identical** to the current ones in all three CLIs. The feature is opt-in and
|
|
548
|
+
regresses nothing.
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
370
552
|
## `trackfw init` — stack-aware scaffolding
|
|
371
553
|
|
|
372
554
|
```
|
|
@@ -377,6 +559,7 @@ The 10 roles installed for each tool: **architect · backend · frontend · qa
|
|
|
377
559
|
? Git hooks? husky / lefthook / none
|
|
378
560
|
? CI system? GitHub Actions / GitLab CI / none
|
|
379
561
|
? Which AI assistants? Claude / Codex / Gemini / Antigravity / Cursor / Copilot / Windsurf / Amazon Q / Kiro
|
|
562
|
+
? Agent identity? Greek / Norse / Potter / Thrones / Chaves / Pioneers / Star Wars / Tolkien / Turma / Egyptian / Name them one by one / Neutral
|
|
380
563
|
```
|
|
381
564
|
|
|
382
565
|
The governance structure (`docs/adr/`, `docs/req/`, `docs/roadmaps/`) is always identical — stack-agnostic. The generated hooks, workflows, and AI integrations adapt to your answers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trackfw",
|
|
3
|
-
"version": "
|
|
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",
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
|
|
6
|
+
const identityStore = require('../identity')
|
|
7
|
+
const { t } = require('../i18n')
|
|
8
|
+
|
|
9
|
+
// identityFileExists reporta se ~/.trackfw/identity.json já existe, sem
|
|
10
|
+
// depender de identityStore.load (que retorna config vazia para "ausente").
|
|
11
|
+
// Espelha internal/commands/init.go:identityFileExists.
|
|
12
|
+
function identityFileExists(home) {
|
|
13
|
+
return fs.existsSync(path.join(home, '.trackfw', 'identity.json'))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// resolveIdentityPreset traduz o valor da flag --identity-preset para uma
|
|
17
|
+
// Config a persistir. "none" e "neutral" significam "não gravar nada" — o
|
|
18
|
+
// chamador não deve criar ~/.trackfw/identity.json para esses valores. Um
|
|
19
|
+
// valor desconhecido é sempre um erro, listando os valores aceitos. Espelha
|
|
20
|
+
// internal/commands/init.go:resolveIdentityPreset.
|
|
21
|
+
function resolveIdentityPreset(value) {
|
|
22
|
+
if (value === 'none' || value === 'neutral') return { cfg: null, shouldSave: false }
|
|
23
|
+
let cfg
|
|
24
|
+
try {
|
|
25
|
+
cfg = identityStore.preset(value)
|
|
26
|
+
} catch {
|
|
27
|
+
const valid = ['none', 'neutral', ...identityStore.presetNames()]
|
|
28
|
+
throw new Error(`identity-preset invalido "${value}" (validos: ${valid.join(', ')})`)
|
|
29
|
+
}
|
|
30
|
+
return { cfg, shouldSave: true }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// applyIdentityPresetFlag resolve e persiste --identity-preset para
|
|
34
|
+
// `agents install|update|uninstall`, reusando resolveIdentityPreset para que
|
|
35
|
+
// ambos os comandos aceitem exatamente os mesmos nomes de preset e rejeitem
|
|
36
|
+
// valores inválidos com o mesmo formato de erro. Espelha
|
|
37
|
+
// internal/commands/integrations_flags.go:applyIdentityPresetFlag.
|
|
38
|
+
function applyIdentityPresetFlag(home, presetValue, operation) {
|
|
39
|
+
const { cfg, shouldSave } = resolveIdentityPreset(presetValue)
|
|
40
|
+
if (!shouldSave) return
|
|
41
|
+
try {
|
|
42
|
+
identityStore.validate(cfg, identityStore.knownAgentIds())
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(`${operation}: identidade invalida: ${err.message}`)
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
identityStore.save(home, cfg)
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new Error(`${operation}: falha ao gravar identidade: ${err.message}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// shouldPromptIdentity decide se `trackfw agents install` (e as operações
|
|
54
|
+
// update/uninstall, que compartilham o mesmo fluxo) deve mostrar o wizard
|
|
55
|
+
// interativo de identidade. Todas as condições devem valer simultaneamente
|
|
56
|
+
// (ADR D2):
|
|
57
|
+
//
|
|
58
|
+
// 1. kind é "agents" — skills não têm identidade e nunca devem perguntar.
|
|
59
|
+
// 2. stdin é um TTY — nunca bloquear uma execução não-interativa.
|
|
60
|
+
// 3. ou não há identidade configurada ainda, ou o usuário pediu
|
|
61
|
+
// explicitamente para reconfigurá-la via --identity.
|
|
62
|
+
//
|
|
63
|
+
// Com uma identidade já configurada e sem a flag --identity, o wizard NÃO
|
|
64
|
+
// deve reaparecer — um wizard que aparece a cada install vira incentivo a
|
|
65
|
+
// automatizar o "pular", o que esvazia a feature. Espelha
|
|
66
|
+
// internal/commands/identity_wizard.go:shouldPromptIdentity.
|
|
67
|
+
function shouldPromptIdentity(kind, isTTY, identityExists, forceFlag) {
|
|
68
|
+
if (kind !== 'agents') return false
|
|
69
|
+
if (!isTTY) return false
|
|
70
|
+
return !identityExists || Boolean(forceFlag)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// identityPresetOptions é a lista fixa de presets temáticos de identidade
|
|
74
|
+
// oferecidos pelo wizard, compartilhada literalmente entre init e agents
|
|
75
|
+
// install para que nenhum consumidor divirja do outro. Espelha
|
|
76
|
+
// internal/commands/identity_wizard.go:identityPresetOptions.
|
|
77
|
+
function identityPresetOptions() {
|
|
78
|
+
return [
|
|
79
|
+
{ name: 'Panteão grego (Zeus, Apolo, Afrodite...)', value: 'greek' },
|
|
80
|
+
{ name: 'Mitologia nórdica (Odin, Thor, Freya...)', value: 'norse' },
|
|
81
|
+
{ name: 'Pioneiros da computação (Turing, Codd, Knuth...)', value: 'pioneers' },
|
|
82
|
+
{ name: 'Harry Potter (Dumbledore, Snape, Luna...)', value: 'potter' },
|
|
83
|
+
{ name: 'Game of Thrones (Tyrion, Jon, Arya...)', value: 'thrones' },
|
|
84
|
+
{ name: 'Senhor dos Anéis (Gandalf, Aragorn, Arwen...)', value: 'tolkien' },
|
|
85
|
+
{ name: 'Star Wars (Yoda, Leia, Vader...)', value: 'starwars' },
|
|
86
|
+
{ name: 'Chaves (Girafales, Madruga, Chiquinha...)', value: 'chaves' },
|
|
87
|
+
{ name: 'Turma da Mônica (Franjinha, Cebolinha, Mônica...)', value: 'turma' },
|
|
88
|
+
{ name: 'Panteão egípcio (Thoth, Ísis, Anúbis...)', value: 'egyptian' },
|
|
89
|
+
{ name: 'Personalizar um a um', value: 'custom' },
|
|
90
|
+
{ name: 'Nomes neutros (padrão)', value: 'neutral' },
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// catalogAgentItem busca o item "agents" do catálogo pelo id. Todo id vindo
|
|
95
|
+
// de identity.knownAgentIds() deve existir no catálogo embedado — se não
|
|
96
|
+
// existir, é erro de programação: falha alto em vez de rotular
|
|
97
|
+
// silenciosamente errado. Espelha o panic em
|
|
98
|
+
// internal/commands/identity_wizard.go:buildCustomIdentityGroup.
|
|
99
|
+
function catalogAgentItem(catalog, id) {
|
|
100
|
+
const found = (catalog.agents || []).find(entry => entry.id === id)
|
|
101
|
+
if (!found) {
|
|
102
|
+
throw new Error(`identity wizard: agent id "${id}" from identity.knownAgentIds() has no entry in the agents catalog`)
|
|
103
|
+
}
|
|
104
|
+
return found
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// buildIdentityConfig constrói a Config em memória para o caminho escolhido
|
|
108
|
+
// no wizard, sem tocar em disco. Espelha
|
|
109
|
+
// internal/commands/identity_wizard.go:buildIdentityConfig.
|
|
110
|
+
function buildIdentityConfig(identitySelect, knownAgentIds, customDisplayNames, userNickname) {
|
|
111
|
+
let cfg
|
|
112
|
+
if (identitySelect === 'custom') {
|
|
113
|
+
const agents = {}
|
|
114
|
+
knownAgentIds.forEach((id, index) => {
|
|
115
|
+
let slug
|
|
116
|
+
try {
|
|
117
|
+
slug = identityStore.slugify(customDisplayNames[index])
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw new Error(`identidade customizada invalida para "${id}": ${err.message}`)
|
|
120
|
+
}
|
|
121
|
+
agents[id] = { display_name: customDisplayNames[index], slug }
|
|
122
|
+
})
|
|
123
|
+
cfg = { agents }
|
|
124
|
+
} else {
|
|
125
|
+
cfg = identityStore.preset(identitySelect)
|
|
126
|
+
}
|
|
127
|
+
cfg.user_nickname = userNickname
|
|
128
|
+
return cfg
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// promptCustomNames pede um nome de exibição para cada agente conhecido,
|
|
132
|
+
// rotulado com a especialidade do catálogo (Item.name + Item.description) —
|
|
133
|
+
// nunca com o id técnico cru (ADR D4). Espelha
|
|
134
|
+
// internal/commands/identity_wizard.go:buildCustomIdentityGroup.
|
|
135
|
+
async function promptCustomNames(catalog, knownAgentIds, prompts) {
|
|
136
|
+
const { input } = prompts
|
|
137
|
+
const helperText = t('init.prompt.identityCustomName')
|
|
138
|
+
const customDisplayNames = []
|
|
139
|
+
for (const id of knownAgentIds) {
|
|
140
|
+
const item = catalogAgentItem(catalog, id)
|
|
141
|
+
const label = `${item.name} — ${item.description}`
|
|
142
|
+
// eslint-disable-next-line no-await-in-loop
|
|
143
|
+
const value = await input({
|
|
144
|
+
message: `${label}\n ${helperText}`,
|
|
145
|
+
validate: candidate => {
|
|
146
|
+
let slug
|
|
147
|
+
try {
|
|
148
|
+
slug = identityStore.slugify(candidate)
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return err.message
|
|
151
|
+
}
|
|
152
|
+
for (let index = 0; index < customDisplayNames.length; index += 1) {
|
|
153
|
+
let otherSlug
|
|
154
|
+
try {
|
|
155
|
+
otherSlug = identityStore.slugify(customDisplayNames[index])
|
|
156
|
+
} catch {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
if (otherSlug === slug) return `slug "${slug}" duplicado com o agente "${knownAgentIds[index]}"`
|
|
160
|
+
}
|
|
161
|
+
return true
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
customDisplayNames.push(value)
|
|
165
|
+
}
|
|
166
|
+
return customDisplayNames
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// confirmIdentitySelection exibe o mapeamento especialidade → nome de
|
|
170
|
+
// exibição (ADR D3) e pede confirmação. O mapeamento é impresso com
|
|
171
|
+
// console.log simples (não por um campo de prompt) para que o alinhamento
|
|
172
|
+
// de colunas siga exatamente o exemplo do ADR. Espelha
|
|
173
|
+
// internal/commands/identity_wizard.go:confirmIdentitySelection.
|
|
174
|
+
async function confirmIdentitySelection(catalog, knownAgentIds, cfg, prompts) {
|
|
175
|
+
const { confirm } = prompts
|
|
176
|
+
const nicknameLabel = t('identity.wizard.nicknameRowLabel')
|
|
177
|
+
let labelWidth = nicknameLabel.length
|
|
178
|
+
const labels = knownAgentIds.map(id => {
|
|
179
|
+
const item = catalogAgentItem(catalog, id)
|
|
180
|
+
if (item.description.length > labelWidth) labelWidth = item.description.length
|
|
181
|
+
return item.description
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
console.log()
|
|
185
|
+
console.log(t('identity.wizard.confirmHeader'))
|
|
186
|
+
knownAgentIds.forEach((id, index) => {
|
|
187
|
+
const agent = cfg.agents[id]
|
|
188
|
+
console.log(` ${labels[index].padEnd(labelWidth)} → ${agent.display_name}`)
|
|
189
|
+
})
|
|
190
|
+
if (cfg.user_nickname) {
|
|
191
|
+
console.log(` ${nicknameLabel.padEnd(labelWidth)} ${cfg.user_nickname}`)
|
|
192
|
+
}
|
|
193
|
+
console.log()
|
|
194
|
+
|
|
195
|
+
return confirm({ message: t('identity.wizard.confirmQuestion') })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// runIdentityWizard é o único wizard interativo de identidade, consumido
|
|
199
|
+
// tanto por `trackfw init` quanto por `trackfw agents install`
|
|
200
|
+
// (ADR-2026-07-25-wizard-unificado-de-identidade-no-agents-install, D1).
|
|
201
|
+
//
|
|
202
|
+
// Mostra, em ordem: seleção de preset/custom, nomes livres em texto (só no
|
|
203
|
+
// modo custom), o apelido do usuário e uma tela de confirmação (D3, D6). Se
|
|
204
|
+
// o usuário recusar a confirmação, o wizard volta à seleção de preset sem
|
|
205
|
+
// persistir nada — nunca retorna tendo gravado uma config parcial ou
|
|
206
|
+
// recusada em disco.
|
|
207
|
+
//
|
|
208
|
+
// Retorna [cfg, persisted]. "neutral" (ou deixar o select vazio) é o
|
|
209
|
+
// caminho "não gravar nada" e retorna [{}, false].
|
|
210
|
+
async function runIdentityWizard(catalog, home, prompts = require('@inquirer/prompts')) {
|
|
211
|
+
const { select, input } = prompts
|
|
212
|
+
const knownAgentIds = identityStore.knownAgentIds()
|
|
213
|
+
|
|
214
|
+
const titleIdentityPreset = t('init.prompt.identityPreset')
|
|
215
|
+
const titleIdentityNickname = t('init.prompt.identityNickname')
|
|
216
|
+
|
|
217
|
+
for (;;) {
|
|
218
|
+
const identitySelect = await select({
|
|
219
|
+
message: titleIdentityPreset,
|
|
220
|
+
choices: identityPresetOptions(),
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
if (identitySelect === '' || identitySelect === 'neutral') {
|
|
224
|
+
return [{}, false]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let customDisplayNames = []
|
|
228
|
+
if (identitySelect === 'custom') {
|
|
229
|
+
customDisplayNames = await promptCustomNames(catalog, knownAgentIds, prompts)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const userNickname = await input({ message: titleIdentityNickname, default: '' })
|
|
233
|
+
|
|
234
|
+
const cfg = buildIdentityConfig(identitySelect, knownAgentIds, customDisplayNames, userNickname)
|
|
235
|
+
identityStore.validate(cfg, knownAgentIds)
|
|
236
|
+
|
|
237
|
+
// eslint-disable-next-line no-await-in-loop
|
|
238
|
+
const confirmed = await confirmIdentitySelection(catalog, knownAgentIds, cfg, prompts)
|
|
239
|
+
if (!confirmed) {
|
|
240
|
+
// D3: recusar volta à seleção de preset sem persistir absolutamente nada.
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
identityStore.save(home, cfg)
|
|
245
|
+
return [cfg, true]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
identityFileExists,
|
|
251
|
+
resolveIdentityPreset,
|
|
252
|
+
applyIdentityPresetFlag,
|
|
253
|
+
shouldPromptIdentity,
|
|
254
|
+
identityPresetOptions,
|
|
255
|
+
buildIdentityConfig,
|
|
256
|
+
confirmIdentitySelection,
|
|
257
|
+
runIdentityWizard,
|
|
258
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const { Command } = require('commander')
|
|
3
3
|
const { t } = require('../i18n')
|
|
4
|
+
const identityStore = require('../identity')
|
|
5
|
+
const identityWizard = require('./identity-wizard')
|
|
6
|
+
const { resolveIdentityPreset, identityFileExists } = identityWizard
|
|
7
|
+
const { resolveScope } = require('./integrations')
|
|
4
8
|
|
|
5
9
|
const cmd = new Command('init')
|
|
6
10
|
cmd.description(t('init.description'))
|
|
7
11
|
cmd.option('--ai-tools <tools>', 'Comma-separated AI tools to configure (claude,codex,gemini,antigravity,cursor,copilot,windsurf,amazonq,kiro)', '')
|
|
8
|
-
cmd.
|
|
12
|
+
cmd.option('--identity-preset <preset>', `Agent identity preset: none, neutral, ${identityStore.presetNames().join(', ')}`, 'none')
|
|
13
|
+
cmd.action(async (options, command) => {
|
|
14
|
+
const os = require('os')
|
|
9
15
|
const path = require('path')
|
|
10
16
|
const generators = require('../generators/init')
|
|
11
17
|
|
|
18
|
+
const home = os.homedir()
|
|
19
|
+
|
|
20
|
+
// A validação e persistência da flag acontecem incondicionalmente, acima
|
|
21
|
+
// do early-return não-TTY abaixo — é isso que faz um --identity-preset
|
|
22
|
+
// inválido falhar alto em CI em vez de silenciosamente não fazer nada.
|
|
23
|
+
const presetChanged = command.getOptionValueSource('identityPreset') === 'cli'
|
|
24
|
+
if (presetChanged) {
|
|
25
|
+
const { cfg, shouldSave } = resolveIdentityPreset(options.identityPreset)
|
|
26
|
+
if (shouldSave) {
|
|
27
|
+
identityStore.validate(cfg, identityStore.knownAgentIds())
|
|
28
|
+
identityStore.save(home, cfg)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Pula o wizard de identidade inteiramente quando a flag foi passada
|
|
33
|
+
// explicitamente (já tratado acima) ou quando um arquivo de identidade já
|
|
34
|
+
// existe — re-executar init nunca deve sobrescrever silenciosamente uma
|
|
35
|
+
// identidade já configurada.
|
|
36
|
+
const skipIdentityWizard = presetChanged || identityFileExists(home)
|
|
37
|
+
|
|
12
38
|
// Modo não-TTY: usar defaults e chamar scaffold diretamente
|
|
13
39
|
if (!process.stdin.isTTY) {
|
|
14
40
|
const cfg = {
|
|
@@ -23,9 +49,11 @@ cmd.action(async (options) => {
|
|
|
23
49
|
await generators.scaffold(cfg)
|
|
24
50
|
const aiTools = String(options.aiTools || '').split(',').map(tool => tool.trim()).filter(Boolean)
|
|
25
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 })
|
|
26
54
|
for (const tool of aiTools) {
|
|
27
55
|
if (!supported.has(tool)) throw new Error(`Unsupported AI tool: ${tool}`)
|
|
28
|
-
await generators.installIntegrationTarget(tool, process.cwd())
|
|
56
|
+
await generators.installIntegrationTarget(tool, process.cwd(), scope)
|
|
29
57
|
}
|
|
30
58
|
console.log(`\n${t('init.success')}`)
|
|
31
59
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
@@ -34,7 +62,7 @@ cmd.action(async (options) => {
|
|
|
34
62
|
|
|
35
63
|
const { input, select, checkbox } = require('@inquirer/prompts')
|
|
36
64
|
|
|
37
|
-
let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, aiTools, requireReqInCommit
|
|
65
|
+
let projectName, projectType, frontend, pkgManager, backend, backendFramework, hooks, ci, aiTools, requireReqInCommit, scope
|
|
38
66
|
|
|
39
67
|
try {
|
|
40
68
|
projectName = await input({
|
|
@@ -158,6 +186,12 @@ cmd.action(async (options) => {
|
|
|
158
186
|
{ name: 'Kiro', value: 'kiro' },
|
|
159
187
|
],
|
|
160
188
|
})
|
|
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
|
+
|
|
161
195
|
} catch (err) {
|
|
162
196
|
// Fallback quando stdin fecha inesperadamente (ex: pipe em TTY simulado)
|
|
163
197
|
const cfg = {
|
|
@@ -175,10 +209,37 @@ cmd.action(async (options) => {
|
|
|
175
209
|
return
|
|
176
210
|
}
|
|
177
211
|
|
|
212
|
+
// O wizard de identidade roda como seu próprio formulário, logo após o
|
|
213
|
+
// formulário principal — o mesmo componente compartilhado (ADR D1) usado
|
|
214
|
+
// por `agents install`. É pulado inteiramente (nenhuma pergunta) quando a
|
|
215
|
+
// flag já resolveu a identidade acima, ou quando um arquivo de identidade
|
|
216
|
+
// já existe.
|
|
217
|
+
if (!skipIdentityWizard) {
|
|
218
|
+
const { catalog } = require('../integrations')
|
|
219
|
+
try {
|
|
220
|
+
await identityWizard.runIdentityWizard(catalog, home)
|
|
221
|
+
} catch {
|
|
222
|
+
// Fallback quando stdin fecha inesperadamente durante o wizard de
|
|
223
|
+
// identidade (mesma tolerância aplicada ao formulário principal acima).
|
|
224
|
+
await generators.scaffold({
|
|
225
|
+
projectName: path.basename(process.cwd()),
|
|
226
|
+
projectType: 'governance',
|
|
227
|
+
frontend: '',
|
|
228
|
+
backend: '',
|
|
229
|
+
pkgManager: 'npm',
|
|
230
|
+
hooks: 'none',
|
|
231
|
+
ci: 'none',
|
|
232
|
+
})
|
|
233
|
+
console.log(`\n${t('init.success')}`)
|
|
234
|
+
require('../generators/init').printArchitectNextSteps(process.cwd())
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
178
239
|
const cfg = { projectName, projectType, frontend, backend, backendFramework, pkgManager, hooks, ci, requireReqInCommit }
|
|
179
240
|
await generators.scaffold(cfg)
|
|
180
241
|
|
|
181
|
-
for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd())
|
|
242
|
+
for (const tool of (aiTools || [])) await generators.installIntegrationTarget(tool, process.cwd(), scope)
|
|
182
243
|
|
|
183
244
|
console.log(`\n${t('init.success')}`)
|
|
184
245
|
require('../generators/init').printArchitectNextSteps(process.cwd())
|