@ancleto/spec 0.7.0 → 0.7.2
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 +35 -1
- package/package.json +1 -1
- package/src/cli/index.js +166 -40
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
- [Características principales](#características-principales)
|
|
24
24
|
- [Requisitos](#requisitos)
|
|
25
25
|
- [Instalación](#instalación)
|
|
26
|
+
- [Hosts soportados y layouts por agente](#hosts-soportados-y-layouts-por-agente)
|
|
26
27
|
- [Guía de .gitignore](#guía-de-gitignore)
|
|
27
28
|
- [Configuración y tiers de costo](#configuración-y-tiers-de-costo)
|
|
28
29
|
- [Uso rápido](#uso-rápido)
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
|
|
40
41
|
**Ancleto** (alias de CLI: `aspec`) es un orquestador ligero para desarrollo de software asistido por IA bajo el paradigma **SDD (Spec-Driven Development)**.
|
|
41
42
|
|
|
42
|
-
Vive dentro de tu entorno de desarrollo (OpenCode, Cursor, VS Code, Roo, etc.) y automatiza tres cosas que normalmente hacés a mano:
|
|
43
|
+
Vive dentro de tu entorno de desarrollo (OpenCode, Claude, Cursor, VS Code, Roo, etc.) y automatiza tres cosas que normalmente hacés a mano:
|
|
43
44
|
|
|
44
45
|
- **Especificar** — escritura guiada de especificaciones y planes de implementación.
|
|
45
46
|
- **Descubrir** — relevamiento topológico del repositorio y empaquetado de contexto.
|
|
@@ -125,6 +126,39 @@ Para medir el consumo **real** por sesión (no estimado): `ancleto stats` lee la
|
|
|
125
126
|
|
|
126
127
|
---
|
|
127
128
|
|
|
129
|
+
## Hosts soportados y layouts por agente
|
|
130
|
+
|
|
131
|
+
`--agent <nombre>` selecciona **el layout nativo a materializar** (el directorio y la convención de
|
|
132
|
+
nombre de cada asset) y el **pipeline de adaptación de frontmatter** de ese host. No arbitra qué
|
|
133
|
+
descubre cada host: si dos hosts leen el mismo directorio, el descubrimiento compartido es un efecto
|
|
134
|
+
del host (el CLI no deduplica ni elige un "ganador").
|
|
135
|
+
|
|
136
|
+
| Host (`--agent`) | skills | agents | commands |
|
|
137
|
+
|---|---|---|---|
|
|
138
|
+
| `opencode` (default) | `.opencode/skills/<n>/SKILL.md` | `.opencode/agents/<n>.md` | `.opencode/commands/<n>.md` |
|
|
139
|
+
| `claude` | `.claude/skills/<n>/SKILL.md` | `.claude/agents/<n>.md` (adaptado) | `.claude/commands/<n>.md` |
|
|
140
|
+
| `vscode` (VS Code / Copilot) | `.github/skills/<n>/SKILL.md` | `.github/agents/<n>.agent.md` | `.github/prompts/<n>.prompt.md` |
|
|
141
|
+
| `antigravity` | `.agents/skills/<n>/SKILL.md` | — (no soportado) | — (no soportado) |
|
|
142
|
+
| `cursor` | `.cursor/skills/<n>/SKILL.md` | — | — |
|
|
143
|
+
| `roo` | `.roo/skills/<n>/SKILL.md` | — | — |
|
|
144
|
+
|
|
145
|
+
Cada host recibe sus skills en **su** directorio nativo. **`.agents/skills` es un punto de lectura
|
|
146
|
+
compartido** —lo leen opencode, Cursor, VS Code y Antigravity—, no un destino universal: Claude lee
|
|
147
|
+
`.claude/skills` y Roo, `.roo/skills`. Por eso el CLI no escribe todas las skills en `.agents/skills`.
|
|
148
|
+
|
|
149
|
+
Los assets que un host no soporta (por ejemplo `agents`/`commands` de `antigravity`, `cursor` y `roo`)
|
|
150
|
+
**no se escriben** y el instalador avisa por `stderr`
|
|
151
|
+
(`skip agents: not supported by host 'antigravity'`) sin abortar. La instalación es **aditiva y no
|
|
152
|
+
destructiva**: instalar con otro agente escribe en su base y preserva los layouts previos, y el
|
|
153
|
+
manifiesto `.ancletorc.installedPaths` acumula la unión de los destinos realmente escritos.
|
|
154
|
+
|
|
155
|
+
La **configuración MCP específica de host** está fuera de alcance: el CLI no genera ni transforma
|
|
156
|
+
`.mcp.json`, `mcp_config.json` ni `.vscode/mcp.json`, y no pisa config MCP preexistente. `claude` se
|
|
157
|
+
soporta con adaptación de frontmatter obligatoria para `agents` (se omiten `model`/`tools` sin
|
|
158
|
+
equivalencia verificada y las claves `mode`/`color`/`temperature`/`permission`), sin MCP propio.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
128
162
|
## Guía de .gitignore
|
|
129
163
|
|
|
130
164
|
El framework **no gestiona tu `.gitignore`**: es una decisión del proyecto. Como referencia, esta es la combinación que funciona bien con Ancleto:
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -35,7 +35,11 @@ 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 | vscode | antigravity | cursor | roo (wizard si no esta guardado)
|
|
38
|
+
ancleto install --agent <nombre> opencode | claude | vscode | antigravity | cursor | roo (wizard si no esta guardado)
|
|
39
|
+
Elige el layout nativo a materializar (directorio + convencion de
|
|
40
|
+
nombre) y el adapter de frontmatter del host. NO arbitra el
|
|
41
|
+
descubrimiento entre hosts: si dos leen el mismo directorio, eso es
|
|
42
|
+
efecto del host (no se deduplica ni se elige "ganador").
|
|
39
43
|
ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
|
|
40
44
|
Parado en un proyecto con .ancletorc opera sobre ese proyecto;
|
|
41
45
|
usa --global para forzar el alcance global
|
|
@@ -591,13 +595,13 @@ async function applyTier(agentsDir, tier, models = null) {
|
|
|
591
595
|
}
|
|
592
596
|
}
|
|
593
597
|
|
|
594
|
-
const SUPPORTED_AGENTS = ['opencode', 'vscode', 'antigravity', 'cursor', 'roo']
|
|
598
|
+
const SUPPORTED_AGENTS = ['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo']
|
|
595
599
|
const DEFAULT_AGENT = 'opencode'
|
|
596
600
|
|
|
597
601
|
function askAgent() {
|
|
598
602
|
return new Promise((resolve) => {
|
|
599
603
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
600
|
-
rl.question(
|
|
604
|
+
rl.question(`Agente/IDE [${SUPPORTED_AGENTS.join('/')}] (default: ${DEFAULT_AGENT}): `, (a) => {
|
|
601
605
|
rl.close()
|
|
602
606
|
const t = a.trim().toLowerCase()
|
|
603
607
|
resolve(SUPPORTED_AGENTS.includes(t) ? t : DEFAULT_AGENT)
|
|
@@ -732,32 +736,145 @@ async function copyAssets(dest) {
|
|
|
732
736
|
}
|
|
733
737
|
}
|
|
734
738
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
739
|
+
// D1: destino nativo por host. Cada asset declara su directorio y su convencion
|
|
740
|
+
// de nombre (ext); null = asset no soportado por ese host. Fuente unica de rutas.
|
|
741
|
+
const MD_EXT = '.md'
|
|
742
|
+
const AGENT_TARGETS = {
|
|
743
|
+
opencode: {
|
|
744
|
+
skills: { dir: '.opencode/skills' },
|
|
745
|
+
agents: { dir: '.opencode/agents', ext: MD_EXT },
|
|
746
|
+
commands: { dir: '.opencode/commands', ext: MD_EXT }
|
|
747
|
+
},
|
|
748
|
+
claude: {
|
|
749
|
+
skills: { dir: '.claude/skills' },
|
|
750
|
+
agents: { dir: '.claude/agents', ext: MD_EXT },
|
|
751
|
+
commands: { dir: '.claude/commands', ext: MD_EXT }
|
|
752
|
+
},
|
|
753
|
+
vscode: {
|
|
754
|
+
skills: { dir: '.github/skills' },
|
|
755
|
+
agents: { dir: '.github/agents', ext: '.agent.md' },
|
|
756
|
+
commands: { dir: '.github/prompts', ext: '.prompt.md' }
|
|
757
|
+
},
|
|
758
|
+
// Antigravity, Cursor y Roo: solo skills (D3/D7). .agents/skills es punto de
|
|
759
|
+
// lectura compartido (D2), no destino universal.
|
|
760
|
+
antigravity: { skills: { dir: '.agents/skills' }, agents: null, commands: null },
|
|
761
|
+
cursor: { skills: { dir: '.cursor/skills' }, agents: null, commands: null },
|
|
762
|
+
roo: { skills: { dir: '.roo/skills' }, agents: null, commands: null }
|
|
741
763
|
}
|
|
742
764
|
|
|
743
765
|
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']
|
|
744
766
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
767
|
+
// Frontmatter: parser propio sin dependencias. Cada entrada conserva su clave y
|
|
768
|
+
// sus lineas crudas (incluye block scalars y mapas anidados), preservando orden.
|
|
769
|
+
function parseFrontmatter(content) {
|
|
770
|
+
const m = /^(---\r?\n)([\s\S]*?)(\r?\n---)(\r?\n?)([\s\S]*)$/.exec(content)
|
|
771
|
+
if (!m) return { hasFrontmatter: false, content }
|
|
772
|
+
const entries = []
|
|
773
|
+
for (const line of m[2].split(/\r?\n/)) {
|
|
774
|
+
if (/^[A-Za-z0-9_-]+:/.test(line)) {
|
|
775
|
+
entries.push({ key: line.slice(0, line.indexOf(':')), lines: [line] })
|
|
776
|
+
} else if (entries.length) {
|
|
777
|
+
entries[entries.length - 1].lines.push(line)
|
|
778
|
+
} else {
|
|
779
|
+
entries.push({ key: null, lines: [line] })
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return { hasFrontmatter: true, open: m[1], entries, close: m[3], nl: m[4], body: m[5] }
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function serializeFrontmatter(parsed) {
|
|
786
|
+
if (!parsed.hasFrontmatter) return parsed.content
|
|
787
|
+
const inner = parsed.entries.flatMap((e) => e.lines).join('\n')
|
|
788
|
+
return `${parsed.open}${inner}${parsed.close}${parsed.nl}${parsed.body}`
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// D4/D5: hosts cuyo formato de agents difiere del origen requieren adaptacion
|
|
792
|
+
// (hoy Claude y VS Code). El resto es identidad (preservacion verbatim). Nunca se
|
|
793
|
+
// inventan equivalencias de model/tools.
|
|
794
|
+
const AGENT_ADAPTER_HOSTS = new Set(['claude', 'vscode'])
|
|
795
|
+
const AGENT_ADAPTER_DROP = new Set(['mode', 'color', 'temperature', 'permission', 'model', 'tools'])
|
|
796
|
+
|
|
797
|
+
function adaptFrontmatter(content, host, assetKind) {
|
|
798
|
+
if (assetKind !== 'agents' || !AGENT_ADAPTER_HOSTS.has(host)) return content
|
|
799
|
+
const parsed = parseFrontmatter(content)
|
|
800
|
+
if (!parsed.hasFrontmatter) return content
|
|
801
|
+
parsed.entries = parsed.entries.filter((e) => !(e.key && AGENT_ADAPTER_DROP.has(e.key)))
|
|
802
|
+
return serializeFrontmatter(parsed)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// D12: asset no soportado -> no se escribe y se avisa a stderr (no bloqueante).
|
|
806
|
+
function warnUnsupportedAsset(asset, agent) {
|
|
807
|
+
console.error(`skip ${asset}: not supported by host '${agent}'`)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function copyDirTransformed(srcDir, destDir, host, assetKind) {
|
|
811
|
+
await mkdir(destDir, { recursive: true })
|
|
812
|
+
for (const e of await readdir(srcDir, { withFileTypes: true })) {
|
|
813
|
+
const src = join(srcDir, e.name)
|
|
814
|
+
const dest = join(destDir, e.name)
|
|
815
|
+
if (e.isDirectory()) {
|
|
816
|
+
await copyDirTransformed(src, dest, host, assetKind)
|
|
817
|
+
} else if (e.isFile() && e.name.endsWith(MD_EXT)) {
|
|
818
|
+
await writeFile(dest, adaptFrontmatter(await readFile(src, 'utf8'), host, assetKind))
|
|
819
|
+
} else if (e.isFile()) {
|
|
820
|
+
await cp(src, dest)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function installAssetFiles(cat, projectDir, spec, host) {
|
|
826
|
+
const destDir = join(projectDir, spec.dir)
|
|
827
|
+
await mkdir(destDir, { recursive: true })
|
|
828
|
+
for (const e of await readdir(join(ROOT, cat), { withFileTypes: true })) {
|
|
829
|
+
if (!e.isFile() || !e.name.endsWith(MD_EXT)) continue
|
|
830
|
+
const base = e.name.slice(0, -MD_EXT.length)
|
|
831
|
+
const content = await readFile(join(ROOT, cat, e.name), 'utf8')
|
|
832
|
+
await writeFile(join(destDir, `${base}${spec.ext}`), adaptFrontmatter(content, host, cat))
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Dueno unico de la instalacion de assets por proyecto: materializa skills,
|
|
837
|
+
// agents y commands en el destino nativo del host y devuelve las rutas escritas.
|
|
838
|
+
async function installAgentAssets(projectDir, agent) {
|
|
839
|
+
const model = AGENT_TARGETS[agent] || AGENT_TARGETS[DEFAULT_AGENT]
|
|
840
|
+
const written = { agents: [], commands: [], skills: [] }
|
|
841
|
+
|
|
842
|
+
if (model.skills) {
|
|
843
|
+
await copyDirTransformed(join(ROOT, 'skills'), join(projectDir, model.skills.dir), agent, 'skills')
|
|
844
|
+
written.skills.push(model.skills.dir)
|
|
845
|
+
} else {
|
|
846
|
+
warnUnsupportedAsset('skills', agent)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
for (const cat of ['agents', 'commands']) {
|
|
850
|
+
const spec = model[cat]
|
|
851
|
+
if (!spec) {
|
|
852
|
+
warnUnsupportedAsset(cat, agent)
|
|
753
853
|
continue
|
|
754
854
|
}
|
|
755
|
-
await
|
|
855
|
+
await installAssetFiles(cat, projectDir, spec, agent)
|
|
856
|
+
written[cat].push(spec.dir)
|
|
857
|
+
}
|
|
858
|
+
return written
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// D10: installedPaths es la union de destinos realmente escritos; un asset
|
|
862
|
+
// omitido (null) no deja rastro. No se borran rutas previas (no destructivo).
|
|
863
|
+
function unionInstalledPaths(existing, next) {
|
|
864
|
+
const out = {}
|
|
865
|
+
for (const k of ['templates', 'agents', 'commands', 'skills']) {
|
|
866
|
+
out[k] = [...new Set([...(existing?.[k] || []), ...(next?.[k] || [])])]
|
|
756
867
|
}
|
|
757
|
-
|
|
758
|
-
|
|
868
|
+
return out
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Convencion de nombre esperada en un directorio instalado (para `check`).
|
|
872
|
+
function installedExtFor(cat, dirRel) {
|
|
873
|
+
for (const t of Object.values(AGENT_TARGETS)) {
|
|
874
|
+
const spec = t[cat]
|
|
875
|
+
if (spec && spec.dir === dirRel && spec.ext) return spec.ext
|
|
759
876
|
}
|
|
760
|
-
return
|
|
877
|
+
return null
|
|
761
878
|
}
|
|
762
879
|
|
|
763
880
|
const CHANGES_ROOT = 'aspec'
|
|
@@ -793,7 +910,7 @@ async function hasRealContent(aspecDir) {
|
|
|
793
910
|
}
|
|
794
911
|
|
|
795
912
|
// Migra la carpeta legacy openspec/ a aspec/ por copia recursiva (no destructiva).
|
|
796
|
-
// Unico dueno del invariante: la invocan init
|
|
913
|
+
// Unico dueno del invariante: la invocan init e install --project antes del scaffold, y upgrade como paso previo (sin scaffold).
|
|
797
914
|
async function migrateLegacyOpenspec(projectDir) {
|
|
798
915
|
const legacyDir = join(projectDir, LEGACY_OPENSPEC_DIR)
|
|
799
916
|
if (!(await exists(legacyDir))) return // sin legacy: no-op silencioso
|
|
@@ -864,9 +981,9 @@ function findInsertAnchor(local, source, name) {
|
|
|
864
981
|
return local.length
|
|
865
982
|
}
|
|
866
983
|
|
|
984
|
+
// Solo mergea los templates raiz (AGENTS.md/PRODUCT.md). Los assets por host
|
|
985
|
+
// los materializa installAgentAssets (dueno unico).
|
|
867
986
|
async function copyTemplates(projectDir) {
|
|
868
|
-
const dest = join(projectDir, '.opencode')
|
|
869
|
-
await copyAssets(dest)
|
|
870
987
|
for (const t of TEMPLATES) {
|
|
871
988
|
const target = join(projectDir, t)
|
|
872
989
|
const source = await readFile(join(ROOT, 'templates', t), 'utf8')
|
|
@@ -962,20 +1079,19 @@ async function install(args) {
|
|
|
962
1079
|
|
|
963
1080
|
if (projectDir) {
|
|
964
1081
|
await copyTemplates(projectDir)
|
|
1082
|
+
await migrateLegacyOpenspec(projectDir)
|
|
965
1083
|
await scaffoldAspec(projectDir)
|
|
966
1084
|
await refreshWorkingContext(projectDir)
|
|
967
|
-
const
|
|
1085
|
+
const written = await installAgentAssets(projectDir, agent)
|
|
968
1086
|
await writeManifest(projectDir, {
|
|
969
1087
|
agent,
|
|
970
1088
|
language,
|
|
971
1089
|
...(excludeChoice !== null ? { discovery: { ...existingDiscovery, exclude: excludeChoice } } : {}),
|
|
972
1090
|
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
973
|
-
installedPaths: {
|
|
1091
|
+
installedPaths: unionInstalledPaths(existingRc?.installedPaths, {
|
|
974
1092
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
skills: [agentSkillsDir]
|
|
978
|
-
}
|
|
1093
|
+
...written
|
|
1094
|
+
})
|
|
979
1095
|
})
|
|
980
1096
|
} else {
|
|
981
1097
|
await copyAssets(target)
|
|
@@ -983,6 +1099,9 @@ async function install(args) {
|
|
|
983
1099
|
|
|
984
1100
|
const models = tierModels(tier, TIERS, gratisModelChoice)
|
|
985
1101
|
await applyTier(join(target, 'agents'), tier, models)
|
|
1102
|
+
// El estado de tier vive en el base de opencode; puede no existir si el host
|
|
1103
|
+
// resuelto ubica sus assets en otro directorio (p. ej. cursor -> .cursor/).
|
|
1104
|
+
await mkdir(target, { recursive: true })
|
|
986
1105
|
await writeFile(tierStatePath(target), tier + '\n')
|
|
987
1106
|
|
|
988
1107
|
if (projectDir) await registerProject(projectDir, { agent, tier })
|
|
@@ -998,7 +1117,7 @@ async function install(args) {
|
|
|
998
1117
|
|
|
999
1118
|
const res = await mergeMcp(target, mcpMap)
|
|
1000
1119
|
const loc = projectDir
|
|
1001
|
-
? `${projectDir} (
|
|
1120
|
+
? `${projectDir} (layout ${agent} + templates en la raiz)`
|
|
1002
1121
|
: `${target} (disponible en todos tus proyectos)`
|
|
1003
1122
|
console.log(`ancleto: instalado en ${loc}`)
|
|
1004
1123
|
console.log(`ancleto: tier de costo de agents: ${tier}`)
|
|
@@ -1019,20 +1138,19 @@ async function upgradeCmd(args) {
|
|
|
1019
1138
|
await migrateLegacyOpenspec(projectDir)
|
|
1020
1139
|
const agent = await resolveAgent(args, rc.agent)
|
|
1021
1140
|
await copyTemplates(projectDir)
|
|
1022
|
-
const
|
|
1141
|
+
const written = await installAgentAssets(projectDir, agent)
|
|
1023
1142
|
const wentContext = await refreshWorkingContext(projectDir)
|
|
1024
1143
|
const manifest = await writeManifest(projectDir, {
|
|
1025
1144
|
agent,
|
|
1026
|
-
installedPaths: {
|
|
1145
|
+
installedPaths: unionInstalledPaths(rc.installedPaths, {
|
|
1027
1146
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
skills: [agentSkillsDir]
|
|
1031
|
-
}
|
|
1147
|
+
...written
|
|
1148
|
+
})
|
|
1032
1149
|
})
|
|
1150
|
+
const skillsDir = written.skills[0] || '.opencode/skills'
|
|
1033
1151
|
console.log(`ancleto: upgrade completo (v${manifest.version}, agente: ${agent})`)
|
|
1034
1152
|
console.log(' ✔ templates re-aplicados (bloques LOCKED actualizados, EXTENSIBLE intacto)')
|
|
1035
|
-
console.log(` ✔ skills actualizadas en ${
|
|
1153
|
+
console.log(` ✔ skills actualizadas en ${skillsDir} (${ANCLETO_SKILLS.length} skills)`)
|
|
1036
1154
|
console.log(wentContext ? ' ✔ working-context.md regenerado' : ' ✔ working-context.md sin reglas activas (vacio)')
|
|
1037
1155
|
console.log(' ✔ manifiesto .ancletorc actualizado')
|
|
1038
1156
|
}
|
|
@@ -1099,12 +1217,17 @@ async function initProject(args) {
|
|
|
1099
1217
|
const persisted = isKnownGratisModel(existing?.gratisModel) ? existing.gratisModel : null
|
|
1100
1218
|
gratisModelChoice = persisted || gratisModel()
|
|
1101
1219
|
}
|
|
1220
|
+
const written = await installAgentAssets(projectDir, agent)
|
|
1102
1221
|
const manifest = await writeManifest(projectDir, {
|
|
1103
1222
|
azure,
|
|
1104
1223
|
discovery,
|
|
1105
1224
|
agent,
|
|
1106
1225
|
language,
|
|
1107
|
-
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {})
|
|
1226
|
+
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
1227
|
+
installedPaths: unionInstalledPaths(existing?.installedPaths, {
|
|
1228
|
+
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
1229
|
+
...written
|
|
1230
|
+
})
|
|
1108
1231
|
})
|
|
1109
1232
|
await copyTemplates(projectDir)
|
|
1110
1233
|
if (tier) {
|
|
@@ -1590,7 +1713,10 @@ async function checkCommand() {
|
|
|
1590
1713
|
missing++
|
|
1591
1714
|
continue
|
|
1592
1715
|
}
|
|
1593
|
-
|
|
1716
|
+
// La convencion de nombre del host (p. ej. vscode: .agent.md/.prompt.md)
|
|
1717
|
+
// se deriva del mismo modelo de destino; en skills no cambia el nombre.
|
|
1718
|
+
const ext = installedExtFor(cat, dirRel)
|
|
1719
|
+
const expected = (await readdir(join(ROOT, cat))).sort().map((f) => (ext ? f.replace(/\.md$/, ext) : f))
|
|
1594
1720
|
const actual = (await readdir(destDir)).sort()
|
|
1595
1721
|
const missingFiles = expected.filter((f) => !actual.includes(f))
|
|
1596
1722
|
const orphanFiles = actual.filter((f) => !expected.includes(f))
|