@ancleto/spec 0.7.2 → 0.9.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 +81 -9
- package/agents/orchestrator.md +8 -0
- package/commands/cleto-security.md +7 -0
- package/commands/cleto-transplant.md +30 -0
- package/package.json +2 -1
- package/profiles/test/agents/reviewer.md +35 -0
- package/profiles/test/agents/tester.md +68 -0
- package/profiles/test/commands/cleto-test-apply.md +10 -0
- package/profiles/test/commands/cleto-test-archive.md +9 -0
- package/profiles/test/commands/cleto-test-coverage.md +10 -0
- package/profiles/test/commands/cleto-test-heal.md +10 -0
- package/profiles/test/commands/cleto-test-proposal.md +10 -0
- package/profiles/test/templates/AGENTS.md +14 -0
- package/skills/ancleto-security/SKILL.md +40 -0
- package/src/cli/index.js +734 -35
package/src/cli/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
-
import { cp, mkdir, access, writeFile, readFile, readdir } from 'node:fs/promises'
|
|
3
|
+
import { cp, mkdir, access, writeFile, readFile, readdir, mkdtemp, rm } from 'node:fs/promises'
|
|
4
4
|
import { spawnSync } from 'node:child_process'
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
6
6
|
import { createInterface } from 'node:readline'
|
|
7
7
|
import { DatabaseSync } from 'node:sqlite'
|
|
8
|
-
import { join, dirname, resolve, basename, relative } from 'node:path'
|
|
8
|
+
import { join, dirname, resolve, basename, relative, isAbsolute } from 'node:path'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
10
|
import { homedir, tmpdir } from 'node:os'
|
|
11
11
|
import { createMemoryEngine, createReadonlyMemoryEngine, defaultMemoryDbPath } from '../core/memory/engine.js'
|
|
@@ -35,17 +35,20 @@ Uso:
|
|
|
35
35
|
ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
|
|
36
36
|
ancleto install --lang <codigo> auto | es | en | pt (idioma de los artifacts; auto = idioma de la conversacion)
|
|
37
37
|
ancleto install --exclude <globs> Paths excluidos del discovery, coma-separados (ej: "**/*.png,docs")
|
|
38
|
-
ancleto install --agent <nombre> opencode | claude | vscode | antigravity | cursor | roo (wizard si no esta guardado)
|
|
38
|
+
ancleto install --agent <nombre> opencode | claude | vscode | antigravity | cursor | roo | copilot (wizard si no esta guardado)
|
|
39
39
|
Elige el layout nativo a materializar (directorio + convencion de
|
|
40
40
|
nombre) y el adapter de frontmatter del host. NO arbitra el
|
|
41
41
|
descubrimiento entre hosts: si dos leen el mismo directorio, eso es
|
|
42
42
|
efecto del host (no se deduplica ni se elige "ganador").
|
|
43
|
+
antigravity instala skills, agents adaptados y commands como skills
|
|
44
|
+
(.agents/*) y mergea el MCP de workspace .agents/mcp_config.json.
|
|
45
|
+
ancleto install --profile <perfil> general | test (perfil de test automation con Playwright; persiste en .ancletorc)
|
|
43
46
|
ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
|
|
44
47
|
Parado en un proyecto con .ancletorc opera sobre ese proyecto;
|
|
45
48
|
usa --global para forzar el alcance global
|
|
46
49
|
ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
|
|
47
|
-
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>]
|
|
48
|
-
Crea .ancletorc en el repositorio actual
|
|
50
|
+
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>] [--no-mcp] [--profile <perfil>]
|
|
51
|
+
Crea .ancletorc en el repositorio actual y configura el MCP del host
|
|
49
52
|
(interactivo en TTY: banner + menu; Azure desactivado por defecto)
|
|
50
53
|
ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
|
|
51
54
|
ancleto discovery [--compress] [--include G] [--ignore G] [--token-budget N]
|
|
@@ -76,6 +79,9 @@ Uso:
|
|
|
76
79
|
ancleto mcp Servidor MCP de memoria propia (stdio) para tu IDE
|
|
77
80
|
expone searchMemory, recordRule y recordDecision
|
|
78
81
|
ancleto check Verifica integridad de archivos instalados vs manifiesto
|
|
82
|
+
ancleto export [--tar <file>] Empaqueta lo portable del proyecto (bundle + manifest.json sin rutas)
|
|
83
|
+
ancleto import <bundle> [--repair] Restaura el bundle regenerando el MCP local; --repair solo arregla entradas rotas
|
|
84
|
+
ancleto import --repair Repara in place el opencode.json con command inexistente
|
|
79
85
|
ancleto doctor Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
80
86
|
ancleto --help Esta ayuda
|
|
81
87
|
ancleto --version Version del paquete
|
|
@@ -151,12 +157,28 @@ async function writeProjectsRegistry(reg) {
|
|
|
151
157
|
return file
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
// D9: directorios locales de agents derivados del host/manifiesto, no de
|
|
161
|
+
// `.opencode/agents` fijo. Un proyecto antigravity usa `.agents/agents`.
|
|
162
|
+
function localAgentDirs(rc, agent) {
|
|
163
|
+
const dirs = new Set(rc?.installedPaths?.agents || [])
|
|
164
|
+
const spec = AGENT_TARGETS[agent || rc?.agent]?.agents
|
|
165
|
+
if (spec?.dir) dirs.add(spec.dir)
|
|
166
|
+
return [...dirs]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function hasLocalAgents(baseDir, rc, agent) {
|
|
170
|
+
for (const d of localAgentDirs(rc, agent)) {
|
|
171
|
+
if (await exists(join(baseDir, d))) return true
|
|
172
|
+
}
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
|
|
154
176
|
async function registerProject(projectDir, { agent, tier } = {}) {
|
|
155
177
|
const abs = resolve(projectDir)
|
|
156
178
|
const rc = await readAncletorc(abs)
|
|
157
179
|
const tierFile = join(abs, '.opencode', '.ancleto-tier')
|
|
158
180
|
const localTier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
159
|
-
const hasScopedAgents = await
|
|
181
|
+
const hasScopedAgents = await hasLocalAgents(abs, rc, agent)
|
|
160
182
|
const entry = {
|
|
161
183
|
path: abs.replace(/\\/g, '/'),
|
|
162
184
|
version: await packageVersion(),
|
|
@@ -231,7 +253,7 @@ async function projectStatus(entry) {
|
|
|
231
253
|
if (!rc) return { ...entry, exists: false, reason: 'sin .ancletorc' }
|
|
232
254
|
const tierFile = join(abs, '.opencode', '.ancleto-tier')
|
|
233
255
|
const tier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
234
|
-
const scoped = await
|
|
256
|
+
const scoped = await hasLocalAgents(abs, rc, entry.agent)
|
|
235
257
|
return {
|
|
236
258
|
...entry,
|
|
237
259
|
exists: true,
|
|
@@ -595,7 +617,7 @@ async function applyTier(agentsDir, tier, models = null) {
|
|
|
595
617
|
}
|
|
596
618
|
}
|
|
597
619
|
|
|
598
|
-
const SUPPORTED_AGENTS = ['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo']
|
|
620
|
+
const SUPPORTED_AGENTS = ['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo', 'copilot']
|
|
599
621
|
const DEFAULT_AGENT = 'opencode'
|
|
600
622
|
|
|
601
623
|
function askAgent() {
|
|
@@ -645,6 +667,43 @@ function langLabel(code) {
|
|
|
645
667
|
return { auto: 'automatico (idioma de la conversacion)', es: 'espanol', en: 'ingles', pt: 'portugues' }[code] || code
|
|
646
668
|
}
|
|
647
669
|
|
|
670
|
+
const SUPPORTED_PROFILES = ['general', 'test']
|
|
671
|
+
const DEFAULT_PROFILE = 'general'
|
|
672
|
+
|
|
673
|
+
function scanProfileFlag(args) {
|
|
674
|
+
const p = flagValue(args, '--profile')
|
|
675
|
+
if (!p) return null
|
|
676
|
+
const norm = p.toLowerCase() === 'test:playwright' ? 'test' : p.toLowerCase()
|
|
677
|
+
if (!SUPPORTED_PROFILES.includes(norm)) {
|
|
678
|
+
console.error(`ancleto: perfil invalido: ${p} (general|test)`)
|
|
679
|
+
process.exit(1)
|
|
680
|
+
}
|
|
681
|
+
return norm
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function validProfile(value) {
|
|
685
|
+
return SUPPORTED_PROFILES.includes(value) ? value : null
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Raiz de activos segun perfil: el general usa el repo; el perfil test usa
|
|
689
|
+
// profiles/test/ como overlay (agents/commands/templates). skills/ es comun.
|
|
690
|
+
function profileAssetsRoot(profile) {
|
|
691
|
+
return profile === 'test' ? join(ROOT, 'profiles', 'test') : ROOT
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Overlay del perfil test sobre los destinos nativos del host (misma ext y
|
|
695
|
+
// mismo adapter que la instalacion base). El paquete base queda intacto.
|
|
696
|
+
async function installProfileOverlay(projectDir, agent) {
|
|
697
|
+
const model = AGENT_TARGETS[agent] || AGENT_TARGETS[DEFAULT_AGENT]
|
|
698
|
+
for (const cat of ['agents', 'commands']) {
|
|
699
|
+
const spec = model[cat]
|
|
700
|
+
if (!spec) continue
|
|
701
|
+
const src = join(ROOT, 'profiles', 'test', cat)
|
|
702
|
+
if (!(await exists(src))) continue
|
|
703
|
+
await installAssetFiles(cat, projectDir, spec, agent, src)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
648
707
|
const EXCLUDE_PRESETS = [
|
|
649
708
|
{ key: 'tests', label: 'tests (*.test.*, *.spec.*, __tests__)', recommended: true, globs: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**'] },
|
|
650
709
|
{ key: 'assets', label: 'assets pesados (*.png, *.jpg, *.svg, *.ico, public/)', recommended: true, globs: ['**/*.png', '**/*.jpg', '**/*.svg', '**/*.ico', 'public'] },
|
|
@@ -729,6 +788,115 @@ async function mergeMcp(configDir, mcpMap) {
|
|
|
729
788
|
return { file: targets.join(', '), added: [...new Set(added)] }
|
|
730
789
|
}
|
|
731
790
|
|
|
791
|
+
// D6: MCP de workspace de Antigravity (`.agents/mcp_config.json`). Merge NO
|
|
792
|
+
// destructivo: se preservan `mcpServers` y demas claves top-level, no se pisa un
|
|
793
|
+
// servidor homonimo, y un JSON invalido se avisa sin sobrescribir. Esquema del host
|
|
794
|
+
// `{ command, args, env }` (se descartan `type`/`enabled` de opencode).
|
|
795
|
+
async function mergeAntigravityMcp(projectDir, mcpMap) {
|
|
796
|
+
if (Object.keys(mcpMap).length === 0) return { file: null, added: [] }
|
|
797
|
+
const file = join(projectDir, '.agents', 'mcp_config.json')
|
|
798
|
+
let cfg = {}
|
|
799
|
+
if (await exists(file)) {
|
|
800
|
+
try {
|
|
801
|
+
cfg = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
|
|
802
|
+
} catch {
|
|
803
|
+
console.warn(`ancleto: no se pudo leer ${basename(file)} como JSON; MCP de Antigravity no se modifico`)
|
|
804
|
+
return { file, added: [] }
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
cfg.mcpServers = cfg.mcpServers || {}
|
|
808
|
+
const added = []
|
|
809
|
+
for (const [name, def] of Object.entries(mcpMap)) {
|
|
810
|
+
if (cfg.mcpServers[name]) continue
|
|
811
|
+
const command = Array.isArray(def.command) ? def.command : [def.command]
|
|
812
|
+
const server = { command: command[0], args: command.slice(1) }
|
|
813
|
+
if (def.env) server.env = def.env
|
|
814
|
+
cfg.mcpServers[name] = server
|
|
815
|
+
added.push(name)
|
|
816
|
+
}
|
|
817
|
+
await mkdir(dirname(file), { recursive: true })
|
|
818
|
+
await writeFile(file, JSON.stringify(cfg, null, 2) + '\n')
|
|
819
|
+
return { file, added }
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Una entrada MCP esta rota cuando su binario es una ruta inexistente en este
|
|
823
|
+
// host: absoluta posix ausente, estilo Windows (caso T480) o command ausente.
|
|
824
|
+
// Comandos relativos (npx, PATH) se presumen sanos.
|
|
825
|
+
function mcpCommandBroken(entry) {
|
|
826
|
+
const cmd = entry?.command
|
|
827
|
+
if (!Array.isArray(cmd) || cmd.length === 0) return true
|
|
828
|
+
const bin = cmd[0]
|
|
829
|
+
if (typeof bin !== 'string' || bin.length === 0) return true
|
|
830
|
+
if (/^[A-Za-z]:[\\/]/.test(bin)) return true
|
|
831
|
+
if (isAbsolute(bin)) return !existsSync(bin)
|
|
832
|
+
return false
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Merge no destructivo hacia copilot-mcp.json del proyecto (nunca toca
|
|
836
|
+
// copilot-instructions.md: ese archivo es del usuario).
|
|
837
|
+
async function mergeCopilotMcp(projectDir, mcpMap) {
|
|
838
|
+
if (Object.keys(mcpMap).length === 0) return { file: null, added: [] }
|
|
839
|
+
const file = join(projectDir, 'copilot-mcp.json')
|
|
840
|
+
let cfg = {}
|
|
841
|
+
if (await exists(file)) {
|
|
842
|
+
try {
|
|
843
|
+
cfg = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
|
|
844
|
+
} catch {
|
|
845
|
+
console.warn('ancleto: no se pudo leer copilot-mcp.json como JSON; MCP no se agrego ahi')
|
|
846
|
+
return { file, added: [] }
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
cfg.mcp = cfg.mcp || {}
|
|
850
|
+
const added = []
|
|
851
|
+
for (const [name, def] of Object.entries(mcpMap)) {
|
|
852
|
+
if (cfg.mcp[name]) continue
|
|
853
|
+
cfg.mcp[name] = def
|
|
854
|
+
added.push(name)
|
|
855
|
+
}
|
|
856
|
+
await writeFile(file, JSON.stringify(cfg, null, 2) + '\n')
|
|
857
|
+
return { file, added }
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// D6: dueno unico del MCP de host. Antigravity mergea `.agents/mcp_config.json`;
|
|
861
|
+
// el resto conserva `mergeMcp` sobre opencode.json (targetDir = .opencode del
|
|
862
|
+
// proyecto o config global).
|
|
863
|
+
async function setupHostMcp(targetDir, mcpMap, { projectDir = null, agent = null } = {}) {
|
|
864
|
+
if (Object.keys(mcpMap).length === 0) return { file: null, added: [] }
|
|
865
|
+
if (agent === 'antigravity' && projectDir) return mergeAntigravityMcp(projectDir, mcpMap)
|
|
866
|
+
await mkdir(targetDir, { recursive: true })
|
|
867
|
+
return mergeMcp(targetDir, mcpMap)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Regenera entradas rotas y agrega ausentes en copilot-mcp.json (para upgrade).
|
|
871
|
+
async function refreshCopilotMcp(projectDir) {
|
|
872
|
+
const file = join(projectDir, 'copilot-mcp.json')
|
|
873
|
+
let cfg = {}
|
|
874
|
+
if (await exists(file)) {
|
|
875
|
+
try {
|
|
876
|
+
cfg = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
|
|
877
|
+
} catch {
|
|
878
|
+
cfg = {}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
cfg.mcp = cfg.mcp || {}
|
|
882
|
+
const fresh = buildDefaultMcp({ withEngram: Boolean(cfg.mcp.engram) })
|
|
883
|
+
const fixed = []
|
|
884
|
+
for (const [name, def] of Object.entries(cfg.mcp)) {
|
|
885
|
+
if (mcpCommandBroken(def) && fresh[name]) {
|
|
886
|
+
cfg.mcp[name] = fresh[name]
|
|
887
|
+
fixed.push(name)
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
for (const [name, def] of Object.entries(fresh)) {
|
|
891
|
+
if (!cfg.mcp[name]) {
|
|
892
|
+
cfg.mcp[name] = def
|
|
893
|
+
fixed.push(`${name} (agregado)`)
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
await writeFile(file, JSON.stringify(cfg, null, 2) + '\n')
|
|
897
|
+
return { file, fixed }
|
|
898
|
+
}
|
|
899
|
+
|
|
732
900
|
async function copyAssets(dest) {
|
|
733
901
|
await mkdir(dest, { recursive: true })
|
|
734
902
|
for (const d of ASSETS) {
|
|
@@ -755,11 +923,26 @@ const AGENT_TARGETS = {
|
|
|
755
923
|
agents: { dir: '.github/agents', ext: '.agent.md' },
|
|
756
924
|
commands: { dir: '.github/prompts', ext: '.prompt.md' }
|
|
757
925
|
},
|
|
758
|
-
// Antigravity
|
|
759
|
-
//
|
|
760
|
-
|
|
926
|
+
// Antigravity: agents adaptados en `.agents/agents` y commands materializados
|
|
927
|
+
// como skills dentro de `.agents/skills` (Antigravity no tiene dir de commands;
|
|
928
|
+
// los Workflows estan deprecados a favor de Agent Skills). `package: 'skill-dir'`
|
|
929
|
+
// marca que el asset se empaqueta como `<dir>/<n>/SKILL.md`, no como archivo plano.
|
|
930
|
+
antigravity: {
|
|
931
|
+
skills: { dir: '.agents/skills' },
|
|
932
|
+
agents: { dir: '.agents/agents', ext: MD_EXT },
|
|
933
|
+
commands: { dir: '.agents/skills', package: 'skill-dir' }
|
|
934
|
+
},
|
|
935
|
+
// Cursor y Roo: solo skills (D3/D7). .agents/skills es punto de lectura
|
|
936
|
+
// compartido (D2), no destino universal.
|
|
761
937
|
cursor: { skills: { dir: '.cursor/skills' }, agents: null, commands: null },
|
|
762
|
-
roo: { skills: { dir: '.roo/skills' }, agents: null, commands: null }
|
|
938
|
+
roo: { skills: { dir: '.roo/skills' }, agents: null, commands: null },
|
|
939
|
+
// Copilot: host de prompts. Agents y commands comparten .github/prompts con
|
|
940
|
+
// ext .prompt.md (sin colision: agents/*.md vs cleto-*.md). Sin skills (D12).
|
|
941
|
+
copilot: {
|
|
942
|
+
skills: null,
|
|
943
|
+
agents: { dir: '.github/prompts', ext: '.prompt.md' },
|
|
944
|
+
commands: { dir: '.github/prompts', ext: '.prompt.md' }
|
|
945
|
+
}
|
|
763
946
|
}
|
|
764
947
|
|
|
765
948
|
const ANCLETO_SKILLS = ['ancleto-new', 'ancleto-propose', 'ancleto-apply', 'ancleto-verify', 'ancleto-archive', 'ancleto-bulk-archive', 'ancleto-continue', 'ancleto-explore', 'ancleto-ff', 'ancleto-onboard', 'ancleto-workflow']
|
|
@@ -788,14 +971,130 @@ function serializeFrontmatter(parsed) {
|
|
|
788
971
|
return `${parsed.open}${inner}${parsed.close}${parsed.nl}${parsed.body}`
|
|
789
972
|
}
|
|
790
973
|
|
|
791
|
-
// D4/D5: hosts cuyo formato de agents difiere del origen requieren adaptacion
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
const AGENT_ADAPTER_HOSTS = new Set(['claude', 'vscode'])
|
|
974
|
+
// D4/D5: hosts cuyo formato de agents difiere del origen requieren adaptacion.
|
|
975
|
+
// Claude/VS Code/Copilot eliminan las claves de opencode; Antigravity usa una
|
|
976
|
+
// transformacion propia (D2). El resto es identidad (preservacion verbatim).
|
|
977
|
+
const AGENT_ADAPTER_HOSTS = new Set(['claude', 'vscode', 'antigravity', 'copilot'])
|
|
795
978
|
const AGENT_ADAPTER_DROP = new Set(['mode', 'color', 'temperature', 'permission', 'model', 'tools'])
|
|
796
979
|
|
|
797
|
-
|
|
980
|
+
// Solo Copilot: los tiers no cambian modelos (el usuario elige en el picker);
|
|
981
|
+
// el orchestrator lo recuerda al empezar.
|
|
982
|
+
const COPILOT_MODEL_NOTE = `
|
|
983
|
+
## Nota de modelo (solo Copilot)
|
|
984
|
+
|
|
985
|
+
Elegí el modelo en el picker de Copilot antes de empezar: los tiers de ancleto
|
|
986
|
+
(normal|minimo|gratis) no cambian modelos acá, solo el nivel de esfuerzo:
|
|
987
|
+
normal = flujo completo, minimo = pasos agrupados y económicos, gratis = modelo
|
|
988
|
+
gratis disponible en tu cuenta.
|
|
989
|
+
`
|
|
990
|
+
|
|
991
|
+
// D3: tabla unica (fijada contra la tabla oficial de frontmatter de Custom
|
|
992
|
+
// Subagents) de ids de Antigravity. Conjunto CERRADO: solo se emiten estos ids.
|
|
993
|
+
// Los ids solo-SDK (`find_file`, `edit_file`, `search_web`, `read_url_content`,
|
|
994
|
+
// ...), los de comunidad (`write_to_file`, `call_mcp_tool`,
|
|
995
|
+
// `multi_replace_file_content`, ...) y los de delegacion (`invoke_subagent`,
|
|
996
|
+
// `start_subagent`, `define_subagent`) NO se emiten: no estan confirmados en el
|
|
997
|
+
// frontmatter y un id inexistente cuelga el subagent (Known Issue). Una clave no
|
|
998
|
+
// listada aqui se omite con aviso a stderr, nunca en silencio (D5).
|
|
999
|
+
const ANTIGRAVITY_TOOL_MAP = {
|
|
1000
|
+
read: 'view_file',
|
|
1001
|
+
edit: 'replace_file_content',
|
|
1002
|
+
grep: 'grep_search',
|
|
1003
|
+
bash: 'run_command',
|
|
1004
|
+
todowrite: 'manage_task'
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Hijos directos del mapa `tools:` con su valor crudo (p. ej. `read: true`).
|
|
1008
|
+
function parseToolFlags(entry) {
|
|
1009
|
+
const flags = []
|
|
1010
|
+
if (!entry) return flags
|
|
1011
|
+
let childIndent = null
|
|
1012
|
+
for (const line of entry.lines.slice(1)) {
|
|
1013
|
+
const m = /^(\s+)([A-Za-z0-9_-]+):\s*(.*?)\s*$/.exec(line)
|
|
1014
|
+
if (!m) continue
|
|
1015
|
+
if (childIndent === null) childIndent = m[1].length
|
|
1016
|
+
if (m[1].length !== childIndent) continue
|
|
1017
|
+
flags.push({ key: m[2], value: m[3] })
|
|
1018
|
+
}
|
|
1019
|
+
return flags
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function frontmatterValue(entry, fallback = null) {
|
|
1023
|
+
if (!entry) return fallback
|
|
1024
|
+
const m = /^[A-Za-z0-9_-]+:\s*(.*?)\s*$/.exec(entry.lines[0] || '')
|
|
1025
|
+
return m ? m[1] : fallback
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// D2/D3/D5/D8: adapta el frontmatter de un agent de opencode al de Antigravity.
|
|
1029
|
+
// `name` (requerido por el host) se inyecta desde el nombre del archivo. `tools` se
|
|
1030
|
+
// emite siempre como lista (el default del host es la lista vacia). Nunca se inventa
|
|
1031
|
+
// un id: una clave sin id verificado se omite con aviso (un id inexistente cuelga el
|
|
1032
|
+
// subagent).
|
|
1033
|
+
function adaptAntigravityFrontmatter(content, name) {
|
|
1034
|
+
const parsed = parseFrontmatter(content)
|
|
1035
|
+
if (!parsed.hasFrontmatter) return content
|
|
1036
|
+
const byKey = (k) => parsed.entries.find((e) => e.key === k)
|
|
1037
|
+
// Claves que el adaptador gestiona: las dropeadas (AGENT_ADAPTER_DROP), las que
|
|
1038
|
+
// re-emite (name/tools/mainAgent/subagent/model/commandExecutionPolicy) y los campos
|
|
1039
|
+
// que preserva del origen (description/skills/mcpServers). Se filtran TODAS del
|
|
1040
|
+
// conjunto conservado para que ninguna pueda quedar duplicada, y se re-emiten una
|
|
1041
|
+
// sola vez en orden determinista, sin importar lo que declare la fuente.
|
|
1042
|
+
const managed = new Set([
|
|
1043
|
+
...AGENT_ADAPTER_DROP,
|
|
1044
|
+
'name',
|
|
1045
|
+
'description',
|
|
1046
|
+
'mainAgent',
|
|
1047
|
+
'subagent',
|
|
1048
|
+
'commandExecutionPolicy',
|
|
1049
|
+
'mcpServers',
|
|
1050
|
+
'skills'
|
|
1051
|
+
])
|
|
1052
|
+
// Se conservan las claves extra (no gestionadas); los campos preservados se
|
|
1053
|
+
// re-emiten abajo desde su primera ocurrencia en el origen.
|
|
1054
|
+
const kept = parsed.entries.filter((e) => e.key && !managed.has(e.key))
|
|
1055
|
+
|
|
1056
|
+
const toolIds = []
|
|
1057
|
+
for (const { key, value } of parseToolFlags(byKey('tools'))) {
|
|
1058
|
+
if (value === 'false') continue
|
|
1059
|
+
if (key === 'skill') continue // no es tool: se cubre por `skills`/surfaceo automatico (OQ2)
|
|
1060
|
+
const id = ANTIGRAVITY_TOOL_MAP[key]
|
|
1061
|
+
if (id) {
|
|
1062
|
+
toolIds.push(id)
|
|
1063
|
+
continue
|
|
1064
|
+
}
|
|
1065
|
+
// D3/D5: regla dura. Un id fuera del frontmatter confirmado (exista o no en el
|
|
1066
|
+
// SDK) no se emite: un id inexistente cuelga el subagent. Se omite con aviso,
|
|
1067
|
+
// nunca en silencio. El uso de MCP NO se infiere desde claves desconocidas del
|
|
1068
|
+
// mapa `tools`: se expresa por `mcpServers`/`.agents/mcp_config.json` (D6).
|
|
1069
|
+
console.error(`skip tool '${key}': no verified Antigravity id for agent '${name}'`)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const mode = frontmatterValue(byKey('mode'))
|
|
1073
|
+
const mainAgent = mode === 'subagent' ? 'false' : 'true'
|
|
1074
|
+
const subagent = mode === 'primary' ? 'false' : 'true'
|
|
1075
|
+
|
|
1076
|
+
// Orden determinista: name, description, tools, mainAgent, subagent, model,
|
|
1077
|
+
// commandExecutionPolicy, mcpServers, skills, y luego las claves extra conservadas.
|
|
1078
|
+
const out = [{ key: 'name', lines: [`name: ${name}`] }]
|
|
1079
|
+
const description = byKey('description')
|
|
1080
|
+
if (description) out.push(description)
|
|
1081
|
+
out.push({ key: 'tools', lines: [`tools: [${toolIds.join(', ')}]`] })
|
|
1082
|
+
out.push({ key: 'mainAgent', lines: [`mainAgent: ${mainAgent}`] })
|
|
1083
|
+
out.push({ key: 'subagent', lines: [`subagent: ${subagent}`] })
|
|
1084
|
+
out.push({ key: 'model', lines: [`model: inherit`] })
|
|
1085
|
+
out.push({ key: 'commandExecutionPolicy', lines: [`commandExecutionPolicy: sandbox`] })
|
|
1086
|
+
const mcpServers = byKey('mcpServers')
|
|
1087
|
+
if (mcpServers) out.push(mcpServers)
|
|
1088
|
+
const skills = byKey('skills')
|
|
1089
|
+
if (skills) out.push(skills)
|
|
1090
|
+
out.push(...kept)
|
|
1091
|
+
parsed.entries = out
|
|
1092
|
+
return serializeFrontmatter(parsed)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function adaptFrontmatter(content, host, assetKind, name) {
|
|
798
1096
|
if (assetKind !== 'agents' || !AGENT_ADAPTER_HOSTS.has(host)) return content
|
|
1097
|
+
if (host === 'antigravity') return adaptAntigravityFrontmatter(content, name)
|
|
799
1098
|
const parsed = parseFrontmatter(content)
|
|
800
1099
|
if (!parsed.hasFrontmatter) return content
|
|
801
1100
|
parsed.entries = parsed.entries.filter((e) => !(e.key && AGENT_ADAPTER_DROP.has(e.key)))
|
|
@@ -822,14 +1121,40 @@ async function copyDirTransformed(srcDir, destDir, host, assetKind) {
|
|
|
822
1121
|
}
|
|
823
1122
|
}
|
|
824
1123
|
|
|
825
|
-
async function installAssetFiles(cat, projectDir, spec, host) {
|
|
1124
|
+
async function installAssetFiles(cat, projectDir, spec, host, srcDir = null) {
|
|
826
1125
|
const destDir = join(projectDir, spec.dir)
|
|
827
1126
|
await mkdir(destDir, { recursive: true })
|
|
828
|
-
|
|
1127
|
+
const origin = srcDir || join(ROOT, cat)
|
|
1128
|
+
for (const e of await readdir(origin, { withFileTypes: true })) {
|
|
1129
|
+
if (!e.isFile() || !e.name.endsWith(MD_EXT)) continue
|
|
1130
|
+
const base = e.name.slice(0, -MD_EXT.length)
|
|
1131
|
+
const content = await readFile(join(origin, e.name), 'utf8')
|
|
1132
|
+
const adapted = adaptFrontmatter(content, host, cat, base)
|
|
1133
|
+
const body = (host === 'copilot' && cat === 'agents' && base === 'orchestrator') ? adapted + COPILOT_MODEL_NOTE : adapted
|
|
1134
|
+
await writeFile(join(destDir, `${base}${spec.ext}`), body)
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// D4: un command de ancleto convertido en skill (`name` + `description` del origen,
|
|
1139
|
+
// body verbatim). Antigravity no tiene dir de commands: sus `/cleto-*` viven como
|
|
1140
|
+
// command-skills dentro de `.agents/skills`.
|
|
1141
|
+
function commandToSkill(content, name) {
|
|
1142
|
+
const parsed = parseFrontmatter(content)
|
|
1143
|
+
const desc = parsed.hasFrontmatter ? parsed.entries.find((e) => e.key === 'description') : null
|
|
1144
|
+
const head = ['---', `name: ${name}`, ...(desc ? desc.lines : []), '---'].join('\n')
|
|
1145
|
+
const body = parsed.hasFrontmatter ? parsed.body : content
|
|
1146
|
+
return `${head}\n${body}`
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Materializa `commands/*.md` como `.agents/skills/<n>/SKILL.md` (asset empaquetado).
|
|
1150
|
+
async function installCommandSkills(projectDir, spec) {
|
|
1151
|
+
const destBase = join(projectDir, spec.dir)
|
|
1152
|
+
for (const e of await readdir(join(ROOT, 'commands'), { withFileTypes: true })) {
|
|
829
1153
|
if (!e.isFile() || !e.name.endsWith(MD_EXT)) continue
|
|
830
1154
|
const base = e.name.slice(0, -MD_EXT.length)
|
|
831
|
-
const content = await readFile(join(ROOT,
|
|
832
|
-
await
|
|
1155
|
+
const content = await readFile(join(ROOT, 'commands', e.name), 'utf8')
|
|
1156
|
+
await mkdir(join(destBase, base), { recursive: true })
|
|
1157
|
+
await writeFile(join(destBase, base, 'SKILL.md'), commandToSkill(content, base))
|
|
833
1158
|
}
|
|
834
1159
|
}
|
|
835
1160
|
|
|
@@ -852,6 +1177,12 @@ async function installAgentAssets(projectDir, agent) {
|
|
|
852
1177
|
warnUnsupportedAsset(cat, agent)
|
|
853
1178
|
continue
|
|
854
1179
|
}
|
|
1180
|
+
// D4: un asset empaquetado (commands de Antigravity) vive dentro del layout de
|
|
1181
|
+
// skills; no se registra un directorio de commands propio en installedPaths.
|
|
1182
|
+
if (spec.package === 'skill-dir') {
|
|
1183
|
+
await installCommandSkills(projectDir, spec)
|
|
1184
|
+
continue
|
|
1185
|
+
}
|
|
855
1186
|
await installAssetFiles(cat, projectDir, spec, agent)
|
|
856
1187
|
written[cat].push(spec.dir)
|
|
857
1188
|
}
|
|
@@ -877,6 +1208,16 @@ function installedExtFor(cat, dirRel) {
|
|
|
877
1208
|
return null
|
|
878
1209
|
}
|
|
879
1210
|
|
|
1211
|
+
// D9: hosts realmente instalados derivados del manifiesto (union de destinos
|
|
1212
|
+
// escritos), no de un unico `rc.agent`. Un proyecto multi-host debe sumar los
|
|
1213
|
+
// esperados de todos los hosts para no reportar huerfanos falsos.
|
|
1214
|
+
function installedHostsFromPaths(ip) {
|
|
1215
|
+
const dirs = new Set([...(ip?.skills || []), ...(ip?.agents || []), ...(ip?.commands || [])])
|
|
1216
|
+
return Object.entries(AGENT_TARGETS)
|
|
1217
|
+
.filter(([, t]) => ['skills', 'agents', 'commands'].some((cat) => t[cat]?.dir && dirs.has(t[cat].dir)))
|
|
1218
|
+
.map(([host]) => host)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
880
1221
|
const CHANGES_ROOT = 'aspec'
|
|
881
1222
|
|
|
882
1223
|
const DEFAULT_ASPEC_CONFIG = `# aspec project configuration
|
|
@@ -893,6 +1234,11 @@ async function scaffoldAspec(projectDir) {
|
|
|
893
1234
|
}
|
|
894
1235
|
}
|
|
895
1236
|
|
|
1237
|
+
async function scaffoldTestspec(projectDir) {
|
|
1238
|
+
await mkdir(join(projectDir, 'testspec', 'specs'), { recursive: true })
|
|
1239
|
+
await mkdir(join(projectDir, 'testspec', 'changes'), { recursive: true })
|
|
1240
|
+
}
|
|
1241
|
+
|
|
896
1242
|
const LEGACY_OPENSPEC_DIR = 'openspec'
|
|
897
1243
|
const MIGRATION_MARKER = '.migrated-from-openspec'
|
|
898
1244
|
|
|
@@ -926,6 +1272,97 @@ async function migrateLegacyOpenspec(projectDir) {
|
|
|
926
1272
|
console.log('ancleto: contenido migrado de openspec/ a aspec/ (openspec/ se conserva como backup)')
|
|
927
1273
|
}
|
|
928
1274
|
|
|
1275
|
+
// Clasifica openspec/ pre-existente: 'none' (ausente o carpeta vacia),
|
|
1276
|
+
// 'external' (layout OpenSpec estandar: specs/ con archivos, o AGENTS.md con
|
|
1277
|
+
// firma OpenSpec) o 'legacy' (resto: rama original, intacta).
|
|
1278
|
+
async function detectLegacyOpenSpec(projectDir) {
|
|
1279
|
+
const legacyDir = join(projectDir, LEGACY_OPENSPEC_DIR)
|
|
1280
|
+
let entries = []
|
|
1281
|
+
try {
|
|
1282
|
+
entries = await readdir(legacyDir)
|
|
1283
|
+
} catch {
|
|
1284
|
+
return 'none'
|
|
1285
|
+
}
|
|
1286
|
+
if (entries.length === 0) return 'none' // carpeta vacia: no-op silencioso
|
|
1287
|
+
try {
|
|
1288
|
+
if ((await readdir(join(legacyDir, 'specs'))).length > 0) return 'external'
|
|
1289
|
+
} catch {
|
|
1290
|
+
// sin specs/: sigue el resto de firmas
|
|
1291
|
+
}
|
|
1292
|
+
for (const candidate of [join(projectDir, 'AGENTS.md'), join(legacyDir, 'AGENTS.md')]) {
|
|
1293
|
+
try {
|
|
1294
|
+
if (/openspec/i.test(await readFile(candidate, 'utf8'))) return 'external'
|
|
1295
|
+
} catch {
|
|
1296
|
+
// archivo inexistente o ilegible: no aporta firma
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return 'legacy'
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
async function askOpenspecImport() {
|
|
1303
|
+
if (!process.stdin.isTTY) return false
|
|
1304
|
+
const ask = (q) => new Promise((resolve) => {
|
|
1305
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
1306
|
+
rl.question(q, (a) => { rl.close(); resolve(a.trim().toLowerCase()) })
|
|
1307
|
+
})
|
|
1308
|
+
let answer = await ask('ancleto: Detecté un proyecto OpenSpec. ¿Importar a aspec/? [s/N/ver] (default: No): ')
|
|
1309
|
+
if (answer === 'ver') {
|
|
1310
|
+
console.log('ancleto: se copiaran openspec/specs y openspec/changes a aspec/ (copia no destructiva; openspec/ se conserva como backup)')
|
|
1311
|
+
answer = await ask('ancleto: ¿Importar a aspec/? [s/N] (default: No): ')
|
|
1312
|
+
}
|
|
1313
|
+
return ['s', 'si', 'sí', 'y', 'yes'].includes(answer)
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Importacion semilla de un OpenSpec externo: copia specs + changes, respeta el
|
|
1317
|
+
// AGENTS.md (nunca se pisa en silencio) y escribe el mismo marcador de
|
|
1318
|
+
// idempotencia. openspec/ siempre se conserva como backup.
|
|
1319
|
+
async function importExternalOpenspec(projectDir) {
|
|
1320
|
+
const aspecDir = join(projectDir, CHANGES_ROOT)
|
|
1321
|
+
const marker = join(aspecDir, MIGRATION_MARKER)
|
|
1322
|
+
if (await exists(marker)) return // ya importada: no-op silencioso
|
|
1323
|
+
if (await hasRealContent(aspecDir)) {
|
|
1324
|
+
console.warn('ancleto: aspec/ ya tiene contenido real — no se importo openspec/ (revisar manualmente; openspec/ se conserva)')
|
|
1325
|
+
return
|
|
1326
|
+
}
|
|
1327
|
+
await cp(join(projectDir, LEGACY_OPENSPEC_DIR), aspecDir, { recursive: true, force: false })
|
|
1328
|
+
const bundledAgents = join(projectDir, LEGACY_OPENSPEC_DIR, 'AGENTS.md')
|
|
1329
|
+
if (await exists(bundledAgents)) {
|
|
1330
|
+
const target = join(projectDir, 'AGENTS.md')
|
|
1331
|
+
if (!(await exists(target))) {
|
|
1332
|
+
await cp(bundledAgents, target)
|
|
1333
|
+
console.log('ancleto: AGENTS.md de OpenSpec adoptado como base (revisarlo)')
|
|
1334
|
+
} else {
|
|
1335
|
+
console.warn('ancleto: se conserva el AGENTS.md local; el de OpenSpec queda en openspec/ como backup')
|
|
1336
|
+
}
|
|
1337
|
+
} else {
|
|
1338
|
+
try {
|
|
1339
|
+
if (/openspec/i.test(await readFile(join(projectDir, 'AGENTS.md'), 'utf8'))) {
|
|
1340
|
+
console.log('ancleto: se respeta el AGENTS.md de OpenSpec local (bloques ancleto fusionados por LOCKED)')
|
|
1341
|
+
}
|
|
1342
|
+
} catch {
|
|
1343
|
+
// sin AGENTS.md local: nada que respetar
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
await writeFile(marker, '# Generado por ancleto: esta aspec/ ya fue importada desde openspec/\n')
|
|
1347
|
+
console.log('ancleto: contenido importado de openspec/ a aspec/ (openspec/ se conserva como backup)')
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Punto unico de deteccion/migracion: lo invocan init, install --project y
|
|
1351
|
+
// upgrade antes del scaffold. En no-TTY rige default No salvo --yes.
|
|
1352
|
+
async function maybeImportOpenspec(projectDir, args = []) {
|
|
1353
|
+
const kind = await detectLegacyOpenSpec(projectDir)
|
|
1354
|
+
if (kind === 'none') return
|
|
1355
|
+
if (kind === 'legacy') {
|
|
1356
|
+
await migrateLegacyOpenspec(projectDir)
|
|
1357
|
+
return
|
|
1358
|
+
}
|
|
1359
|
+
if (args.includes('--yes') || await askOpenspecImport()) {
|
|
1360
|
+
await importExternalOpenspec(projectDir)
|
|
1361
|
+
} else {
|
|
1362
|
+
console.log('ancleto: importacion de openspec/ omitida (openspec/ se conserva)')
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
929
1366
|
function extractLockedBlocks(content) {
|
|
930
1367
|
const blocks = new Map()
|
|
931
1368
|
const re = /<!--\s*LOCKED:\s*([\w-]+)\s*-->([\s\S]*?)<!--\s*\/LOCKED:\s*\1\s*-->/g
|
|
@@ -983,10 +1420,11 @@ function findInsertAnchor(local, source, name) {
|
|
|
983
1420
|
|
|
984
1421
|
// Solo mergea los templates raiz (AGENTS.md/PRODUCT.md). Los assets por host
|
|
985
1422
|
// los materializa installAgentAssets (dueno unico).
|
|
986
|
-
async function copyTemplates(projectDir) {
|
|
1423
|
+
async function copyTemplates(projectDir, profile = DEFAULT_PROFILE) {
|
|
987
1424
|
for (const t of TEMPLATES) {
|
|
988
1425
|
const target = join(projectDir, t)
|
|
989
|
-
const
|
|
1426
|
+
const profileTemplate = join(profileAssetsRoot(profile), 'templates', t)
|
|
1427
|
+
const source = await readFile(await exists(profileTemplate) ? profileTemplate : join(ROOT, 'templates', t), 'utf8')
|
|
990
1428
|
if (!(await exists(target))) {
|
|
991
1429
|
await writeFile(target, source)
|
|
992
1430
|
continue
|
|
@@ -1031,6 +1469,7 @@ async function install(args) {
|
|
|
1031
1469
|
|
|
1032
1470
|
let agent = agentFlag || (existingRc?.agent && SUPPORTED_AGENTS.includes(existingRc.agent) ? existingRc.agent : null)
|
|
1033
1471
|
let language = scanLangFlag(args) || (existingRc?.language && SUPPORTED_LANGS.includes(existingRc.language) ? existingRc.language : null)
|
|
1472
|
+
const profile = scanProfileFlag(args) || validProfile(existingRc?.profile) || DEFAULT_PROFILE
|
|
1034
1473
|
const excludeFlag = scanExcludeFlag(args)
|
|
1035
1474
|
const existingDiscovery = existingRc?.discovery || { outputDir: 'docs/technical-discovery', exclude: [] }
|
|
1036
1475
|
let excludeChoice = excludeFlag
|
|
@@ -1078,14 +1517,17 @@ async function install(args) {
|
|
|
1078
1517
|
}
|
|
1079
1518
|
|
|
1080
1519
|
if (projectDir) {
|
|
1081
|
-
await copyTemplates(projectDir)
|
|
1082
|
-
await
|
|
1520
|
+
await copyTemplates(projectDir, profile)
|
|
1521
|
+
await maybeImportOpenspec(projectDir, args)
|
|
1083
1522
|
await scaffoldAspec(projectDir)
|
|
1523
|
+
if (profile === 'test') await scaffoldTestspec(projectDir)
|
|
1084
1524
|
await refreshWorkingContext(projectDir)
|
|
1085
1525
|
const written = await installAgentAssets(projectDir, agent)
|
|
1526
|
+
if (profile === 'test') await installProfileOverlay(projectDir, agent)
|
|
1086
1527
|
await writeManifest(projectDir, {
|
|
1087
1528
|
agent,
|
|
1088
1529
|
language,
|
|
1530
|
+
profile,
|
|
1089
1531
|
...(excludeChoice !== null ? { discovery: { ...existingDiscovery, exclude: excludeChoice } } : {}),
|
|
1090
1532
|
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
1091
1533
|
installedPaths: unionInstalledPaths(existingRc?.installedPaths, {
|
|
@@ -1095,6 +1537,9 @@ async function install(args) {
|
|
|
1095
1537
|
})
|
|
1096
1538
|
} else {
|
|
1097
1539
|
await copyAssets(target)
|
|
1540
|
+
if (agent === 'copilot') {
|
|
1541
|
+
console.log('ancleto: los prompts de Copilot son por repo; corré install --project para generarlos')
|
|
1542
|
+
}
|
|
1098
1543
|
}
|
|
1099
1544
|
|
|
1100
1545
|
const models = tierModels(tier, TIERS, gratisModelChoice)
|
|
@@ -1115,7 +1560,11 @@ async function install(args) {
|
|
|
1115
1560
|
}
|
|
1116
1561
|
}
|
|
1117
1562
|
|
|
1118
|
-
const res = await
|
|
1563
|
+
const res = await setupHostMcp(target, mcpMap, { projectDir, agent })
|
|
1564
|
+
if (projectDir && agent === 'copilot') {
|
|
1565
|
+
const cres = await mergeCopilotMcp(projectDir, mcpMap)
|
|
1566
|
+
if (cres.added.length) console.log(`ancleto: MCP configurados: ${cres.added.join(', ')} en ${cres.file}`)
|
|
1567
|
+
}
|
|
1119
1568
|
const loc = projectDir
|
|
1120
1569
|
? `${projectDir} (layout ${agent} + templates en la raiz)`
|
|
1121
1570
|
: `${target} (disponible en todos tus proyectos)`
|
|
@@ -1132,16 +1581,26 @@ async function upgradeCmd(args) {
|
|
|
1132
1581
|
const projectDir = process.cwd()
|
|
1133
1582
|
const rc = await readAncletorc(projectDir)
|
|
1134
1583
|
if (!rc) {
|
|
1584
|
+
if ((await detectLegacyOpenSpec(projectDir)) !== 'none') {
|
|
1585
|
+
await maybeImportOpenspec(projectDir, args)
|
|
1586
|
+
await scaffoldAspec(projectDir)
|
|
1587
|
+
console.log("ancleto: openspec/ migrado. Corré 'ancleto init' para completar la configuración.")
|
|
1588
|
+
return
|
|
1589
|
+
}
|
|
1135
1590
|
console.error("Error: No se encontro .ancletorc. Ejecuta 'ancleto init' primero.")
|
|
1136
1591
|
process.exit(1)
|
|
1137
1592
|
}
|
|
1138
|
-
await
|
|
1593
|
+
await maybeImportOpenspec(projectDir, args)
|
|
1139
1594
|
const agent = await resolveAgent(args, rc.agent)
|
|
1140
|
-
|
|
1595
|
+
const profile = scanProfileFlag(args) || validProfile(rc.profile) || DEFAULT_PROFILE
|
|
1596
|
+
await copyTemplates(projectDir, profile)
|
|
1597
|
+
if (profile === 'test') await scaffoldTestspec(projectDir)
|
|
1141
1598
|
const written = await installAgentAssets(projectDir, agent)
|
|
1599
|
+
if (profile === 'test') await installProfileOverlay(projectDir, agent)
|
|
1142
1600
|
const wentContext = await refreshWorkingContext(projectDir)
|
|
1143
1601
|
const manifest = await writeManifest(projectDir, {
|
|
1144
1602
|
agent,
|
|
1603
|
+
profile,
|
|
1145
1604
|
installedPaths: unionInstalledPaths(rc.installedPaths, {
|
|
1146
1605
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
1147
1606
|
...written
|
|
@@ -1150,7 +1609,15 @@ async function upgradeCmd(args) {
|
|
|
1150
1609
|
const skillsDir = written.skills[0] || '.opencode/skills'
|
|
1151
1610
|
console.log(`ancleto: upgrade completo (v${manifest.version}, agente: ${agent})`)
|
|
1152
1611
|
console.log(' ✔ templates re-aplicados (bloques LOCKED actualizados, EXTENSIBLE intacto)')
|
|
1153
|
-
|
|
1612
|
+
if (agent === 'copilot') {
|
|
1613
|
+
const { file, fixed } = await refreshCopilotMcp(projectDir)
|
|
1614
|
+
console.log(` ✔ prompts regenerados en .github/prompts (copilot-instructions.md intacto)`)
|
|
1615
|
+
console.log(fixed.length
|
|
1616
|
+
? ` ✔ MCP regenerados en ${file}: ${fixed.join(', ')}`
|
|
1617
|
+
: ` ✔ MCP sanos en ${file}, sin cambios`)
|
|
1618
|
+
} else {
|
|
1619
|
+
console.log(` ✔ skills actualizadas en ${skillsDir} (${ANCLETO_SKILLS.length} skills)`)
|
|
1620
|
+
}
|
|
1154
1621
|
console.log(wentContext ? ' ✔ working-context.md regenerado' : ' ✔ working-context.md sin reglas activas (vacio)')
|
|
1155
1622
|
console.log(' ✔ manifiesto .ancletorc actualizado')
|
|
1156
1623
|
}
|
|
@@ -1158,9 +1625,11 @@ async function upgradeCmd(args) {
|
|
|
1158
1625
|
async function initProject(args) {
|
|
1159
1626
|
const projectDir = process.cwd()
|
|
1160
1627
|
const withAzure = args.includes('--with-azure')
|
|
1628
|
+
const withMcp = !args.includes('--no-mcp')
|
|
1161
1629
|
const agentFlag = scanAgentFlag(args)
|
|
1162
1630
|
const tierFlag = scanTierFlag(args)
|
|
1163
1631
|
const langFlag = scanLangFlag(args)
|
|
1632
|
+
const profileFlag = scanProfileFlag(args)
|
|
1164
1633
|
const existing = await readAncletorc(projectDir)
|
|
1165
1634
|
const azure = existing?.azure ?? { enabled: false }
|
|
1166
1635
|
let discovery = existing?.discovery ?? { outputDir: 'docs/technical-discovery', exclude: [] }
|
|
@@ -1170,6 +1639,7 @@ async function initProject(args) {
|
|
|
1170
1639
|
|
|
1171
1640
|
let agent, tier, language
|
|
1172
1641
|
language = langFlag || (existing?.language && SUPPORTED_LANGS.includes(existing.language) ? existing.language : null)
|
|
1642
|
+
const profile = profileFlag || validProfile(existing?.profile) || DEFAULT_PROFILE
|
|
1173
1643
|
const isInteractive = Boolean(process.stdout.isTTY) && (!agentFlag || !tierFlag || !language || (excludeChoice === null && (discovery.exclude || []).length === 0))
|
|
1174
1644
|
if (isInteractive) {
|
|
1175
1645
|
await showBanner()
|
|
@@ -1218,26 +1688,34 @@ async function initProject(args) {
|
|
|
1218
1688
|
gratisModelChoice = persisted || gratisModel()
|
|
1219
1689
|
}
|
|
1220
1690
|
const written = await installAgentAssets(projectDir, agent)
|
|
1691
|
+
if (profile === 'test') await installProfileOverlay(projectDir, agent)
|
|
1221
1692
|
const manifest = await writeManifest(projectDir, {
|
|
1222
1693
|
azure,
|
|
1223
1694
|
discovery,
|
|
1224
1695
|
agent,
|
|
1225
1696
|
language,
|
|
1697
|
+
profile,
|
|
1226
1698
|
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
1227
1699
|
installedPaths: unionInstalledPaths(existing?.installedPaths, {
|
|
1228
1700
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
1229
1701
|
...written
|
|
1230
1702
|
})
|
|
1231
1703
|
})
|
|
1232
|
-
await
|
|
1704
|
+
await maybeImportOpenspec(projectDir, args)
|
|
1705
|
+
await copyTemplates(projectDir, profile)
|
|
1706
|
+
if (profile === 'test') await scaffoldTestspec(projectDir)
|
|
1233
1707
|
if (tier) {
|
|
1234
1708
|
await applyTier(join(projectDir, '.opencode', 'agents'), tier, tierModels(tier, TIERS, gratisModelChoice))
|
|
1235
1709
|
}
|
|
1236
|
-
await migrateLegacyOpenspec(projectDir)
|
|
1237
1710
|
await scaffoldAspec(projectDir)
|
|
1238
1711
|
await refreshWorkingContext(projectDir)
|
|
1239
1712
|
await registerProject(projectDir, { agent, tier: tier || null })
|
|
1713
|
+
// D7: init configura el MCP del host igual que install (mismo dueno, sin rama
|
|
1714
|
+
// especial por host); --no-mcp conserva el escape.
|
|
1715
|
+
const mcpMap = withMcp ? buildDefaultMcp({}) : {}
|
|
1716
|
+
const mcpRes = await setupHostMcp(join(projectDir, '.opencode'), mcpMap, { projectDir, agent })
|
|
1240
1717
|
if (azure.enabled) console.log(AZURE_MCP_NOTICE)
|
|
1718
|
+
if (mcpRes.added.length) console.log(`ancleto: MCP configurados: ${mcpRes.added.join(', ')} en ${mcpRes.file}`)
|
|
1241
1719
|
console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'} (Agente: ${agent})${tier ? ` (Tier: ${tier})` : ''} (Idioma: ${language})`)
|
|
1242
1720
|
}
|
|
1243
1721
|
|
|
@@ -1693,6 +2171,8 @@ async function checkCommand() {
|
|
|
1693
2171
|
process.exit(1)
|
|
1694
2172
|
}
|
|
1695
2173
|
const ip = rc.installedPaths
|
|
2174
|
+
// D9: la union de hosts instalados se lee del manifiesto, no de un unico `rc.agent`.
|
|
2175
|
+
const installedHosts = installedHostsFromPaths(ip)
|
|
1696
2176
|
let missing = 0
|
|
1697
2177
|
let orphans = 0
|
|
1698
2178
|
|
|
@@ -1716,7 +2196,18 @@ async function checkCommand() {
|
|
|
1716
2196
|
// La convencion de nombre del host (p. ej. vscode: .agent.md/.prompt.md)
|
|
1717
2197
|
// se deriva del mismo modelo de destino; en skills no cambia el nombre.
|
|
1718
2198
|
const ext = installedExtFor(cat, dirRel)
|
|
1719
|
-
|
|
2199
|
+
let expected = (await readdir(join(ROOT, cat))).sort().map((f) => (ext ? f.replace(/\.md$/, ext) : f))
|
|
2200
|
+
// D9: los command-skills de Antigravity viven dentro de `.agents/skills`; el
|
|
2201
|
+
// conjunto esperado de ese dir suma el catalogo de skills y los commands de
|
|
2202
|
+
// cualquier host instalado que empaquete commands ahi (union, no rc.agent).
|
|
2203
|
+
const packagesCommandsHere = cat === 'skills' && installedHosts.some((h) => {
|
|
2204
|
+
const cs = AGENT_TARGETS[h].commands
|
|
2205
|
+
return cs?.package === 'skill-dir' && cs.dir === dirRel
|
|
2206
|
+
})
|
|
2207
|
+
if (packagesCommandsHere) {
|
|
2208
|
+
const cmds = (await readdir(join(ROOT, 'commands'))).filter((f) => f.endsWith(MD_EXT)).map((f) => f.slice(0, -MD_EXT.length))
|
|
2209
|
+
expected = [...new Set([...expected, ...cmds])].sort()
|
|
2210
|
+
}
|
|
1720
2211
|
const actual = (await readdir(destDir)).sort()
|
|
1721
2212
|
const missingFiles = expected.filter((f) => !actual.includes(f))
|
|
1722
2213
|
const orphanFiles = actual.filter((f) => !expected.includes(f))
|
|
@@ -1749,10 +2240,13 @@ async function checkTierOrphan(cwd) {
|
|
|
1749
2240
|
if (!(await exists(tierFile))) return warnings
|
|
1750
2241
|
const tier = (await readFile(tierFile, 'utf8')).trim()
|
|
1751
2242
|
if (!TIERS[tier]) return warnings
|
|
1752
|
-
|
|
2243
|
+
// D9: se cuentan los agents locales del host realmente instalados, no
|
|
2244
|
+
// `.opencode/agents` fijo (un proyecto antigravity usa `.agents/agents`).
|
|
2245
|
+
const rc = await readAncletorc(cwd)
|
|
1753
2246
|
let count = 0
|
|
1754
|
-
|
|
1755
|
-
|
|
2247
|
+
for (const d of localAgentDirs(rc)) {
|
|
2248
|
+
const p = join(cwd, d)
|
|
2249
|
+
if (await exists(p)) count += (await readdir(p)).filter((f) => f.endsWith(MD_EXT)).length
|
|
1756
2250
|
}
|
|
1757
2251
|
if (count === 0) {
|
|
1758
2252
|
warnings.push(`tier "${tier}" sin agentes locales donde aplicarlo — este proyecto usa los agentes globales. Corre 'ancleto install --project .' para que el tier aplique, o borra .opencode/.ancleto-tier.`)
|
|
@@ -2065,6 +2559,205 @@ async function statsCommand(args) {
|
|
|
2065
2559
|
console.log(` totales (${shownLabel}): entrada ${fmtTokens(totals.input)} · salida ${fmtTokens(totals.output)} · razon ${fmtTokens(totals.reasoning)} · cache ${fmtTokens(totals.cacheRead)} · costo ${fmtCost(totals.cost)}`)
|
|
2066
2560
|
}
|
|
2067
2561
|
|
|
2562
|
+
const EXPORT_FORMAT = 'ancleto-export/1'
|
|
2563
|
+
|
|
2564
|
+
async function readProjectMcp(projectDir) {
|
|
2565
|
+
for (const c of ['opencode.json', 'opencode.jsonc']) {
|
|
2566
|
+
const p = join(projectDir, '.opencode', c)
|
|
2567
|
+
if (await exists(p)) {
|
|
2568
|
+
try {
|
|
2569
|
+
return { file: p, cfg: JSON.parse((await readFile(p, 'utf8')).replace(/^\uFEFF/, '')) }
|
|
2570
|
+
} catch {
|
|
2571
|
+
return { file: p, cfg: {} }
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return { file: join(projectDir, '.opencode', 'opencode.json'), cfg: {} }
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// Repara entradas rotas y agrega ausentes (solo las de intendedNames cuando se
|
|
2579
|
+
// indica; con null solo repara las existentes). Nunca toca las sanas.
|
|
2580
|
+
async function repairProjectMcp(projectDir, fresh, intendedNames = null) {
|
|
2581
|
+
const { file, cfg } = await readProjectMcp(projectDir)
|
|
2582
|
+
cfg.mcp = cfg.mcp || {}
|
|
2583
|
+
const fixed = []
|
|
2584
|
+
for (const [name, def] of Object.entries(cfg.mcp)) {
|
|
2585
|
+
if (mcpCommandBroken(def) && fresh[name]) {
|
|
2586
|
+
cfg.mcp[name] = fresh[name]
|
|
2587
|
+
fixed.push(name)
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
if (intendedNames) {
|
|
2591
|
+
for (const name of intendedNames) {
|
|
2592
|
+
if (!cfg.mcp[name] && fresh[name]) {
|
|
2593
|
+
cfg.mcp[name] = fresh[name]
|
|
2594
|
+
fixed.push(`${name} (agregado)`)
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
await mkdir(dirname(file), { recursive: true })
|
|
2599
|
+
await writeFile(file, JSON.stringify(cfg, null, 2) + '\n')
|
|
2600
|
+
return { file, fixed }
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
async function exportCmd(args) {
|
|
2604
|
+
const projectDir = process.cwd()
|
|
2605
|
+
const rc = await readAncletorc(projectDir)
|
|
2606
|
+
if (!rc) {
|
|
2607
|
+
console.error("Error: No se encontro .ancletorc. Ejecuta 'ancleto init' primero.")
|
|
2608
|
+
process.exit(1)
|
|
2609
|
+
}
|
|
2610
|
+
const { cfg } = await readProjectMcp(projectDir)
|
|
2611
|
+
const mcpIntent = Object.entries(cfg.mcp || {})
|
|
2612
|
+
.map(([name, def]) => ({ name, type: def?.type || 'local', enabled: def?.enabled !== false }))
|
|
2613
|
+
const tierFile = join(projectDir, '.opencode', '.ancleto-tier')
|
|
2614
|
+
const tier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
2615
|
+
const manifest = {
|
|
2616
|
+
format: EXPORT_FORMAT,
|
|
2617
|
+
ancletoVersion: await packageVersion(),
|
|
2618
|
+
exportedAt: new Date().toISOString(),
|
|
2619
|
+
agent: rc.agent || null,
|
|
2620
|
+
tier,
|
|
2621
|
+
language: rc.language || 'auto',
|
|
2622
|
+
profile: rc.profile || 'general',
|
|
2623
|
+
...(rc.gratisModel ? { gratisModel: rc.gratisModel } : {}),
|
|
2624
|
+
flags: { azure: Boolean(rc.azure?.enabled), engram: mcpIntent.some((m) => m.name === 'engram') },
|
|
2625
|
+
mcp: mcpIntent
|
|
2626
|
+
}
|
|
2627
|
+
const serialized = JSON.stringify(manifest)
|
|
2628
|
+
if (serialized.includes(projectDir) || /[A-Za-z]:\\/.test(serialized) || serialized.includes(homedir())) {
|
|
2629
|
+
console.error('ancleto: el manifiesto contiene rutas absolutas; export abortado')
|
|
2630
|
+
process.exit(1)
|
|
2631
|
+
}
|
|
2632
|
+
const tarIdx = args.indexOf('--tar')
|
|
2633
|
+
if (tarIdx >= 0 && !args[tarIdx + 1]) {
|
|
2634
|
+
console.error('ancleto: --tar requiere un archivo destino')
|
|
2635
|
+
process.exit(1)
|
|
2636
|
+
}
|
|
2637
|
+
const tarFile = tarIdx >= 0 ? resolve(args[tarIdx + 1]) : null
|
|
2638
|
+
const outDir = tarFile ? await mkdtemp(join(tmpdir(), 'ancleto-export-')) : join(projectDir, 'ancleto-export')
|
|
2639
|
+
await mkdir(outDir, { recursive: true })
|
|
2640
|
+
// Allowlist portable: nunca se copian opencode.json (rutas absolutas),
|
|
2641
|
+
// .ancleto-tier, memory.db, service.json ni la config global.
|
|
2642
|
+
if (await exists(join(projectDir, '.ancletorc'))) await cp(join(projectDir, '.ancletorc'), join(outDir, '.ancletorc'))
|
|
2643
|
+
for (const t of TEMPLATES) {
|
|
2644
|
+
if (await exists(join(projectDir, t))) await cp(join(projectDir, t), join(outDir, t))
|
|
2645
|
+
}
|
|
2646
|
+
if (await exists(join(projectDir, 'aspec'))) await cp(join(projectDir, 'aspec'), join(outDir, 'aspec'), { recursive: true })
|
|
2647
|
+
for (const d of ASSETS) {
|
|
2648
|
+
const src = join(projectDir, '.opencode', d)
|
|
2649
|
+
if (await exists(src)) await cp(src, join(outDir, '.opencode', d), { recursive: true })
|
|
2650
|
+
}
|
|
2651
|
+
await writeFile(join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
|
|
2652
|
+
if (tarFile) {
|
|
2653
|
+
const r = spawnSync('tar', ['czf', tarFile, '-C', outDir, '.'], { encoding: 'utf8' })
|
|
2654
|
+
if (r.status !== 0) {
|
|
2655
|
+
console.error(`ancleto: no se pudo crear el tarball (${(r.stderr || '').trim() || 'tar no disponible'})`)
|
|
2656
|
+
process.exit(1)
|
|
2657
|
+
}
|
|
2658
|
+
await rm(outDir, { recursive: true, force: true })
|
|
2659
|
+
console.log(`ancleto: exportado a ${tarFile}`)
|
|
2660
|
+
} else {
|
|
2661
|
+
console.log(`ancleto: exportado a ${join(projectDir, 'ancleto-export')}`)
|
|
2662
|
+
}
|
|
2663
|
+
console.log(`ancleto: manifiesto v${manifest.ancletoVersion} con ${mcpIntent.length} MCP (solo intencion, sin rutas)`)
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
async function importCmd(args) {
|
|
2667
|
+
const projectDir = process.cwd()
|
|
2668
|
+
const repairOnly = args.includes('--repair')
|
|
2669
|
+
const bundleArg = args.find((a) => !a.startsWith('-'))
|
|
2670
|
+
if (!repairOnly && !bundleArg) {
|
|
2671
|
+
console.error('ancleto: uso: ancleto import <bundle> [--repair] | ancleto import --repair')
|
|
2672
|
+
process.exit(1)
|
|
2673
|
+
}
|
|
2674
|
+
let manifest = null
|
|
2675
|
+
if (bundleArg) {
|
|
2676
|
+
const resolved = resolve(bundleArg)
|
|
2677
|
+
if (!existsSync(resolved)) {
|
|
2678
|
+
console.error(`ancleto: bundle no encontrado: ${bundleArg}`)
|
|
2679
|
+
process.exit(1)
|
|
2680
|
+
}
|
|
2681
|
+
let bundleDir = resolved
|
|
2682
|
+
if (/\.tgz$|\.tar\.gz$/.test(resolved)) {
|
|
2683
|
+
bundleDir = await mkdtemp(join(tmpdir(), 'ancleto-import-'))
|
|
2684
|
+
const r = spawnSync('tar', ['xzf', resolved, '-C', bundleDir], { encoding: 'utf8' })
|
|
2685
|
+
if (r.status !== 0) {
|
|
2686
|
+
console.error('ancleto: no se pudo extraer el bundle')
|
|
2687
|
+
process.exit(1)
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
try {
|
|
2691
|
+
manifest = JSON.parse(await readFile(join(bundleDir, 'manifest.json'), 'utf8'))
|
|
2692
|
+
} catch {
|
|
2693
|
+
console.error('ancleto: bundle invalido (sin manifest.json)')
|
|
2694
|
+
process.exit(1)
|
|
2695
|
+
}
|
|
2696
|
+
if (manifest.format !== EXPORT_FORMAT) {
|
|
2697
|
+
console.error(`ancleto: formato de bundle no soportado: ${manifest.format}`)
|
|
2698
|
+
process.exit(1)
|
|
2699
|
+
}
|
|
2700
|
+
const localVersion = await packageVersion()
|
|
2701
|
+
if (manifest.ancletoVersion && manifest.ancletoVersion !== localVersion) {
|
|
2702
|
+
console.warn(`ancleto: el bundle es de v${manifest.ancletoVersion} y el CLI es v${localVersion} (se continua igual)`)
|
|
2703
|
+
}
|
|
2704
|
+
const bundleRc = await readAncletorc(bundleDir)
|
|
2705
|
+
if (bundleRc) {
|
|
2706
|
+
await writeManifest(projectDir, {
|
|
2707
|
+
...(bundleRc.agent ? { agent: bundleRc.agent } : {}),
|
|
2708
|
+
...(bundleRc.language ? { language: bundleRc.language } : {}),
|
|
2709
|
+
...(bundleRc.profile ? { profile: bundleRc.profile } : {}),
|
|
2710
|
+
...(bundleRc.gratisModel ? { gratisModel: bundleRc.gratisModel } : {}),
|
|
2711
|
+
...(bundleRc.azure ? { azure: bundleRc.azure } : {}),
|
|
2712
|
+
...(bundleRc.discovery ? { discovery: bundleRc.discovery } : {})
|
|
2713
|
+
})
|
|
2714
|
+
}
|
|
2715
|
+
if (await exists(join(bundleDir, 'aspec'))) {
|
|
2716
|
+
await cp(join(bundleDir, 'aspec'), join(projectDir, 'aspec'), { recursive: true, force: false })
|
|
2717
|
+
}
|
|
2718
|
+
for (const t of TEMPLATES) {
|
|
2719
|
+
const src = join(bundleDir, t)
|
|
2720
|
+
if (!(await exists(src))) continue
|
|
2721
|
+
const target = join(projectDir, t)
|
|
2722
|
+
const content = await readFile(src, 'utf8')
|
|
2723
|
+
if (!(await exists(target))) {
|
|
2724
|
+
await writeFile(target, content)
|
|
2725
|
+
continue
|
|
2726
|
+
}
|
|
2727
|
+
const current = await readFile(target, 'utf8')
|
|
2728
|
+
const merged = mergeLocked(content, current, t)
|
|
2729
|
+
if (merged !== current) await writeFile(target, merged)
|
|
2730
|
+
}
|
|
2731
|
+
for (const d of ASSETS) {
|
|
2732
|
+
const src = join(bundleDir, '.opencode', d)
|
|
2733
|
+
if (await exists(src)) await cp(src, join(projectDir, '.opencode', d), { recursive: true, force: false })
|
|
2734
|
+
}
|
|
2735
|
+
const rcNow = await readAncletorc(projectDir)
|
|
2736
|
+
const agent = (rcNow?.agent && SUPPORTED_AGENTS.includes(rcNow.agent)) ? rcNow.agent : DEFAULT_AGENT
|
|
2737
|
+
const importProfile = validProfile(rcNow?.profile) || DEFAULT_PROFILE
|
|
2738
|
+
await installAgentAssets(projectDir, agent)
|
|
2739
|
+
if (importProfile === 'test') await installProfileOverlay(projectDir, agent)
|
|
2740
|
+
if (manifest.tier && TIERS[manifest.tier]) {
|
|
2741
|
+
await applyTier(join(projectDir, '.opencode', 'agents'), manifest.tier, tierModels(manifest.tier, TIERS, manifest.gratisModel))
|
|
2742
|
+
}
|
|
2743
|
+
await scaffoldAspec(projectDir)
|
|
2744
|
+
console.log(`ancleto: bundle aplicado (agente: ${agent}${manifest.tier ? `, tier: ${manifest.tier}` : ''})`)
|
|
2745
|
+
}
|
|
2746
|
+
const withEngram = manifest
|
|
2747
|
+
? manifest.flags?.engram === true
|
|
2748
|
+
: Boolean((await readProjectMcp(projectDir)).cfg.mcp?.engram)
|
|
2749
|
+
const fresh = buildDefaultMcp({ withEngram })
|
|
2750
|
+
if (manifest?.flags?.azure) {
|
|
2751
|
+
fresh['azure-devops'] = { type: 'local', enabled: true, command: ['npx', '-y', '@davstack/mcp-azure-devops'] }
|
|
2752
|
+
}
|
|
2753
|
+
const intended = manifest ? manifest.mcp.map((m) => m.name) : null
|
|
2754
|
+
const { file, fixed } = await repairProjectMcp(projectDir, fresh, intended)
|
|
2755
|
+
console.log(fixed.length
|
|
2756
|
+
? `ancleto: MCP regenerados en ${file}: ${fixed.join(', ')}`
|
|
2757
|
+
: `ancleto: MCP sanos en ${file}, sin cambios`)
|
|
2758
|
+
await doctorCommand()
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2068
2761
|
async function doctorCommand() {
|
|
2069
2762
|
let fatal = false
|
|
2070
2763
|
|
|
@@ -2150,6 +2843,12 @@ switch (cmd) {
|
|
|
2150
2843
|
case 'doctor':
|
|
2151
2844
|
await doctorCommand()
|
|
2152
2845
|
break
|
|
2846
|
+
case 'export':
|
|
2847
|
+
await exportCmd(rest)
|
|
2848
|
+
break
|
|
2849
|
+
case 'import':
|
|
2850
|
+
await importCmd(rest)
|
|
2851
|
+
break
|
|
2153
2852
|
case 'mcp':
|
|
2154
2853
|
await serveMemoryMcp()
|
|
2155
2854
|
break
|