@ancleto/spec 0.8.0 → 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 +55 -3
- 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 +487 -21
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
- [Referencia de comandos CLI](#referencia-de-comandos-cli)
|
|
32
32
|
- [Motor de memoria persistente](#motor-de-memoria-persistente)
|
|
33
33
|
- [Comandos del ciclo SDD en tu IDE](#comandos-del-ciclo-sdd-en-tu-ide)
|
|
34
|
+
- [Perfil de test automation](#perfil-de-test-automation)
|
|
35
|
+
- [Portabilidad entre máquinas](#portabilidad-entre-máquinas)
|
|
34
36
|
- [Azure DevOps (opcional)](#azure-devops-opcional)
|
|
35
37
|
- [Referencia: agentes y modelos por tier](#referencia-agentes-y-modelos-por-tier)
|
|
36
38
|
|
|
@@ -74,6 +76,12 @@ Ancleto resuelve esto centralizando las reglas de negocio en un motor de memoria
|
|
|
74
76
|
|
|
75
77
|
**Wizard interactivo** — Banner animado y menús navegables con flechas en `init` e `install`, sin librerías pesadas.
|
|
76
78
|
|
|
79
|
+
**Hosts e IDEs (7)** — opencode, claude, vscode, antigravity, cursor, roo y copilot, cada uno con su layout nativo (Copilot: prompts en `.github/prompts` + MCP en `copilot-mcp.json`).
|
|
80
|
+
|
|
81
|
+
**Perfil de test automation** — `--profile test`: tester ampliado (planning/generation/healing/coverage), comandos `cleto-test-*` y estructura `testspec/` para proyectos Playwright.
|
|
82
|
+
|
|
83
|
+
**Portabilidad entre máquinas** — `ancleto export`/`import` mudan el proyecto regenerando el MCP local (`import --repair` repara rutas rotas sin bundle).
|
|
84
|
+
|
|
77
85
|
---
|
|
78
86
|
|
|
79
87
|
## Requisitos
|
|
@@ -141,6 +149,7 @@ del host (el CLI no deduplica ni elige un "ganador").
|
|
|
141
149
|
| `antigravity` | `.agents/skills/<n>/SKILL.md` | `.agents/agents/<n>.md` (adaptado) | `.agents/skills/<n>/SKILL.md` (como skill) |
|
|
142
150
|
| `cursor` | `.cursor/skills/<n>/SKILL.md` | — | — |
|
|
143
151
|
| `roo` | `.roo/skills/<n>/SKILL.md` | — | — |
|
|
152
|
+
| `copilot` | — | `.github/prompts/<n>.prompt.md` | `.github/prompts/<n>.prompt.md` |
|
|
144
153
|
|
|
145
154
|
Cada host recibe sus skills en **su** directorio nativo. **`.agents/skills` es un punto de lectura
|
|
146
155
|
compartido** —lo leen opencode, Cursor, VS Code y Antigravity—, no un destino universal: Claude lee
|
|
@@ -174,7 +183,11 @@ incluyendo el servidor de memoria `ancleto-memory`; preserva `mcpServers` preexi
|
|
|
174
183
|
homónimos. `.mcp.json` (Claude) y `.vscode/mcp.json` (VS Code) siguen fuera de alcance, al igual que el
|
|
175
184
|
MCP global de Antigravity. `claude` se soporta con adaptación de frontmatter obligatoria para `agents`
|
|
176
185
|
(se omiten `model`/`tools` sin equivalencia verificada y las claves
|
|
177
|
-
`mode`/`color`/`temperature`/`permission`), sin MCP propio.
|
|
186
|
+
`mode`/`color`/`temperature`/`permission`), sin MCP propio. **Copilot** instala agents y commands
|
|
187
|
+
como prompt files en `.github/prompts/*.prompt.md` (mismo adapter, sin `model:`) y su MCP en
|
|
188
|
+
`copilot-mcp.json` (merge en install, regeneración de rotas en upgrade, nunca toca tu
|
|
189
|
+
`copilot-instructions.md`); el modelo lo elegís en el picker —el tier solo marca el nivel de
|
|
190
|
+
esfuerzo—. Tanto `init` como `install --project`
|
|
178
191
|
configuran el MCP del host en la misma ejecución; `--no-mcp` lo omite.
|
|
179
192
|
|
|
180
193
|
---
|
|
@@ -317,10 +330,10 @@ ancleto discovery # Genera .discovery-map.json y empaqueta el repo
|
|
|
317
330
|
## Referencia de comandos CLI
|
|
318
331
|
|
|
319
332
|
```bash
|
|
320
|
-
ancleto init [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>] [--with-azure]
|
|
333
|
+
ancleto init [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>] [--with-azure] [--profile <perfil>]
|
|
321
334
|
# Configura el proyecto (interactivo en TTY)
|
|
322
335
|
|
|
323
|
-
ancleto install [--project <dir>] [--tier <nivel>] [--agent <nombre>] [--lang <codigo>] [--exclude <globs>] [--no-mcp]
|
|
336
|
+
ancleto install [--project <dir>] [--tier <nivel>] [--agent <nombre>] [--lang <codigo>] [--exclude <globs>] [--no-mcp] [--profile <perfil>]
|
|
324
337
|
# Instala agentes, skills y templates
|
|
325
338
|
|
|
326
339
|
ancleto update # Re-instala la última versión sobre lo existente
|
|
@@ -329,6 +342,13 @@ ancleto update # Re-instala la última versión sobre lo existente
|
|
|
329
342
|
|
|
330
343
|
ancleto upgrade # Re-aplica templates y skills respetando tus personalizaciones
|
|
331
344
|
|
|
345
|
+
ancleto export [--tar <archivo>]
|
|
346
|
+
# Empaqueta lo portable del proyecto (bundle + manifest.json sin rutas)
|
|
347
|
+
|
|
348
|
+
ancleto import <bundle> [--repair] | ancleto import --repair
|
|
349
|
+
# Restaura el bundle regenerando el MCP local; --repair solo arregla
|
|
350
|
+
# entradas con command inexistente
|
|
351
|
+
|
|
332
352
|
ancleto check # Verifica la integridad de la instalación (faltantes / huérfanos)
|
|
333
353
|
|
|
334
354
|
ancleto specs check [--change <nombre>] [--json]
|
|
@@ -406,11 +426,43 @@ Una vez instalado, tu IDE expone el ciclo de vida completo como comandos barra:
|
|
|
406
426
|
| `/cleto-ff` | Avanzar rápido con el contexto ya recuperado. |
|
|
407
427
|
| `/cleto-apply` | Aplicar el código del change. |
|
|
408
428
|
| `/cleto-verify` | Verificar reglas, tests y memoria antes de cerrar. |
|
|
429
|
+
| `/cleto-security` | Realizar una auditoría de seguridad integral en el proyecto o change. |
|
|
409
430
|
| `/cleto-sync` | Sincronizar las specs con el estado del repositorio. |
|
|
410
431
|
| `/cleto-archive` | Archivar el change y registrar aprendizajes. |
|
|
411
432
|
| `/cleto-continue` | Retomar un change con artefactos pendientes. |
|
|
412
433
|
| `/cleto-explore` | Explorar un problema sin comprometerse a implementar. |
|
|
413
434
|
| `/cleto-onboard` | Recorrer el ciclo completo en modo tutorial. |
|
|
435
|
+
| `/cleto-bulk-archive` | Archivar varios changes juntos. |
|
|
436
|
+
| `/cleto-recall` | Recuperar memoria del proyecto antes de empezar. |
|
|
437
|
+
| `/cleto-transplant` | Mudar el proyecto entre máquinas (export/import). |
|
|
438
|
+
|
|
439
|
+
Con perfil `test` se suman `cleto-test-proposal`, `cleto-test-apply`, `cleto-test-heal`, `cleto-test-coverage` y `cleto-test-archive` (ver perfil abajo).
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Perfil de test automation
|
|
444
|
+
|
|
445
|
+
Para proyectos de test automation con Playwright:
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
ancleto init --profile test # o: ancleto install --project . --profile test
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Instala un tester ampliado con 4 workflows (planning, generation, healing, coverage), comandos `cleto-test-*`, estructura `testspec/specs` + `testspec/changes` (o reuso de `aspec/`) y convenciones Playwright en `AGENTS.md`. El orchestrator rutea lo test-only al tester ampliado y lo mixto al SDD general. Sin Playwright instalado, los workflows degradan a análisis y lo reportan. Combinable con cualquier host (`--profile test --agent copilot`, etc.).
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Portabilidad entre máquinas
|
|
456
|
+
|
|
457
|
+
Al copiar un proyecto de una PC a otra, las rutas absolutas del MCP se rompen (caso real: Windows → Linux). Para mudarlo:
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
ancleto export --tar traslado.tgz # en origen, antes de copiar
|
|
461
|
+
ancleto import traslado.tgz # en destino, regenera el MCP local + doctor
|
|
462
|
+
ancleto import --repair # sin bundle: repara entradas con command inexistente
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
El bundle lleva solo lo portable (manifiesto con intención MCP, sin rutas ni secretos: nunca incluye credenciales, `service.json`, global ni `memory.db`). El import no pisa entradas MCP sanas.
|
|
414
466
|
|
|
415
467
|
---
|
|
416
468
|
|
package/agents/orchestrator.md
CHANGED
|
@@ -234,6 +234,14 @@ Delegate directly to `@tester` when the request is limited to testing work such
|
|
|
234
234
|
|
|
235
235
|
Do not use this category if the request is likely to require product-code changes, behavior changes, or broader requirement clarification.
|
|
236
236
|
|
|
237
|
+
### Test profile routing (only when `.ancletorc` declares `profile: test`)
|
|
238
|
+
|
|
239
|
+
When the project uses the test automation profile, refine the classification above:
|
|
240
|
+
|
|
241
|
+
- Change is test-only (new/fixed coverage, no product-code change) → run the profile workflow: planning (`cleto-test-proposal`), generation (`cleto-test-apply`), healing (`cleto-test-heal`), coverage (`cleto-test-coverage`); delegate to the amplified `@tester`.
|
|
242
|
+
- Change is mixed (product code + tests) → follow the general SDD paths above, with the amplified `@tester` owning the testing stages.
|
|
243
|
+
- Testing changes live under `testspec/changes/` (or `aspec/changes/` when the project reuses aspec).
|
|
244
|
+
|
|
237
245
|
### 3. `direct-implementation`
|
|
238
246
|
|
|
239
247
|
Skip `@spec-writer` and delegate directly to implementation when the request is limited to:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Perform a comprehensive security audit of the codebase or a specific change
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Invoke the `ancleto-security` skill with the Skill tool and follow it exactly; do not improvise steps.
|
|
6
|
+
|
|
7
|
+
**Input**: The argument after `/cleto-security` is the optional target (e.g., a change name, a file path, or 'all'). If omitted, infer from context or ask the user.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Move a project between machines via ancleto export/import
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Mudá un proyecto ancleto entre máquinas sin romper el MCP local.
|
|
6
|
+
|
|
7
|
+
**En la máquina origen** (antes de copiar el proyecto), parado en su raíz:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
ancleto export --tar traslado.tgz
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Llevá el proyecto + `traslado.tgz` a la máquina destino (cualquier medio: USB, red, nube).
|
|
14
|
+
|
|
15
|
+
**En la máquina destino** (tras copiar el proyecto), parado en su raíz:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
ancleto import traslado.tgz
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Esto aplica los portables (`.ancletorc`, templates, `aspec/`, agents, commands, skills),
|
|
22
|
+
regenera las entradas MCP con las rutas locales y corre `ancleto doctor` al final.
|
|
23
|
+
|
|
24
|
+
Si no tenés bundle y el `opencode.json` apunta a rutas inexistentes:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
ancleto import --repair
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Nunca se exportan credenciales, tokens, `service.json`, la config global ni `memory.db`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ancleto/spec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Orquestador SDD liviano con subagentes optimizados para costo/tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"commands",
|
|
13
13
|
"skills",
|
|
14
14
|
"templates",
|
|
15
|
+
"profiles",
|
|
15
16
|
"src"
|
|
16
17
|
],
|
|
17
18
|
"publishConfig": {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Reviews test automation work for correctness, conventions, and approved-scope compliance (test profile)
|
|
3
|
+
mode: subagent
|
|
4
|
+
model: opencode-go/qwen3.6-plus
|
|
5
|
+
temperature: 0.1
|
|
6
|
+
color: '#ef4444'
|
|
7
|
+
tools:
|
|
8
|
+
read: true
|
|
9
|
+
write: false
|
|
10
|
+
edit: false
|
|
11
|
+
bash: false
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Reviewer Agent (test automation profile)
|
|
15
|
+
|
|
16
|
+
You perform a final review of test automation work for this project.
|
|
17
|
+
|
|
18
|
+
Read `AGENTS.md` at the repo root for project-specific conventions, tech stack, and patterns.
|
|
19
|
+
|
|
20
|
+
## Primary Responsibility
|
|
21
|
+
|
|
22
|
+
Review testing work using one of these approved inputs:
|
|
23
|
+
|
|
24
|
+
- An active testing change in `testspec/changes/{change-name}/` (or `aspec/changes/{change-name}/`)
|
|
25
|
+
- A direct test-only request explicitly approved by `@orchestrator`
|
|
26
|
+
|
|
27
|
+
## What to check
|
|
28
|
+
|
|
29
|
+
- Every generated or healed test traces to an approved plan item or requirement; no orphan tests.
|
|
30
|
+
- Playwright patterns match the repo conventions (selectors, fixtures, timeouts); no invented patterns when an established one exists.
|
|
31
|
+
- Healing changed the smaller side: test updated for intentional behavior change, product code touched only within the approved scope — otherwise flag for `@orchestrator`.
|
|
32
|
+
- The Validation Ledger classifies every command (`passed`, `task-regression`, `pre-existing-unrelated`, `inconclusive`); failures outside task-owned files are not dismissed as out of scope.
|
|
33
|
+
- No production behavior change hides inside a "test-only" change.
|
|
34
|
+
|
|
35
|
+
You do not have Bash and must not run commands. You do not write specs. Only raise flags so `@orchestrator` can decide.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Plans, generates, heals and covers Playwright automation tests (test profile)
|
|
3
|
+
mode: subagent
|
|
4
|
+
model: opencode-go/deepseek-v4.1-flash
|
|
5
|
+
temperature: 0.1
|
|
6
|
+
color: '#3b82f6'
|
|
7
|
+
tools:
|
|
8
|
+
read: true
|
|
9
|
+
write: true
|
|
10
|
+
edit: true
|
|
11
|
+
bash: true
|
|
12
|
+
permission:
|
|
13
|
+
bash:
|
|
14
|
+
'*': allow
|
|
15
|
+
'*az *': deny
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Tester Agent (test automation profile)
|
|
19
|
+
|
|
20
|
+
You are the QA Automation Engineer for this project, running the **test automation profile**.
|
|
21
|
+
Everything in the base tester contract applies (input modes, bash rules, verification
|
|
22
|
+
priority, output expectations). This profile adds four test-automation workflows and
|
|
23
|
+
Playwright conventions below.
|
|
24
|
+
|
|
25
|
+
Read `AGENTS.md` at the repo root for project-specific testing conventions, frameworks, and patterns.
|
|
26
|
+
|
|
27
|
+
## Playwright precondition
|
|
28
|
+
|
|
29
|
+
Before any workflow that executes tests, check whether Playwright is available in the
|
|
30
|
+
target project (`package.json` dependency or `playwright.config.*` at the repo root).
|
|
31
|
+
|
|
32
|
+
- If present: run the workflows normally.
|
|
33
|
+
- If absent: degrade with a clear message — deliver planning and coverage analysis
|
|
34
|
+
(no execution), and do NOT attempt healing or generation runs. Report the gap instead
|
|
35
|
+
of guessing.
|
|
36
|
+
|
|
37
|
+
## Workflows
|
|
38
|
+
|
|
39
|
+
### 1. planning
|
|
40
|
+
|
|
41
|
+
Propose what to test: read the approved change artifacts (or the orchestrator brief),
|
|
42
|
+
define the coverage target (critical flows, edge cases, regressions), and record it as
|
|
43
|
+
a testing change under `testspec/changes/{name}/` (or `aspec/changes/{name}/` when the
|
|
44
|
+
project reuses aspec). No test code yet.
|
|
45
|
+
|
|
46
|
+
### 2. generation
|
|
47
|
+
|
|
48
|
+
Generate Playwright tests from the approved testing plan. Follow the repo's existing
|
|
49
|
+
test structure and patterns before introducing a new pattern. Each generated test maps
|
|
50
|
+
to a plan item; untestable items are reported as gaps, not skipped silently.
|
|
51
|
+
|
|
52
|
+
### 3. healing
|
|
53
|
+
|
|
54
|
+
When a Playwright test fails: reproduce first, then diagnose (product bug vs stale
|
|
55
|
+
test vs environment). Fix the smaller side — update the test when behavior changed
|
|
56
|
+
intentionally, fix the product code only when the orchestrator scope allows it;
|
|
57
|
+
otherwise escalate back to `@orchestrator` for reclassification. Re-run to green.
|
|
58
|
+
|
|
59
|
+
### 4. coverage
|
|
60
|
+
|
|
61
|
+
Report gaps between specs (source-of-truth and deltas) and existing tests: which
|
|
62
|
+
requirements lack automation, which tests lack a backing requirement. Output a
|
|
63
|
+
prioritized gap list, not new tests.
|
|
64
|
+
|
|
65
|
+
## Output expectations (profile addition)
|
|
66
|
+
|
|
67
|
+
Append to the base summary: workflow used (planning/generation/healing/coverage),
|
|
68
|
+
Playwright availability, and the coverage gaps found (if any).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate Playwright tests from an approved testing plan
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You are running the test-automation **generation** workflow under the test profile.
|
|
6
|
+
|
|
7
|
+
1. Load the approved testing plan (`testspec/changes/{name}/` or the aspec change).
|
|
8
|
+
2. Verify the Playwright precondition; if absent, stop and report instead of generating runnable tests.
|
|
9
|
+
3. Generate one Playwright test per plan item using the repo's existing patterns. Map each test to its item.
|
|
10
|
+
4. Report untestable items as gaps (never skip silently) and hand the new tests to validation (healing/coverage as needed).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Archive a finished testing change
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You are running the test-automation **archive** workflow under the test profile.
|
|
6
|
+
|
|
7
|
+
1. Verify the testing change is complete: plan approved, tests generated, suite green (or gaps explicitly accepted).
|
|
8
|
+
2. Confirm the delta specs reflect the final tests (re-delegate updates when behavior changed mid-flight).
|
|
9
|
+
3. Archive per the project's SDD close process (same rules as `cleto-archive`), keeping `testspec/` deltas consistent.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Report gaps between specs and existing automation
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You are running the test-automation **coverage** workflow under the test profile.
|
|
6
|
+
|
|
7
|
+
1. Read the source-of-truth specs (`testspec/specs/` or `aspec/specs/`) plus active deltas.
|
|
8
|
+
2. Map each requirement to the existing Playwright tests (no execution needed — static analysis suffices).
|
|
9
|
+
3. Output a prioritized gap list: requirements without automation, tests without a backing requirement.
|
|
10
|
+
4. Do not write new tests here; propose them via `cleto-test-proposal` when asked.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Diagnose and repair a failing Playwright test
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You are running the test-automation **healing** workflow under the test profile.
|
|
6
|
+
|
|
7
|
+
1. Reproduce the failure first (run the single failing test, not the whole suite).
|
|
8
|
+
2. Diagnose: product bug vs stale test vs environment issue.
|
|
9
|
+
3. Fix the smaller side: update the test when the behavior change was intentional; touch product code only within the orchestrator-approved scope, otherwise escalate to `@orchestrator` for reclassification.
|
|
10
|
+
4. Re-run to green and report cause, side fixed, and commands run.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Propose what to automate (test planning) as a testing change
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You are running the test-automation **planning** workflow under the test profile.
|
|
6
|
+
|
|
7
|
+
1. Read the approved change artifacts (or the orchestrator brief) and the current Playwright suite layout.
|
|
8
|
+
2. Define the coverage target: critical flows, edge cases, regressions.
|
|
9
|
+
3. Check the Playwright precondition (`package.json` / `playwright.config.*`): if absent, plan analysis-only and say so.
|
|
10
|
+
4. Record the plan as a testing change under `testspec/changes/{name}/` (proposal + tasks; reuse `aspec/changes/{name}/` when the project reuses aspec). STOP and wait for approval before generating tests.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# AI Agents Context (test automation profile)
|
|
2
|
+
|
|
3
|
+
> Contexto global para agentes de IA en proyectos con perfil de test automation.
|
|
4
|
+
|
|
5
|
+
<!-- LOCKED: test-profile -->
|
|
6
|
+
### Test automation profile (Playwright)
|
|
7
|
+
|
|
8
|
+
Este proyecto usa el perfil `test` de ancleto (`profile: test` en `.ancletorc`).
|
|
9
|
+
|
|
10
|
+
- Workflows: planning (`cleto-test-proposal`), generation (`cleto-test-apply`), healing (`cleto-test-heal`), coverage (`cleto-test-coverage`), archive (`cleto-test-archive`).
|
|
11
|
+
- Estructura: `testspec/specs/` (cobertura actual) + `testspec/changes/` (deltas), o reuso de `aspec/` cuando el proyecto ya lo usa.
|
|
12
|
+
- Framework: Playwright. Si no está instalado, los workflows degradan a análisis sin ejecución y lo reportan.
|
|
13
|
+
- Ruteo: cambio test-only → tester ampliado; cambio mixto → SDD general con apoyo del tester.
|
|
14
|
+
<!-- /LOCKED: test-profile -->
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ancleto-security
|
|
3
|
+
description: Perform a comprehensive security audit of the codebase or a specific change. Checks for common vulnerabilities like SQL injection, XSS, hardcoded secrets, etc.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: No external binaries required. Works with any agent runtime.
|
|
6
|
+
metadata:
|
|
7
|
+
author: ancleto
|
|
8
|
+
version: '1.0'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# aspec Security Audit
|
|
12
|
+
|
|
13
|
+
Perform a comprehensive security audit of the project.
|
|
14
|
+
|
|
15
|
+
**Input**: Optionally a change name or directory (e.g., `add-auth`, `src/`); if omitted, infer from context.
|
|
16
|
+
|
|
17
|
+
## Steps
|
|
18
|
+
|
|
19
|
+
### 1. Identify the Scope
|
|
20
|
+
Determine if the user wants to review a specific file, recent changes, or the whole project.
|
|
21
|
+
|
|
22
|
+
### 2. Basic Static Analysis
|
|
23
|
+
Run existing built-in static analysis tools (e.g., `npm run lint`) to catch obvious security issues caught by linters.
|
|
24
|
+
|
|
25
|
+
### 3. Agent Manual Review
|
|
26
|
+
Actively search for vulnerabilities in the target scope using file viewing and grep tools. Look for:
|
|
27
|
+
- **SQL Injections:** Check for concatenated queries instead of parameterized ones.
|
|
28
|
+
- **XSS (Cross-Site Scripting):** Review how user inputs are rendered in views.
|
|
29
|
+
- **Hardcoded Secrets:** Look for passwords, API tokens, or encryption keys in source code.
|
|
30
|
+
- **Command Injection:** Review usage of `exec`, `spawn`, or similar functions with unsanitized user input.
|
|
31
|
+
- **Path Traversal:** Check manipulation of file paths with user input.
|
|
32
|
+
|
|
33
|
+
### 4. Create Audit Report
|
|
34
|
+
Generate an artifact named `security_audit_report.md` detailing the findings:
|
|
35
|
+
- Classify by severity: Critical, High, Medium, Low.
|
|
36
|
+
- Show exactly in which file and line the issue occurs.
|
|
37
|
+
- Propose a remediation or corrected code.
|
|
38
|
+
|
|
39
|
+
### 5. Action Plan
|
|
40
|
+
Present the report to the user and offer to apply the security fixes.
|
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,18 +35,19 @@ 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
43
|
antigravity instala skills, agents adaptados y commands como skills
|
|
44
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)
|
|
45
46
|
ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
|
|
46
47
|
Parado en un proyecto con .ancletorc opera sobre ese proyecto;
|
|
47
48
|
usa --global para forzar el alcance global
|
|
48
49
|
ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
|
|
49
|
-
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>] [--no-mcp]
|
|
50
|
+
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>] [--lang <codigo>] [--exclude <globs>] [--no-mcp] [--profile <perfil>]
|
|
50
51
|
Crea .ancletorc en el repositorio actual y configura el MCP del host
|
|
51
52
|
(interactivo en TTY: banner + menu; Azure desactivado por defecto)
|
|
52
53
|
ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
|
|
@@ -78,6 +79,9 @@ Uso:
|
|
|
78
79
|
ancleto mcp Servidor MCP de memoria propia (stdio) para tu IDE
|
|
79
80
|
expone searchMemory, recordRule y recordDecision
|
|
80
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
|
|
81
85
|
ancleto doctor Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
82
86
|
ancleto --help Esta ayuda
|
|
83
87
|
ancleto --version Version del paquete
|
|
@@ -613,7 +617,7 @@ async function applyTier(agentsDir, tier, models = null) {
|
|
|
613
617
|
}
|
|
614
618
|
}
|
|
615
619
|
|
|
616
|
-
const SUPPORTED_AGENTS = ['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo']
|
|
620
|
+
const SUPPORTED_AGENTS = ['opencode', 'claude', 'vscode', 'antigravity', 'cursor', 'roo', 'copilot']
|
|
617
621
|
const DEFAULT_AGENT = 'opencode'
|
|
618
622
|
|
|
619
623
|
function askAgent() {
|
|
@@ -663,6 +667,43 @@ function langLabel(code) {
|
|
|
663
667
|
return { auto: 'automatico (idioma de la conversacion)', es: 'espanol', en: 'ingles', pt: 'portugues' }[code] || code
|
|
664
668
|
}
|
|
665
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
|
+
|
|
666
707
|
const EXCLUDE_PRESETS = [
|
|
667
708
|
{ key: 'tests', label: 'tests (*.test.*, *.spec.*, __tests__)', recommended: true, globs: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**'] },
|
|
668
709
|
{ key: 'assets', label: 'assets pesados (*.png, *.jpg, *.svg, *.ico, public/)', recommended: true, globs: ['**/*.png', '**/*.jpg', '**/*.svg', '**/*.ico', 'public'] },
|
|
@@ -778,6 +819,44 @@ async function mergeAntigravityMcp(projectDir, mcpMap) {
|
|
|
778
819
|
return { file, added }
|
|
779
820
|
}
|
|
780
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
|
+
|
|
781
860
|
// D6: dueno unico del MCP de host. Antigravity mergea `.agents/mcp_config.json`;
|
|
782
861
|
// el resto conserva `mergeMcp` sobre opencode.json (targetDir = .opencode del
|
|
783
862
|
// proyecto o config global).
|
|
@@ -788,6 +867,36 @@ async function setupHostMcp(targetDir, mcpMap, { projectDir = null, agent = null
|
|
|
788
867
|
return mergeMcp(targetDir, mcpMap)
|
|
789
868
|
}
|
|
790
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
|
+
|
|
791
900
|
async function copyAssets(dest) {
|
|
792
901
|
await mkdir(dest, { recursive: true })
|
|
793
902
|
for (const d of ASSETS) {
|
|
@@ -826,7 +935,14 @@ const AGENT_TARGETS = {
|
|
|
826
935
|
// Cursor y Roo: solo skills (D3/D7). .agents/skills es punto de lectura
|
|
827
936
|
// compartido (D2), no destino universal.
|
|
828
937
|
cursor: { skills: { dir: '.cursor/skills' }, agents: null, commands: null },
|
|
829
|
-
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
|
+
}
|
|
830
946
|
}
|
|
831
947
|
|
|
832
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']
|
|
@@ -856,11 +972,22 @@ function serializeFrontmatter(parsed) {
|
|
|
856
972
|
}
|
|
857
973
|
|
|
858
974
|
// D4/D5: hosts cuyo formato de agents difiere del origen requieren adaptacion.
|
|
859
|
-
// Claude/VS Code eliminan las claves de opencode; Antigravity usa una
|
|
975
|
+
// Claude/VS Code/Copilot eliminan las claves de opencode; Antigravity usa una
|
|
860
976
|
// transformacion propia (D2). El resto es identidad (preservacion verbatim).
|
|
861
|
-
const AGENT_ADAPTER_HOSTS = new Set(['claude', 'vscode', 'antigravity'])
|
|
977
|
+
const AGENT_ADAPTER_HOSTS = new Set(['claude', 'vscode', 'antigravity', 'copilot'])
|
|
862
978
|
const AGENT_ADAPTER_DROP = new Set(['mode', 'color', 'temperature', 'permission', 'model', 'tools'])
|
|
863
979
|
|
|
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
|
+
|
|
864
991
|
// D3: tabla unica (fijada contra la tabla oficial de frontmatter de Custom
|
|
865
992
|
// Subagents) de ids de Antigravity. Conjunto CERRADO: solo se emiten estos ids.
|
|
866
993
|
// Los ids solo-SDK (`find_file`, `edit_file`, `search_web`, `read_url_content`,
|
|
@@ -994,14 +1121,17 @@ async function copyDirTransformed(srcDir, destDir, host, assetKind) {
|
|
|
994
1121
|
}
|
|
995
1122
|
}
|
|
996
1123
|
|
|
997
|
-
async function installAssetFiles(cat, projectDir, spec, host) {
|
|
1124
|
+
async function installAssetFiles(cat, projectDir, spec, host, srcDir = null) {
|
|
998
1125
|
const destDir = join(projectDir, spec.dir)
|
|
999
1126
|
await mkdir(destDir, { recursive: true })
|
|
1000
|
-
|
|
1127
|
+
const origin = srcDir || join(ROOT, cat)
|
|
1128
|
+
for (const e of await readdir(origin, { withFileTypes: true })) {
|
|
1001
1129
|
if (!e.isFile() || !e.name.endsWith(MD_EXT)) continue
|
|
1002
1130
|
const base = e.name.slice(0, -MD_EXT.length)
|
|
1003
|
-
const content = await readFile(join(
|
|
1004
|
-
|
|
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)
|
|
1005
1135
|
}
|
|
1006
1136
|
}
|
|
1007
1137
|
|
|
@@ -1104,6 +1234,11 @@ async function scaffoldAspec(projectDir) {
|
|
|
1104
1234
|
}
|
|
1105
1235
|
}
|
|
1106
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
|
+
|
|
1107
1242
|
const LEGACY_OPENSPEC_DIR = 'openspec'
|
|
1108
1243
|
const MIGRATION_MARKER = '.migrated-from-openspec'
|
|
1109
1244
|
|
|
@@ -1137,6 +1272,97 @@ async function migrateLegacyOpenspec(projectDir) {
|
|
|
1137
1272
|
console.log('ancleto: contenido migrado de openspec/ a aspec/ (openspec/ se conserva como backup)')
|
|
1138
1273
|
}
|
|
1139
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
|
+
|
|
1140
1366
|
function extractLockedBlocks(content) {
|
|
1141
1367
|
const blocks = new Map()
|
|
1142
1368
|
const re = /<!--\s*LOCKED:\s*([\w-]+)\s*-->([\s\S]*?)<!--\s*\/LOCKED:\s*\1\s*-->/g
|
|
@@ -1194,10 +1420,11 @@ function findInsertAnchor(local, source, name) {
|
|
|
1194
1420
|
|
|
1195
1421
|
// Solo mergea los templates raiz (AGENTS.md/PRODUCT.md). Los assets por host
|
|
1196
1422
|
// los materializa installAgentAssets (dueno unico).
|
|
1197
|
-
async function copyTemplates(projectDir) {
|
|
1423
|
+
async function copyTemplates(projectDir, profile = DEFAULT_PROFILE) {
|
|
1198
1424
|
for (const t of TEMPLATES) {
|
|
1199
1425
|
const target = join(projectDir, t)
|
|
1200
|
-
const
|
|
1426
|
+
const profileTemplate = join(profileAssetsRoot(profile), 'templates', t)
|
|
1427
|
+
const source = await readFile(await exists(profileTemplate) ? profileTemplate : join(ROOT, 'templates', t), 'utf8')
|
|
1201
1428
|
if (!(await exists(target))) {
|
|
1202
1429
|
await writeFile(target, source)
|
|
1203
1430
|
continue
|
|
@@ -1242,6 +1469,7 @@ async function install(args) {
|
|
|
1242
1469
|
|
|
1243
1470
|
let agent = agentFlag || (existingRc?.agent && SUPPORTED_AGENTS.includes(existingRc.agent) ? existingRc.agent : null)
|
|
1244
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
|
|
1245
1473
|
const excludeFlag = scanExcludeFlag(args)
|
|
1246
1474
|
const existingDiscovery = existingRc?.discovery || { outputDir: 'docs/technical-discovery', exclude: [] }
|
|
1247
1475
|
let excludeChoice = excludeFlag
|
|
@@ -1289,14 +1517,17 @@ async function install(args) {
|
|
|
1289
1517
|
}
|
|
1290
1518
|
|
|
1291
1519
|
if (projectDir) {
|
|
1292
|
-
await copyTemplates(projectDir)
|
|
1293
|
-
await
|
|
1520
|
+
await copyTemplates(projectDir, profile)
|
|
1521
|
+
await maybeImportOpenspec(projectDir, args)
|
|
1294
1522
|
await scaffoldAspec(projectDir)
|
|
1523
|
+
if (profile === 'test') await scaffoldTestspec(projectDir)
|
|
1295
1524
|
await refreshWorkingContext(projectDir)
|
|
1296
1525
|
const written = await installAgentAssets(projectDir, agent)
|
|
1526
|
+
if (profile === 'test') await installProfileOverlay(projectDir, agent)
|
|
1297
1527
|
await writeManifest(projectDir, {
|
|
1298
1528
|
agent,
|
|
1299
1529
|
language,
|
|
1530
|
+
profile,
|
|
1300
1531
|
...(excludeChoice !== null ? { discovery: { ...existingDiscovery, exclude: excludeChoice } } : {}),
|
|
1301
1532
|
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
1302
1533
|
installedPaths: unionInstalledPaths(existingRc?.installedPaths, {
|
|
@@ -1306,6 +1537,9 @@ async function install(args) {
|
|
|
1306
1537
|
})
|
|
1307
1538
|
} else {
|
|
1308
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
|
+
}
|
|
1309
1543
|
}
|
|
1310
1544
|
|
|
1311
1545
|
const models = tierModels(tier, TIERS, gratisModelChoice)
|
|
@@ -1327,6 +1561,10 @@ async function install(args) {
|
|
|
1327
1561
|
}
|
|
1328
1562
|
|
|
1329
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
|
+
}
|
|
1330
1568
|
const loc = projectDir
|
|
1331
1569
|
? `${projectDir} (layout ${agent} + templates en la raiz)`
|
|
1332
1570
|
: `${target} (disponible en todos tus proyectos)`
|
|
@@ -1343,16 +1581,26 @@ async function upgradeCmd(args) {
|
|
|
1343
1581
|
const projectDir = process.cwd()
|
|
1344
1582
|
const rc = await readAncletorc(projectDir)
|
|
1345
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
|
+
}
|
|
1346
1590
|
console.error("Error: No se encontro .ancletorc. Ejecuta 'ancleto init' primero.")
|
|
1347
1591
|
process.exit(1)
|
|
1348
1592
|
}
|
|
1349
|
-
await
|
|
1593
|
+
await maybeImportOpenspec(projectDir, args)
|
|
1350
1594
|
const agent = await resolveAgent(args, rc.agent)
|
|
1351
|
-
|
|
1595
|
+
const profile = scanProfileFlag(args) || validProfile(rc.profile) || DEFAULT_PROFILE
|
|
1596
|
+
await copyTemplates(projectDir, profile)
|
|
1597
|
+
if (profile === 'test') await scaffoldTestspec(projectDir)
|
|
1352
1598
|
const written = await installAgentAssets(projectDir, agent)
|
|
1599
|
+
if (profile === 'test') await installProfileOverlay(projectDir, agent)
|
|
1353
1600
|
const wentContext = await refreshWorkingContext(projectDir)
|
|
1354
1601
|
const manifest = await writeManifest(projectDir, {
|
|
1355
1602
|
agent,
|
|
1603
|
+
profile,
|
|
1356
1604
|
installedPaths: unionInstalledPaths(rc.installedPaths, {
|
|
1357
1605
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
1358
1606
|
...written
|
|
@@ -1361,7 +1609,15 @@ async function upgradeCmd(args) {
|
|
|
1361
1609
|
const skillsDir = written.skills[0] || '.opencode/skills'
|
|
1362
1610
|
console.log(`ancleto: upgrade completo (v${manifest.version}, agente: ${agent})`)
|
|
1363
1611
|
console.log(' ✔ templates re-aplicados (bloques LOCKED actualizados, EXTENSIBLE intacto)')
|
|
1364
|
-
|
|
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
|
+
}
|
|
1365
1621
|
console.log(wentContext ? ' ✔ working-context.md regenerado' : ' ✔ working-context.md sin reglas activas (vacio)')
|
|
1366
1622
|
console.log(' ✔ manifiesto .ancletorc actualizado')
|
|
1367
1623
|
}
|
|
@@ -1373,6 +1629,7 @@ async function initProject(args) {
|
|
|
1373
1629
|
const agentFlag = scanAgentFlag(args)
|
|
1374
1630
|
const tierFlag = scanTierFlag(args)
|
|
1375
1631
|
const langFlag = scanLangFlag(args)
|
|
1632
|
+
const profileFlag = scanProfileFlag(args)
|
|
1376
1633
|
const existing = await readAncletorc(projectDir)
|
|
1377
1634
|
const azure = existing?.azure ?? { enabled: false }
|
|
1378
1635
|
let discovery = existing?.discovery ?? { outputDir: 'docs/technical-discovery', exclude: [] }
|
|
@@ -1382,6 +1639,7 @@ async function initProject(args) {
|
|
|
1382
1639
|
|
|
1383
1640
|
let agent, tier, language
|
|
1384
1641
|
language = langFlag || (existing?.language && SUPPORTED_LANGS.includes(existing.language) ? existing.language : null)
|
|
1642
|
+
const profile = profileFlag || validProfile(existing?.profile) || DEFAULT_PROFILE
|
|
1385
1643
|
const isInteractive = Boolean(process.stdout.isTTY) && (!agentFlag || !tierFlag || !language || (excludeChoice === null && (discovery.exclude || []).length === 0))
|
|
1386
1644
|
if (isInteractive) {
|
|
1387
1645
|
await showBanner()
|
|
@@ -1430,22 +1688,25 @@ async function initProject(args) {
|
|
|
1430
1688
|
gratisModelChoice = persisted || gratisModel()
|
|
1431
1689
|
}
|
|
1432
1690
|
const written = await installAgentAssets(projectDir, agent)
|
|
1691
|
+
if (profile === 'test') await installProfileOverlay(projectDir, agent)
|
|
1433
1692
|
const manifest = await writeManifest(projectDir, {
|
|
1434
1693
|
azure,
|
|
1435
1694
|
discovery,
|
|
1436
1695
|
agent,
|
|
1437
1696
|
language,
|
|
1697
|
+
profile,
|
|
1438
1698
|
...(gratisModelChoice ? { gratisModel: gratisModelChoice } : {}),
|
|
1439
1699
|
installedPaths: unionInstalledPaths(existing?.installedPaths, {
|
|
1440
1700
|
templates: ['AGENTS.md', 'PRODUCT.md'],
|
|
1441
1701
|
...written
|
|
1442
1702
|
})
|
|
1443
1703
|
})
|
|
1444
|
-
await
|
|
1704
|
+
await maybeImportOpenspec(projectDir, args)
|
|
1705
|
+
await copyTemplates(projectDir, profile)
|
|
1706
|
+
if (profile === 'test') await scaffoldTestspec(projectDir)
|
|
1445
1707
|
if (tier) {
|
|
1446
1708
|
await applyTier(join(projectDir, '.opencode', 'agents'), tier, tierModels(tier, TIERS, gratisModelChoice))
|
|
1447
1709
|
}
|
|
1448
|
-
await migrateLegacyOpenspec(projectDir)
|
|
1449
1710
|
await scaffoldAspec(projectDir)
|
|
1450
1711
|
await refreshWorkingContext(projectDir)
|
|
1451
1712
|
await registerProject(projectDir, { agent, tier: tier || null })
|
|
@@ -2298,6 +2559,205 @@ async function statsCommand(args) {
|
|
|
2298
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)}`)
|
|
2299
2560
|
}
|
|
2300
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
|
+
|
|
2301
2761
|
async function doctorCommand() {
|
|
2302
2762
|
let fatal = false
|
|
2303
2763
|
|
|
@@ -2383,6 +2843,12 @@ switch (cmd) {
|
|
|
2383
2843
|
case 'doctor':
|
|
2384
2844
|
await doctorCommand()
|
|
2385
2845
|
break
|
|
2846
|
+
case 'export':
|
|
2847
|
+
await exportCmd(rest)
|
|
2848
|
+
break
|
|
2849
|
+
case 'import':
|
|
2850
|
+
await importCmd(rest)
|
|
2851
|
+
break
|
|
2386
2852
|
case 'mcp':
|
|
2387
2853
|
await serveMemoryMcp()
|
|
2388
2854
|
break
|