trackfw 2.15.0 → 2.16.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 +174 -0
- package/package.json +1 -1
- package/src/commands/identity-wizard.js +258 -0
- package/src/commands/init.js +54 -1
- package/src/commands/integrations.js +58 -2
- package/src/commands/update.js +8 -1
- 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 +51 -1
- package/src/integrations/render.js +149 -14
|
@@ -15,10 +15,21 @@
|
|
|
15
15
|
"projectType_fullstack": "Full-stack (frontend + backend)",
|
|
16
16
|
"projectType_frontend": "Solo frontend",
|
|
17
17
|
"projectType_backend": "Solo backend",
|
|
18
|
-
"projectType_governance": "Solo gobernanza (sin stack de build)"
|
|
18
|
+
"projectType_governance": "Solo gobernanza (sin stack de build)",
|
|
19
|
+
"identityPreset": "¿Cómo quieres nombrar a tus agentes?",
|
|
20
|
+
"identityCustomName": "Nombre de exhibición para el agente",
|
|
21
|
+
"identityNickname": "¿Cómo debemos llamarte? (opcional)"
|
|
19
22
|
},
|
|
20
23
|
"success": "✓ trackfw inicializado — ejecuta 'trackfw status' para ver el estado de gobernanza."
|
|
21
24
|
},
|
|
25
|
+
"identity": {
|
|
26
|
+
"inUse": "identidad: {{count}} agente(s) personalizado(s)",
|
|
27
|
+
"wizard": {
|
|
28
|
+
"confirmHeader": "── Confirmación ──────────────────────────────",
|
|
29
|
+
"confirmQuestion": "¿Confirmar?",
|
|
30
|
+
"nicknameRowLabel": "Cómo te llamaremos:"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
22
33
|
"adr": {
|
|
23
34
|
"description": "Gestionar Architecture Decision Records",
|
|
24
35
|
"new": {
|
|
@@ -15,10 +15,21 @@
|
|
|
15
15
|
"projectType_fullstack": "Full-stack (frontend + backend)",
|
|
16
16
|
"projectType_frontend": "Somente frontend",
|
|
17
17
|
"projectType_backend": "Somente backend",
|
|
18
|
-
"projectType_governance": "Somente governança (sem stack de build)"
|
|
18
|
+
"projectType_governance": "Somente governança (sem stack de build)",
|
|
19
|
+
"identityPreset": "Como você quer nomear seus agentes?",
|
|
20
|
+
"identityCustomName": "Nome de exibição para o agente",
|
|
21
|
+
"identityNickname": "Como devemos te chamar? (opcional)"
|
|
19
22
|
},
|
|
20
23
|
"success": "✓ trackfw inicializado — execute 'trackfw status' para ver o estado de governança."
|
|
21
24
|
},
|
|
25
|
+
"identity": {
|
|
26
|
+
"inUse": "identidade: {{count}} agente(s) personalizado(s)",
|
|
27
|
+
"wizard": {
|
|
28
|
+
"confirmHeader": "── Confirmação ──────────────────────────────",
|
|
29
|
+
"confirmQuestion": "Confirmar?",
|
|
30
|
+
"nicknameRowLabel": "Como você será chamado:"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
22
33
|
"adr": {
|
|
23
34
|
"description": "Gerenciar Architecture Decision Records",
|
|
24
35
|
"new": {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
|
|
7
|
+
// SCHEMA_VERSION é a versão de schema atual do arquivo de configuração de
|
|
8
|
+
// identidade. Espelha internal/identity/identity.go:schemaVersion.
|
|
9
|
+
const SCHEMA_VERSION = 1
|
|
10
|
+
|
|
11
|
+
// slugPattern casa um slug válido: segmentos alfanuméricos em minúsculas
|
|
12
|
+
// unidos por hífens simples, ex.: "zeus", "meu-agente".
|
|
13
|
+
const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
14
|
+
|
|
15
|
+
function identityPath(homeDir) {
|
|
16
|
+
return path.join(homeDir, '.trackfw', 'identity.json')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// load lê a configuração de identidade de <homeDir>/.trackfw/identity.json.
|
|
20
|
+
//
|
|
21
|
+
// Se o arquivo não existir, load retorna uma Config vazia sem erro — este é
|
|
22
|
+
// o caminho de não-regressão e nunca deve surgir como falha para quem ainda
|
|
23
|
+
// não customizou sua identidade. Espelha internal/identity/identity.go:Load.
|
|
24
|
+
function load(homeDir) {
|
|
25
|
+
const filename = identityPath(homeDir)
|
|
26
|
+
let data
|
|
27
|
+
try {
|
|
28
|
+
data = fs.readFileSync(filename, 'utf8')
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err.code === 'ENOENT') return { schema_version: SCHEMA_VERSION, agents: {} }
|
|
31
|
+
throw new Error(`identity: falha ao ler ${filename}: ${err.message}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let parsed
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(data)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw new Error(`identity: falha ao decodificar ${filename}: ${err.message}`)
|
|
39
|
+
}
|
|
40
|
+
if (parsed.schema_version !== SCHEMA_VERSION) {
|
|
41
|
+
throw new Error(`identity: versao de schema nao suportada em ${filename}: ${parsed.schema_version} (esperado ${SCHEMA_VERSION})`)
|
|
42
|
+
}
|
|
43
|
+
if (!parsed.agents || typeof parsed.agents !== 'object') parsed.agents = {}
|
|
44
|
+
return parsed
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// save persiste cfg em <homeDir>/.trackfw/identity.json de forma atômica.
|
|
48
|
+
// Espelha internal/identity/identity.go:Save — inclusive a omissão de
|
|
49
|
+
// user_nickname vazio e agents vazio, que resulta do comportamento
|
|
50
|
+
// omitempty do encoding/json do Go, e a ordenação alfabética das chaves do
|
|
51
|
+
// mapa agents, que resulta da ordenação de mapas do encoding/json do Go.
|
|
52
|
+
function save(homeDir, cfg) {
|
|
53
|
+
const out = { schema_version: SCHEMA_VERSION }
|
|
54
|
+
if (cfg.user_nickname) out.user_nickname = cfg.user_nickname
|
|
55
|
+
const agents = cfg.agents || {}
|
|
56
|
+
const ids = Object.keys(agents).sort()
|
|
57
|
+
if (ids.length > 0) {
|
|
58
|
+
const sortedAgents = {}
|
|
59
|
+
for (const id of ids) {
|
|
60
|
+
sortedAgents[id] = { display_name: agents[id].display_name, slug: agents[id].slug }
|
|
61
|
+
}
|
|
62
|
+
out.agents = sortedAgents
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = `${JSON.stringify(out, null, 2)}\n`
|
|
66
|
+
const filename = identityPath(homeDir)
|
|
67
|
+
atomicWrite(filename, data, 0o600)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// atomicWrite grava data em filename de forma atômica: cria um arquivo
|
|
71
|
+
// temporário no mesmo diretório, escreve, sincroniza e então renomeia para o
|
|
72
|
+
// lugar final. Espelha internal/identity/identity.go:atomicWrite.
|
|
73
|
+
function atomicWrite(filename, data, mode) {
|
|
74
|
+
const directory = path.dirname(filename)
|
|
75
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
76
|
+
const temporaryName = path.join(directory, `.trackfw-tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`)
|
|
77
|
+
const fd = fs.openSync(temporaryName, 'w', mode)
|
|
78
|
+
try {
|
|
79
|
+
fs.writeSync(fd, data)
|
|
80
|
+
fs.fsyncSync(fd)
|
|
81
|
+
} finally {
|
|
82
|
+
fs.closeSync(fd)
|
|
83
|
+
}
|
|
84
|
+
fs.renameSync(temporaryName, filename)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// agentName retorna o display name customizado sufixado com "-tf". Este é o
|
|
88
|
+
// único lugar do código onde o sufixo "-tf" é aplicado a um slug. Espelha
|
|
89
|
+
// internal/identity/identity.go:AgentName.
|
|
90
|
+
function agentName(slug) {
|
|
91
|
+
return `${slug}-tf`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// validate verifica cfg quanto à integridade estrutural e referencial:
|
|
95
|
+
// - todo id de agente em cfg.agents deve estar presente em knownIds
|
|
96
|
+
// - display_name não pode ser vazio
|
|
97
|
+
// - slug deve casar com ^[a-z0-9]+(-[a-z0-9]+)*$
|
|
98
|
+
// - slugs devem ser únicos entre os agentes
|
|
99
|
+
// - slug não pode terminar com o sufixo "-tf" (ele é acrescentado
|
|
100
|
+
// automaticamente por agentName)
|
|
101
|
+
// Espelha internal/identity/identity.go:Validate.
|
|
102
|
+
function validate(cfg, knownIds) {
|
|
103
|
+
const known = new Set(knownIds)
|
|
104
|
+
const seenSlugs = new Map()
|
|
105
|
+
const agents = (cfg && cfg.agents) || {}
|
|
106
|
+
|
|
107
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
108
|
+
if (!known.has(id)) {
|
|
109
|
+
throw new Error(`identity: agente desconhecido "${id}" nao esta na lista de agentes conhecidos`)
|
|
110
|
+
}
|
|
111
|
+
if (!agent.display_name) {
|
|
112
|
+
throw new Error(`identity: display_name vazio para o agente "${id}"`)
|
|
113
|
+
}
|
|
114
|
+
if (!SLUG_PATTERN.test(agent.slug)) {
|
|
115
|
+
throw new Error(`identity: slug invalido "${agent.slug}" para o agente "${id}" (esperado padrao ${SLUG_PATTERN.source})`)
|
|
116
|
+
}
|
|
117
|
+
if (seenSlugs.has(agent.slug)) {
|
|
118
|
+
throw new Error(`identity: slug duplicado "${agent.slug}" entre os agentes "${seenSlugs.get(agent.slug)}" e "${id}"`)
|
|
119
|
+
}
|
|
120
|
+
if (agent.slug.endsWith('-tf')) {
|
|
121
|
+
throw new Error(`identity: slug "${agent.slug}" do agente "${id}" nao deve incluir o sufixo "-tf"; ele e acrescentado automaticamente (use "${agent.slug.slice(0, -3)}" em vez de "${agent.slug}")`)
|
|
122
|
+
}
|
|
123
|
+
seenSlugs.set(agent.slug, id)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// lookup retorna a identidade configurada para id, se houver. Espelha
|
|
128
|
+
// internal/identity/identity.go:Lookup.
|
|
129
|
+
function lookup(cfg, id) {
|
|
130
|
+
const agents = (cfg && cfg.agents) || {}
|
|
131
|
+
return Object.prototype.hasOwnProperty.call(agents, id) ? agents[id] : undefined
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { SCHEMA_VERSION, load, save, agentName, validate, lookup, identityPath }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { slugify } = require('./slug')
|
|
4
|
+
const { knownAgentIds, preset, presetNames } = require('./preset')
|
|
5
|
+
const { load, save, agentName, validate, lookup, identityPath, SCHEMA_VERSION } = require('./config')
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
slugify,
|
|
9
|
+
knownAgentIds,
|
|
10
|
+
preset,
|
|
11
|
+
presetNames,
|
|
12
|
+
load,
|
|
13
|
+
save,
|
|
14
|
+
agentName,
|
|
15
|
+
validate,
|
|
16
|
+
lookup,
|
|
17
|
+
identityPath,
|
|
18
|
+
SCHEMA_VERSION,
|
|
19
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// knownAgentIds retorna a lista canônica de ids de agentes conhecidos pelo
|
|
4
|
+
// trackfw, em ordem estável e determinística. Espelha
|
|
5
|
+
// internal/identity/preset.go:KnownAgentIDs.
|
|
6
|
+
function knownAgentIds() {
|
|
7
|
+
return [
|
|
8
|
+
'architect',
|
|
9
|
+
'backend',
|
|
10
|
+
'frontend',
|
|
11
|
+
'qa',
|
|
12
|
+
'infra',
|
|
13
|
+
'security',
|
|
14
|
+
'dba',
|
|
15
|
+
'ux',
|
|
16
|
+
'code-quality',
|
|
17
|
+
'data',
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// presets contém o catálogo de presets temáticos de identidade. display_name
|
|
22
|
+
// e slug são HARDCODED aqui — não derivados chamando slugify em tempo de
|
|
23
|
+
// execução — porque essa é uma decisão explícita do ADR: hardcoding evita
|
|
24
|
+
// depender do comportamento de normalização Unicode sendo idêntico entre os
|
|
25
|
+
// CLIs Go, Node.js e Python. Espelha internal/identity/preset.go:presets.
|
|
26
|
+
const PRESETS = {
|
|
27
|
+
greek: {
|
|
28
|
+
architect: { display_name: 'Zeus', slug: 'zeus' },
|
|
29
|
+
backend: { display_name: 'Apolo', slug: 'apolo' },
|
|
30
|
+
frontend: { display_name: 'Afrodite', slug: 'afrodite' },
|
|
31
|
+
qa: { display_name: 'Ártemis', slug: 'artemis' },
|
|
32
|
+
infra: { display_name: 'Ares', slug: 'ares' },
|
|
33
|
+
security: { display_name: 'Hades', slug: 'hades' },
|
|
34
|
+
dba: { display_name: 'Poseidon', slug: 'poseidon' },
|
|
35
|
+
ux: { display_name: 'Atena', slug: 'atena' },
|
|
36
|
+
'code-quality': { display_name: 'Hefesto', slug: 'hefesto' },
|
|
37
|
+
data: { display_name: 'Métis', slug: 'metis' },
|
|
38
|
+
},
|
|
39
|
+
norse: {
|
|
40
|
+
architect: { display_name: 'Odin', slug: 'odin' },
|
|
41
|
+
backend: { display_name: 'Thor', slug: 'thor' },
|
|
42
|
+
frontend: { display_name: 'Freya', slug: 'freya' },
|
|
43
|
+
qa: { display_name: 'Heimdall', slug: 'heimdall' },
|
|
44
|
+
infra: { display_name: 'Tyr', slug: 'tyr' },
|
|
45
|
+
security: { display_name: 'Vidar', slug: 'vidar' },
|
|
46
|
+
dba: { display_name: 'Njord', slug: 'njord' },
|
|
47
|
+
ux: { display_name: 'Idun', slug: 'idun' },
|
|
48
|
+
'code-quality': { display_name: 'Bragi', slug: 'bragi' },
|
|
49
|
+
data: { display_name: 'Mimir', slug: 'mimir' },
|
|
50
|
+
},
|
|
51
|
+
potter: {
|
|
52
|
+
architect: { display_name: 'Dumbledore', slug: 'dumbledore' },
|
|
53
|
+
backend: { display_name: 'Snape', slug: 'snape' },
|
|
54
|
+
frontend: { display_name: 'Luna', slug: 'luna' },
|
|
55
|
+
qa: { display_name: 'Moody', slug: 'moody' },
|
|
56
|
+
infra: { display_name: 'Hagrid', slug: 'hagrid' },
|
|
57
|
+
security: { display_name: 'Kingsley', slug: 'kingsley' },
|
|
58
|
+
dba: { display_name: 'Flitwick', slug: 'flitwick' },
|
|
59
|
+
ux: { display_name: 'Tonks', slug: 'tonks' },
|
|
60
|
+
'code-quality': { display_name: 'Hermione', slug: 'hermione' },
|
|
61
|
+
data: { display_name: 'Trelawney', slug: 'trelawney' },
|
|
62
|
+
},
|
|
63
|
+
thrones: {
|
|
64
|
+
architect: { display_name: 'Tyrion', slug: 'tyrion' },
|
|
65
|
+
backend: { display_name: 'Jon', slug: 'jon' },
|
|
66
|
+
frontend: { display_name: 'Sansa', slug: 'sansa' },
|
|
67
|
+
qa: { display_name: 'Arya', slug: 'arya' },
|
|
68
|
+
infra: { display_name: 'Brienne', slug: 'brienne' },
|
|
69
|
+
security: { display_name: 'Varys', slug: 'varys' },
|
|
70
|
+
dba: { display_name: 'Samwell', slug: 'samwell' },
|
|
71
|
+
ux: { display_name: 'Margaery', slug: 'margaery' },
|
|
72
|
+
'code-quality': { display_name: 'Stannis', slug: 'stannis' },
|
|
73
|
+
data: { display_name: 'Bran', slug: 'bran' },
|
|
74
|
+
},
|
|
75
|
+
chaves: {
|
|
76
|
+
architect: { display_name: 'Girafales', slug: 'girafales' },
|
|
77
|
+
backend: { display_name: 'Madruga', slug: 'madruga' },
|
|
78
|
+
frontend: { display_name: 'Chiquinha', slug: 'chiquinha' },
|
|
79
|
+
qa: { display_name: 'Florinda', slug: 'florinda' },
|
|
80
|
+
infra: { display_name: 'Barriga', slug: 'barriga' },
|
|
81
|
+
security: { display_name: 'Quico', slug: 'quico' },
|
|
82
|
+
dba: { display_name: 'Clotilde', slug: 'clotilde' },
|
|
83
|
+
ux: { display_name: 'Popis', slug: 'popis' },
|
|
84
|
+
'code-quality': { display_name: 'Nhonho', slug: 'nhonho' },
|
|
85
|
+
data: { display_name: 'Godinez', slug: 'godinez' },
|
|
86
|
+
},
|
|
87
|
+
pioneers: {
|
|
88
|
+
architect: { display_name: 'Turing', slug: 'turing' },
|
|
89
|
+
backend: { display_name: 'Ritchie', slug: 'ritchie' },
|
|
90
|
+
frontend: { display_name: 'Berners-Lee', slug: 'berners-lee' },
|
|
91
|
+
qa: { display_name: 'Hamilton', slug: 'hamilton' },
|
|
92
|
+
infra: { display_name: 'Torvalds', slug: 'torvalds' },
|
|
93
|
+
security: { display_name: 'Diffie', slug: 'diffie' },
|
|
94
|
+
dba: { display_name: 'Codd', slug: 'codd' },
|
|
95
|
+
ux: { display_name: 'Norman', slug: 'norman' },
|
|
96
|
+
'code-quality': { display_name: 'Knuth', slug: 'knuth' },
|
|
97
|
+
data: { display_name: 'Hopper', slug: 'hopper' },
|
|
98
|
+
},
|
|
99
|
+
starwars: {
|
|
100
|
+
architect: { display_name: 'Yoda', slug: 'yoda' },
|
|
101
|
+
backend: { display_name: 'Han', slug: 'han' },
|
|
102
|
+
frontend: { display_name: 'Leia', slug: 'leia' },
|
|
103
|
+
qa: { display_name: 'Ahsoka', slug: 'ahsoka' },
|
|
104
|
+
infra: { display_name: 'Chewbacca', slug: 'chewbacca' },
|
|
105
|
+
security: { display_name: 'Vader', slug: 'vader' },
|
|
106
|
+
dba: { display_name: 'R2-D2', slug: 'r2-d2' },
|
|
107
|
+
ux: { display_name: 'Padmé', slug: 'padme' },
|
|
108
|
+
'code-quality': { display_name: 'Obi-Wan', slug: 'obi-wan' },
|
|
109
|
+
data: { display_name: 'C-3PO', slug: 'c-3po' },
|
|
110
|
+
},
|
|
111
|
+
tolkien: {
|
|
112
|
+
architect: { display_name: 'Gandalf', slug: 'gandalf' },
|
|
113
|
+
backend: { display_name: 'Aragorn', slug: 'aragorn' },
|
|
114
|
+
frontend: { display_name: 'Arwen', slug: 'arwen' },
|
|
115
|
+
qa: { display_name: 'Legolas', slug: 'legolas' },
|
|
116
|
+
infra: { display_name: 'Gimli', slug: 'gimli' },
|
|
117
|
+
security: { display_name: 'Boromir', slug: 'boromir' },
|
|
118
|
+
dba: { display_name: 'Elrond', slug: 'elrond' },
|
|
119
|
+
ux: { display_name: 'Galadriel', slug: 'galadriel' },
|
|
120
|
+
'code-quality': { display_name: 'Faramir', slug: 'faramir' },
|
|
121
|
+
data: { display_name: 'Bilbo', slug: 'bilbo' },
|
|
122
|
+
},
|
|
123
|
+
turma: {
|
|
124
|
+
architect: { display_name: 'Franjinha', slug: 'franjinha' },
|
|
125
|
+
backend: { display_name: 'Cebolinha', slug: 'cebolinha' },
|
|
126
|
+
frontend: { display_name: 'Magali', slug: 'magali' },
|
|
127
|
+
qa: { display_name: 'Mônica', slug: 'monica' },
|
|
128
|
+
infra: { display_name: 'Cascão', slug: 'cascao' },
|
|
129
|
+
security: { display_name: 'Bidu', slug: 'bidu' },
|
|
130
|
+
dba: { display_name: 'Marocas', slug: 'marocas' },
|
|
131
|
+
ux: { display_name: 'Anjinho', slug: 'anjinho' },
|
|
132
|
+
'code-quality': { display_name: 'Titi', slug: 'titi' },
|
|
133
|
+
data: { display_name: 'Chico', slug: 'chico' },
|
|
134
|
+
},
|
|
135
|
+
egyptian: {
|
|
136
|
+
architect: { display_name: 'Thoth', slug: 'thoth' },
|
|
137
|
+
backend: { display_name: 'Rá', slug: 'ra' },
|
|
138
|
+
frontend: { display_name: 'Ísis', slug: 'isis' },
|
|
139
|
+
qa: { display_name: 'Hórus', slug: 'horus' },
|
|
140
|
+
infra: { display_name: 'Ptah', slug: 'ptah' },
|
|
141
|
+
security: { display_name: 'Anúbis', slug: 'anubis' },
|
|
142
|
+
dba: { display_name: 'Seshat', slug: 'seshat' },
|
|
143
|
+
ux: { display_name: 'Bastet', slug: 'bastet' },
|
|
144
|
+
'code-quality': { display_name: 'Maat', slug: 'maat' },
|
|
145
|
+
data: { display_name: 'Osíris', slug: 'osiris' },
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// presetOrder é a ordem canônica e estável em que os nomes de presets são
|
|
150
|
+
// listados (ex.: por presetNames e em mensagens de erro).
|
|
151
|
+
const PRESET_ORDER = ['greek', 'norse', 'potter', 'thrones', 'chaves', 'pioneers', 'starwars', 'tolkien', 'turma', 'egyptian']
|
|
152
|
+
|
|
153
|
+
// preset retorna a Config de identidade para o nome de preset temático dado.
|
|
154
|
+
// Se name não for um preset conhecido, lança erro listando os nomes válidos.
|
|
155
|
+
//
|
|
156
|
+
// A Config retornada é sempre uma cópia: mutá-la não afeta a tabela de
|
|
157
|
+
// presets no nível do módulo nem qualquer outra Config retornada por uma
|
|
158
|
+
// chamada anterior a preset().
|
|
159
|
+
function preset(name) {
|
|
160
|
+
const agents = PRESETS[name]
|
|
161
|
+
if (!agents) {
|
|
162
|
+
throw new Error(`identity: preset desconhecido "${name}" (validos: ${presetNames().join(', ')})`)
|
|
163
|
+
}
|
|
164
|
+
const copied = {}
|
|
165
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
166
|
+
copied[id] = { display_name: agent.display_name, slug: agent.slug }
|
|
167
|
+
}
|
|
168
|
+
return { schema_version: 1, agents: copied }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// presetNames retorna os nomes de todos os presets conhecidos, na ordem
|
|
172
|
+
// estável: greek, norse, potter, thrones, chaves, pioneers, starwars,
|
|
173
|
+
// tolkien, turma, egyptian.
|
|
174
|
+
function presetNames() {
|
|
175
|
+
return [...PRESET_ORDER]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Verificação em tempo de carga do módulo: presetOrder e PRESETS devem estar
|
|
179
|
+
// em sincronia — guarda defensiva contra drift silencioso entre o mapa e a
|
|
180
|
+
// ordem documentada (espelha o init() de internal/identity/preset.go).
|
|
181
|
+
;(function assertPresetOrderInSync() {
|
|
182
|
+
if (PRESET_ORDER.length !== Object.keys(PRESETS).length) {
|
|
183
|
+
throw new Error('identity: presetOrder e PRESETS fora de sincronia')
|
|
184
|
+
}
|
|
185
|
+
const seen = new Set()
|
|
186
|
+
for (const name of PRESET_ORDER) {
|
|
187
|
+
if (!PRESETS[name]) {
|
|
188
|
+
throw new Error(`identity: preset "${name}" listado em presetOrder mas ausente de PRESETS`)
|
|
189
|
+
}
|
|
190
|
+
seen.add(name)
|
|
191
|
+
}
|
|
192
|
+
if (seen.size !== PRESET_ORDER.length) {
|
|
193
|
+
throw new Error('identity: presetOrder contem nomes duplicados')
|
|
194
|
+
}
|
|
195
|
+
})()
|
|
196
|
+
|
|
197
|
+
module.exports = { knownAgentIds, preset, presetNames }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// maxSlugLength é o tamanho máximo aceito para um slug gerado.
|
|
4
|
+
const MAX_SLUG_LENGTH = 40
|
|
5
|
+
|
|
6
|
+
// collapseDashes colapsa sequências de '-' em um único '-'.
|
|
7
|
+
function collapseDashes(value) {
|
|
8
|
+
let out = ''
|
|
9
|
+
let previousDash = false
|
|
10
|
+
for (const char of value) {
|
|
11
|
+
if (char === '-') {
|
|
12
|
+
if (previousDash) continue
|
|
13
|
+
previousDash = true
|
|
14
|
+
} else {
|
|
15
|
+
previousDash = false
|
|
16
|
+
}
|
|
17
|
+
out += char
|
|
18
|
+
}
|
|
19
|
+
return out
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// slugify converte input em um slug normalizado que casa com
|
|
23
|
+
// ^[a-z0-9]+(-[a-z0-9]+)*$.
|
|
24
|
+
//
|
|
25
|
+
// Passos, na ordem exata (espelham internal/identity/slug.go):
|
|
26
|
+
// 1. Normalização Unicode NFD + remoção de marcas diacríticas (\p{Mn}).
|
|
27
|
+
// 2. Lowercase.
|
|
28
|
+
// 3. Espaços (incluindo sequências) e underscores viram '-'.
|
|
29
|
+
// 4. Qualquer caractere fora de [a-z0-9-] é descartado.
|
|
30
|
+
// 5. Sequências de '-' colapsam em um único '-'.
|
|
31
|
+
// 6. '-' nas bordas (início/fim) são removidos.
|
|
32
|
+
// 7. Resultado vazio é erro.
|
|
33
|
+
// 8. Resultado com mais de 40 caracteres é erro.
|
|
34
|
+
//
|
|
35
|
+
// slugify nunca "corrige" silenciosamente uma entrada degenerada — sempre
|
|
36
|
+
// lança erro em vez de adivinhar.
|
|
37
|
+
function slugify(input) {
|
|
38
|
+
const folded = String(input).normalize('NFD').replace(/\p{Mn}/gu, '')
|
|
39
|
+
const lowered = folded.toLowerCase()
|
|
40
|
+
const replaced = lowered.replace(/[ _]/g, '-')
|
|
41
|
+
|
|
42
|
+
let filtered = ''
|
|
43
|
+
for (const char of replaced) {
|
|
44
|
+
if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') {
|
|
45
|
+
filtered += char
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const collapsed = collapseDashes(filtered)
|
|
50
|
+
const trimmed = collapsed.replace(/^-+/, '').replace(/-+$/, '')
|
|
51
|
+
|
|
52
|
+
if (trimmed === '') {
|
|
53
|
+
throw new Error(`identity: slug vazio para "${input}"`)
|
|
54
|
+
}
|
|
55
|
+
if (trimmed.length > MAX_SLUG_LENGTH) {
|
|
56
|
+
throw new Error(`identity: slug "${trimmed}" excede o tamanho maximo de ${MAX_SLUG_LENGTH} caracteres (tem ${trimmed.length})`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return trimmed
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { slugify, MAX_SLUG_LENGTH }
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const os = require('node:os')
|
|
4
|
+
|
|
3
5
|
const { catalog, items, target, surfaceFor, readAsset } = require('./catalog')
|
|
4
6
|
const { render } = require('./render')
|
|
5
7
|
const { IntegrationManager } = require('./manager')
|
|
6
8
|
const { legacyHashes } = require('./legacy')
|
|
9
|
+
const identityStore = require('../identity')
|
|
7
10
|
|
|
8
11
|
function parseSurfaces(values = []) {
|
|
9
12
|
const result = {}
|
|
@@ -40,6 +43,7 @@ function selections(kind, options = {}) {
|
|
|
40
43
|
|
|
41
44
|
function buildPlans(kind, options = {}) {
|
|
42
45
|
const selected = selections(kind, options)
|
|
46
|
+
const identityConfig = options.identity || { agents: {} }
|
|
43
47
|
const plans = []
|
|
44
48
|
for (const { target: targetEntry, surface } of selected.targets) {
|
|
45
49
|
const capability = surface.capabilities[kind]
|
|
@@ -50,7 +54,7 @@ function buildPlans(kind, options = {}) {
|
|
|
50
54
|
for (const item of selected.itemEntries) {
|
|
51
55
|
for (const installPath of paths) {
|
|
52
56
|
const destination = installPath.path.replace('{{id}}', item.id)
|
|
53
|
-
const content = render({ target: targetEntry.id, kind, item, content: readAsset(item), capability, destination })
|
|
57
|
+
const content = render({ target: targetEntry.id, kind, item, content: readAsset(item), capability, destination, identity: identityConfig })
|
|
54
58
|
const claim = { target: targetEntry.id, surface: surface.id, scope, kind, item: item.id }
|
|
55
59
|
plans.push({
|
|
56
60
|
claim,
|
|
@@ -89,7 +93,13 @@ function result(kind, plans, statuses) {
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
function execute(kind, operation, options = {}, roots = {}) {
|
|
92
|
-
|
|
96
|
+
// A identidade é resolvida do disco antes de buildPlans — pular esta etapa
|
|
97
|
+
// reverteria silenciosamente os nomes customizados dos agentes para os
|
|
98
|
+
// defaults neutros (espelha internal/commands/integrations_flags.go:
|
|
99
|
+
// executeIntegrationMutation/executeIntegrationList).
|
|
100
|
+
const homeRoot = roots.homeRoot || os.homedir()
|
|
101
|
+
const identityConfig = options.identity !== undefined ? options.identity : identityStore.load(homeRoot)
|
|
102
|
+
const plans = buildPlans(kind, { ...options, identity: identityConfig })
|
|
93
103
|
const manager = new IntegrationManager(roots)
|
|
94
104
|
let statuses
|
|
95
105
|
if (operation === 'list') statuses = manager.inspect(plans)
|
|
@@ -5,6 +5,8 @@ const fs = require('node:fs')
|
|
|
5
5
|
const os = require('node:os')
|
|
6
6
|
const path = require('node:path')
|
|
7
7
|
|
|
8
|
+
const { frontmatterName } = require('./render')
|
|
9
|
+
|
|
8
10
|
const SCHEMA_VERSION = 1
|
|
9
11
|
const sha256 = content => crypto.createHash('sha256').update(content).digest('hex')
|
|
10
12
|
const claimKey = claim => [claim.target, claim.surface, claim.scope, claim.kind, claim.item].join('\u0000')
|
|
@@ -28,7 +30,7 @@ class IntegrationManager {
|
|
|
28
30
|
} else if (path.isAbsolute(destination)) {
|
|
29
31
|
resolved = path.normalize(destination)
|
|
30
32
|
} else {
|
|
31
|
-
if (!destination || path.normalize(destination) !== destination || destination === '.' || destination.startsWith(
|
|
33
|
+
if (!destination || path.posix.normalize(destination) !== destination || destination === '.' || destination.startsWith('../')) throw new Error(`Unsafe destination: ${destination}`)
|
|
32
34
|
resolved = path.resolve(root, destination)
|
|
33
35
|
}
|
|
34
36
|
const rel = path.relative(root, resolved)
|
|
@@ -145,6 +147,7 @@ class IntegrationManager {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
preflight(operation, { plan, file }, manifest, force) {
|
|
150
|
+
if (operation !== 'uninstall') this.detectNameCollision(plan, file, force)
|
|
148
151
|
const status = this.inspectResolved(plan, file, manifest)
|
|
149
152
|
const record = manifest.artifacts[file]
|
|
150
153
|
const owned = Boolean(record && record.claims.some(claim => claimKey(claim) === claimKey(plan.claim)))
|
|
@@ -159,6 +162,53 @@ class IntegrationManager {
|
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
164
|
|
|
165
|
+
// detectNameCollision protege contra dois artefatos de agente gerenciados
|
|
166
|
+
// distintos declarando o mesmo "name" de frontmatter dentro do mesmo
|
|
167
|
+
// diretório de destino (ADR ADR-2026-07-25-identidade-personalizavel-de-
|
|
168
|
+
// agentes, seção D4). Isso importa porque, com identidades customizáveis,
|
|
169
|
+
// dois item.id diferentes poderiam resolver para o mesmo nome renderizado
|
|
170
|
+
// (ex.: dois agentes ambos apontados para o slug "zeus"), e algumas
|
|
171
|
+
// surfaces usam esse campo name para descoberta de agentes.
|
|
172
|
+
//
|
|
173
|
+
// Limitação: a varredura só inspeciona irmãos ".md", porque é o único
|
|
174
|
+
// formato onde temos uma forma barata e sem dependências (frontmatterName)
|
|
175
|
+
// de ler de volta o name declarado a partir de bytes já renderizados.
|
|
176
|
+
// Artefatos JSON (cli-agent-json/agent-json) e TOML (custom-agent-toml)
|
|
177
|
+
// não são varridos para colisões. Espelha
|
|
178
|
+
// internal/integrations/manager.go:detectNameCollision.
|
|
179
|
+
detectNameCollision(plan, file, force) {
|
|
180
|
+
if (plan.claim.kind !== 'agents') return
|
|
181
|
+
if (path.extname(file) !== '.md') return
|
|
182
|
+
const desiredName = frontmatterName(plan.content)
|
|
183
|
+
if (!desiredName) return
|
|
184
|
+
const directory = path.dirname(file)
|
|
185
|
+
let entries
|
|
186
|
+
try {
|
|
187
|
+
entries = fs.readdirSync(directory, { withFileTypes: true })
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err.code === 'ENOENT') return
|
|
190
|
+
throw new Error(`scan ${directory} for name collisions: ${err.message}`)
|
|
191
|
+
}
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (entry.isDirectory() || path.extname(entry.name) !== '.md') continue
|
|
194
|
+
const candidate = path.join(directory, entry.name)
|
|
195
|
+
if (candidate === file) continue
|
|
196
|
+
let data
|
|
197
|
+
try {
|
|
198
|
+
data = fs.readFileSync(candidate)
|
|
199
|
+
} catch {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
const candidateName = frontmatterName(data)
|
|
203
|
+
if (!candidateName || candidateName !== desiredName) continue
|
|
204
|
+
if (force) {
|
|
205
|
+
process.stderr.write(`aviso: ${candidate} declara o mesmo name ${desiredName} que ${file}; prosseguindo por --force\n`)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`artifact ${file} declares name ${desiredName} which collides with existing file ${candidate}`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
162
212
|
inspectResolved(plan, file, manifest) {
|
|
163
213
|
const record = manifest.artifacts[file]
|
|
164
214
|
const managed = Boolean(record && record.claims.some(claim => claimKey(claim) === claimKey(plan.claim)))
|