@ancleto/spec 0.6.28 → 0.6.30
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 +42 -0
- package/package.json +1 -1
- package/src/cli/index.js +331 -6
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
- [Guía de .gitignore](#guía-de-gitignore)
|
|
27
27
|
- [Configuración y tiers de costo](#configuración-y-tiers-de-costo)
|
|
28
28
|
- [Uso rápido](#uso-rápido)
|
|
29
|
+
- [Proyectos con ancleto](#proyectos-con-ancleto)
|
|
29
30
|
- [Referencia de comandos CLI](#referencia-de-comandos-cli)
|
|
30
31
|
- [Motor de memoria persistente](#motor-de-memoria-persistente)
|
|
31
32
|
- [Comandos del ciclo SDD en tu IDE](#comandos-del-ciclo-sdd-en-tu-ide)
|
|
@@ -138,6 +139,36 @@ El framework **no gestiona tu `.gitignore`**: es una decisión del proyecto. Com
|
|
|
138
139
|
|
|
139
140
|
---
|
|
140
141
|
|
|
142
|
+
## Proyectos con ancleto
|
|
143
|
+
|
|
144
|
+
Cuando instalás ancleto **en un proyecto** (`install --project`, `init` o `update`), el CLI lo anota en un registro global (`~/.config/ancleto/projects.json`). Así podés ver de un vistazo todos los proyectos que lo usan, sin importar desde qué terminal estés:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
ancleto projects # lista (o `ancleto list --projects`)
|
|
148
|
+
ancleto projects info # estado del proyecto actual
|
|
149
|
+
ancleto projects scan "D:/Repos D/Proyectos" # descubre y registra los que ya existen
|
|
150
|
+
ancleto projects prune # saca del registro los borrados/movidos
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
La lista muestra versión instalada, origen (por proyecto vs agentes globales), tier y agente:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
ancleto: proyectos (2 activos de 3 registrados · CLI v0.6.28)
|
|
157
|
+
|
|
158
|
+
PROYECTO VERSIÓN ORIGEN TIER AGENTE RUTA
|
|
159
|
+
● App Coffice 0.6.28 scoped minimo opencode D:/Repos D/Proyectos/App Coffice
|
|
160
|
+
● spec 0.6.28 scoped normal opencode D:/Repos D/Proyectos/ancleto/spec
|
|
161
|
+
✖ Volatile — muerto — — D:/Repos D/tmp/volatile
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `●` verde: instalación por proyecto · `○` amarillo: usa agentes globales · `✖` rojo: ruta inexistente (candidata a `prune`).
|
|
165
|
+
- Versión en rojo = desactualizada respecto del CLI instalado.
|
|
166
|
+
- `--json` en todos para scripting.
|
|
167
|
+
|
|
168
|
+
El registro es tuyo y local: no viaja con el repo ni se comparte. `ANCLETO_PROJECTS_FILE` permite apuntarlo a otro lado.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
141
172
|
## Configuración y tiers de costo
|
|
142
173
|
|
|
143
174
|
En la primera ejecución, un **wizard interactivo** te guía por los pasos de configuración (agente/IDE y nivel de gasto). También podés pasarlos por flags:
|
|
@@ -198,6 +229,8 @@ ancleto install [--project <dir>] [--tier <nivel>] [--agent <nombre>] [--no-mcp]
|
|
|
198
229
|
# Instala agentes, skills y templates
|
|
199
230
|
|
|
200
231
|
ancleto update # Re-instala la última versión sobre lo existente
|
|
232
|
+
# Parado en un proyecto con .ancletorc opera sobre ESE proyecto;
|
|
233
|
+
# usá --global para forzar el alcance global
|
|
201
234
|
|
|
202
235
|
ancleto upgrade # Re-aplica templates y skills respetando tus personalizaciones
|
|
203
236
|
|
|
@@ -209,6 +242,15 @@ ancleto specs check [--change <nombre>] [--json]
|
|
|
209
242
|
ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
|
|
210
243
|
# Tokens por sesión de opencode (rollup de subagentes; --session desglosa por agente)
|
|
211
244
|
|
|
245
|
+
ancleto projects [list] [--json]
|
|
246
|
+
# Lista los proyectos que usan ancleto (versión, tier, origen, ruta)
|
|
247
|
+
ancleto projects scan <raíz> [--dry-run] [--json]
|
|
248
|
+
# Descubre y registra proyectos ancleto bajo una raíz
|
|
249
|
+
ancleto projects prune [--dry-run] [--json]
|
|
250
|
+
# Quita del registro los proyectos borrados o movidos
|
|
251
|
+
ancleto projects info [ruta] [--json]
|
|
252
|
+
# Estado de un proyecto (versión, memoria, seed, changes activos)
|
|
253
|
+
|
|
212
254
|
ancleto doctor # Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
213
255
|
|
|
214
256
|
ancleto memory context [--scope <project|feature|task>] [--out <archivo>]
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -34,6 +34,8 @@ Uso:
|
|
|
34
34
|
ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
|
|
35
35
|
ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (wizard si no esta guardado)
|
|
36
36
|
ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
|
|
37
|
+
Parado en un proyecto con .ancletorc opera sobre ese proyecto;
|
|
38
|
+
usa --global para forzar el alcance global
|
|
37
39
|
ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
|
|
38
40
|
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>]
|
|
39
41
|
Crea .ancletorc en el repositorio actual
|
|
@@ -47,6 +49,14 @@ Uso:
|
|
|
47
49
|
ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
|
|
48
50
|
Tokens por sesion de opencode (entrada/salida/razonamiento/cache),
|
|
49
51
|
con rollup de subagentes y desglose por agente con --session
|
|
52
|
+
ancleto projects [list] [--json] Lista los proyectos que usan ancleto (version, tier, origen, ruta)
|
|
53
|
+
ancleto projects scan <raiz> [--dry-run] [--json]
|
|
54
|
+
Descubre y registra proyectos ancleto bajo una raiz
|
|
55
|
+
ancleto projects prune [--dry-run] [--json]
|
|
56
|
+
Quita del registro los proyectos borrados o movidos
|
|
57
|
+
ancleto projects info [ruta] [--json]
|
|
58
|
+
Estado de un proyecto (version, memoria, seed, changes activos)
|
|
59
|
+
ancleto list --projects [--json] Alias de "ancleto projects list"
|
|
50
60
|
ancleto memory context [--scope X] [--out file]
|
|
51
61
|
Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
|
|
52
62
|
ancleto memory list [--type X] [--scope X] [--all] [--json]
|
|
@@ -101,6 +111,301 @@ function globalConfigDir() {
|
|
|
101
111
|
: join(homedir(), '.config', 'opencode')
|
|
102
112
|
}
|
|
103
113
|
|
|
114
|
+
// Registro global de proyectos que usan ancleto (no depende de opencode).
|
|
115
|
+
function projectsRegistryPath() {
|
|
116
|
+
if (process.env.ANCLETO_PROJECTS_FILE) return process.env.ANCLETO_PROJECTS_FILE
|
|
117
|
+
const base = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'ancleto') : join(homedir(), '.config', 'ancleto')
|
|
118
|
+
return join(base, 'projects.json')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normKey(p) {
|
|
122
|
+
return normPath(resolve(p))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readProjectsRegistry() {
|
|
126
|
+
const file = projectsRegistryPath()
|
|
127
|
+
if (!(await exists(file))) return { schemaVersion: 1, projects: {} }
|
|
128
|
+
try {
|
|
129
|
+
const data = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
|
|
130
|
+
return { schemaVersion: 1, projects: data.projects || {} }
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.warn(`ancleto: registro de proyectos ilegible (${file}): ${err.message}`)
|
|
133
|
+
return { schemaVersion: 1, projects: {} }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function writeProjectsRegistry(reg) {
|
|
138
|
+
const file = projectsRegistryPath()
|
|
139
|
+
await mkdir(dirname(file), { recursive: true })
|
|
140
|
+
await writeFile(file, JSON.stringify(reg, null, 2) + '\n')
|
|
141
|
+
return file
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function registerProject(projectDir, { agent, tier } = {}) {
|
|
145
|
+
const abs = resolve(projectDir)
|
|
146
|
+
const rc = await readAncletorc(abs)
|
|
147
|
+
const tierFile = join(abs, '.opencode', '.ancleto-tier')
|
|
148
|
+
const localTier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
149
|
+
const hasScopedAgents = await exists(join(abs, '.opencode', 'agents'))
|
|
150
|
+
const entry = {
|
|
151
|
+
path: abs.replace(/\\/g, '/'),
|
|
152
|
+
version: await packageVersion(),
|
|
153
|
+
agent: agent || rc?.agent || 'opencode',
|
|
154
|
+
tier: tier || localTier || null,
|
|
155
|
+
scoped: hasScopedAgents,
|
|
156
|
+
registeredAt: new Date().toISOString()
|
|
157
|
+
}
|
|
158
|
+
const reg = await readProjectsRegistry()
|
|
159
|
+
const key = normKey(abs)
|
|
160
|
+
entry.registeredAt = reg.projects[key]?.registeredAt || entry.registeredAt
|
|
161
|
+
entry.lastSeen = new Date().toISOString()
|
|
162
|
+
reg.projects[key] = entry
|
|
163
|
+
await writeProjectsRegistry(reg)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function unregisterProject(projectDir) {
|
|
167
|
+
const reg = await readProjectsRegistry()
|
|
168
|
+
const key = normKey(projectDir)
|
|
169
|
+
if (!reg.projects[key]) return false
|
|
170
|
+
delete reg.projects[key]
|
|
171
|
+
await writeProjectsRegistry(reg)
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Color helpers (respetan NO_COLOR y TTY).
|
|
176
|
+
const useColor = () => process.stdout.isTTY && !process.env.NO_COLOR
|
|
177
|
+
function paint(code, s) {
|
|
178
|
+
return useColor() ? `\x1b[${code}m${s}\x1b[0m` : String(s)
|
|
179
|
+
}
|
|
180
|
+
const c = {
|
|
181
|
+
dim: (s) => paint('2', s),
|
|
182
|
+
bold: (s) => paint('1', s),
|
|
183
|
+
green: (s) => paint('32', s),
|
|
184
|
+
yellow: (s) => paint('33', s),
|
|
185
|
+
red: (s) => paint('31', s),
|
|
186
|
+
cyan: (s) => paint('36', s)
|
|
187
|
+
}
|
|
188
|
+
const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '')
|
|
189
|
+
function padVisible(s, width) {
|
|
190
|
+
const len = stripAnsi(s).length
|
|
191
|
+
return s + ' '.repeat(Math.max(0, width - len))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Descubre proyectos ancleto bajo una raiz (busca .ancletorc, con profundidad acotada).
|
|
195
|
+
async function discoverProjectsUnder(root, maxDepth = 3) {
|
|
196
|
+
const found = []
|
|
197
|
+
const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', 'vendor'])
|
|
198
|
+
async function walk(dir, depth) {
|
|
199
|
+
let entries
|
|
200
|
+
try {
|
|
201
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
202
|
+
} catch {
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
if (entries.some((e) => e.isFile() && (e.name === '.ancletorc' || e.name === '.ancletorc'))) found.push(dir)
|
|
206
|
+
if (depth >= maxDepth) return
|
|
207
|
+
for (const e of entries) {
|
|
208
|
+
if (!e.isDirectory() || skip.has(e.name) || e.name.startsWith('.')) continue
|
|
209
|
+
await walk(join(dir, e.name), depth + 1)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
await walk(resolve(root), 0)
|
|
213
|
+
return found
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Lee el estado real de un proyecto registrado (o descubre que ya no existe).
|
|
217
|
+
async function projectStatus(entry) {
|
|
218
|
+
const abs = entry.path
|
|
219
|
+
if (!(await exists(abs))) return { ...entry, exists: false }
|
|
220
|
+
const rc = await readAncletorc(abs)
|
|
221
|
+
if (!rc) return { ...entry, exists: false, reason: 'sin .ancletorc' }
|
|
222
|
+
const tierFile = join(abs, '.opencode', '.ancleto-tier')
|
|
223
|
+
const tier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
224
|
+
const scoped = await exists(join(abs, '.opencode', 'agents'))
|
|
225
|
+
return {
|
|
226
|
+
...entry,
|
|
227
|
+
exists: true,
|
|
228
|
+
path: abs.replace(/\\/g, '/'),
|
|
229
|
+
version: rc.version || entry.version || null,
|
|
230
|
+
agent: rc.agent || entry.agent || null,
|
|
231
|
+
tier,
|
|
232
|
+
scoped
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function projectsList(reg, { asJson, currentVersion }) {
|
|
237
|
+
const rows = await Promise.all(Object.values(reg.projects).map(projectStatus))
|
|
238
|
+
rows.sort((a, b) => String(a.path).localeCompare(String(b.path)))
|
|
239
|
+
if (asJson) {
|
|
240
|
+
console.log(JSON.stringify({ ok: true, cliVersion: currentVersion, registry: projectsRegistryPath(), count: rows.length, projects: rows }, null, 2))
|
|
241
|
+
return rows
|
|
242
|
+
}
|
|
243
|
+
if (rows.length === 0) {
|
|
244
|
+
console.log('ancleto: no hay proyectos registrados todavia')
|
|
245
|
+
console.log(c.dim(' registra uno con: ancleto install --project <ruta> · o descubre con: ancleto projects scan <raiz>'))
|
|
246
|
+
return rows
|
|
247
|
+
}
|
|
248
|
+
const live = rows.filter((r) => r.exists)
|
|
249
|
+
console.log(`${c.bold('ancleto: proyectos')} ${c.dim(`(${live.length} activos de ${rows.length} registrados · CLI v${currentVersion})`)}`)
|
|
250
|
+
console.log('')
|
|
251
|
+
const header = ['', padVisible('PROYECTO', 22), padVisible('VERSIÓN', 10), padVisible('ORIGEN', 10), padVisible('TIER', 9), padVisible('AGENTE', 10), 'RUTA']
|
|
252
|
+
console.log(c.dim(` ${header.join(' ')}`))
|
|
253
|
+
for (const r of rows) {
|
|
254
|
+
const name = r.path.split('/').filter(Boolean).pop() || r.path
|
|
255
|
+
if (!r.exists) {
|
|
256
|
+
console.log(` ${c.red('✖')} ${padVisible(c.red(name.slice(0, 21)), 22)} ${padVisible(c.dim('—'), 10)} ${padVisible(c.red('muerto'), 10)} ${padVisible('—', 9)} ${padVisible('—', 10)} ${c.dim(r.path)}`)
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
const outdated = r.version && currentVersion && r.version !== currentVersion
|
|
260
|
+
const marker = r.scoped ? c.green('●') : c.yellow('○')
|
|
261
|
+
const ver = outdated ? c.red(r.version) : c.green(r.version || '—')
|
|
262
|
+
const origin = r.scoped ? c.green('scoped') : c.yellow('global')
|
|
263
|
+
console.log(` ${marker} ${padVisible(c.bold(name.slice(0, 21)), 22)} ${padVisible(ver, 10)} ${padVisible(origin, 10)} ${padVisible(r.tier || c.dim('—'), 9)} ${padVisible(r.agent || '—', 10)} ${r.path}`)
|
|
264
|
+
}
|
|
265
|
+
console.log('')
|
|
266
|
+
console.log(c.dim(' ● instalación por proyecto ○ usa agentes globales versión en rojo = desactualizada'))
|
|
267
|
+
return rows
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function projectsScan(root, { asJson, dryRun }) {
|
|
271
|
+
if (!root) {
|
|
272
|
+
console.error('ancleto: uso: ancleto projects scan <raiz> [--dry-run] [--json]')
|
|
273
|
+
process.exit(1)
|
|
274
|
+
}
|
|
275
|
+
if (!(await exists(root))) {
|
|
276
|
+
console.error(`ancleto: no existe la raiz "${root}"`)
|
|
277
|
+
process.exit(1)
|
|
278
|
+
}
|
|
279
|
+
const found = await discoverProjectsUnder(root)
|
|
280
|
+
const reg = await readProjectsRegistry()
|
|
281
|
+
const added = []
|
|
282
|
+
const known = []
|
|
283
|
+
for (const dir of found) {
|
|
284
|
+
const key = normKey(dir)
|
|
285
|
+
if (reg.projects[key]) {
|
|
286
|
+
known.push(dir)
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
added.push(dir)
|
|
290
|
+
if (!dryRun) await registerProject(dir)
|
|
291
|
+
}
|
|
292
|
+
if (asJson) {
|
|
293
|
+
console.log(JSON.stringify({ ok: true, root: resolve(root), found: found.length, added: added.map((p) => p.replace(/\\/g, '/')), known: known.map((p) => p.replace(/\\/g, '/')), dryRun: !!dryRun }, null, 2))
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
console.log(`${c.bold('ancleto: scan')} ${c.dim(resolve(root).replace(/\\/g, '/'))}`)
|
|
297
|
+
console.log(` ${c.dim('encontrados:')} ${found.length} proyectos con .ancletorc`)
|
|
298
|
+
for (const p of added) console.log(` ${dryRun ? c.yellow('·') : c.green('+')} ${p.replace(/\\/g, '/')}`)
|
|
299
|
+
for (const p of known) console.log(` ${c.dim('=')} ${c.dim(p.replace(/\\/g, '/'))}`)
|
|
300
|
+
if (dryRun) console.log(c.dim('\n --dry-run: no se registro nada'))
|
|
301
|
+
else console.log(c.dim(`\n registrados: ${added.length} nuevos`))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function projectsPrune({ asJson, dryRun }) {
|
|
305
|
+
const reg = await readProjectsRegistry()
|
|
306
|
+
const removed = []
|
|
307
|
+
for (const [key, entry] of Object.entries(reg.projects)) {
|
|
308
|
+
const st = await projectStatus(entry)
|
|
309
|
+
if (!st.exists) {
|
|
310
|
+
removed.push({ key, path: entry.path, reason: st.reason || 'ruta inexistente' })
|
|
311
|
+
if (!dryRun) delete reg.projects[key]
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (!dryRun && removed.length) await writeProjectsRegistry(reg)
|
|
315
|
+
if (asJson) {
|
|
316
|
+
console.log(JSON.stringify({ ok: true, pruned: removed.length, removed, dryRun: !!dryRun }, null, 2))
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
if (removed.length === 0) {
|
|
320
|
+
console.log(c.dim('ancleto: no hay entradas muertas en el registro'))
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
console.log(`${c.bold('ancleto: prune')}${dryRun ? ` ${c.dim('(dry-run)')}` : ''}`)
|
|
324
|
+
for (const r of removed) console.log(` ${dryRun ? c.yellow('·') : c.red('−')} ${r.path} ${c.dim(`(${r.reason})`)}`)
|
|
325
|
+
console.log(dryRun ? c.dim(`\n --dry-run: se quitarian ${removed.length}`) : c.dim(`\n removidos: ${removed.length}`))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function projectsInfo(target, { asJson, currentVersion }) {
|
|
329
|
+
const abs = resolve(target || process.cwd())
|
|
330
|
+
const rc = await readAncletorc(abs)
|
|
331
|
+
if (!rc) {
|
|
332
|
+
console.error(`ancleto: "${abs}" no parece un proyecto ancleto (sin .ancletorc)`)
|
|
333
|
+
process.exit(1)
|
|
334
|
+
}
|
|
335
|
+
const st = await projectStatus({ path: abs, ...rc })
|
|
336
|
+
const hasMemory = await exists(join(abs, '.ancleto', 'memory.db'))
|
|
337
|
+
const hasSeedIndex = await exists(join(abs, rc.discovery?.outputDir || 'docs/technical-discovery', 'index.md'))
|
|
338
|
+
const hasWorkingContext = await exists(join(abs, '.ancleto', 'working-context.md'))
|
|
339
|
+
const activeChangesDir = join(abs, CHANGES_ROOT, 'changes')
|
|
340
|
+
let activeChanges = 0
|
|
341
|
+
if (await exists(activeChangesDir)) {
|
|
342
|
+
const entries = await readdir(activeChangesDir, { withFileTypes: true })
|
|
343
|
+
activeChanges = entries.filter((e) => e.isDirectory() && e.name !== 'archive').length
|
|
344
|
+
}
|
|
345
|
+
const info = {
|
|
346
|
+
path: abs.replace(/\\/g, '/'),
|
|
347
|
+
version: st.version,
|
|
348
|
+
outdated: st.version && currentVersion ? st.version !== currentVersion : false,
|
|
349
|
+
agent: st.agent,
|
|
350
|
+
tier: st.tier,
|
|
351
|
+
scoped: st.scoped,
|
|
352
|
+
azureEnabled: !!rc.azure?.enabled,
|
|
353
|
+
memoryDb: hasMemory,
|
|
354
|
+
technicalSeed: hasSeedIndex,
|
|
355
|
+
workingContext: hasWorkingContext,
|
|
356
|
+
activeChanges
|
|
357
|
+
}
|
|
358
|
+
if (asJson) {
|
|
359
|
+
console.log(JSON.stringify({ ok: true, cliVersion: currentVersion, ...info }, null, 2))
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
console.log(`${c.bold((abs.split(/[\\/]/).filter(Boolean).pop() || abs))} ${c.dim(info.path)}`)
|
|
363
|
+
console.log('')
|
|
364
|
+
console.log(` versión ${info.outdated ? c.red(`${info.version} → ${currentVersion} disponible`) : c.green(info.version || '—')}`)
|
|
365
|
+
console.log(` instalación ${info.scoped ? c.green('por proyecto (scoped)') : c.yellow('agentes globales')}`)
|
|
366
|
+
console.log(` tier / agente ${info.tier || c.dim('—')} / ${info.agent || '—'}`)
|
|
367
|
+
console.log(` Azure DevOps ${info.azureEnabled ? c.green('habilitado') : c.dim('deshabilitado')}`)
|
|
368
|
+
console.log(` memoria ${info.memoryDb ? c.green('sí (.ancleto/memory.db)') : c.dim('no')}`)
|
|
369
|
+
console.log(` seed técnico ${info.technicalSeed ? c.green('generado') : c.dim('sin generar')}`)
|
|
370
|
+
console.log(` changes ${info.activeChanges > 0 ? c.cyan(`${info.activeChanges} activo(s)`) : c.dim('ninguno')}`)
|
|
371
|
+
if (info.outdated) console.log(c.dim(`\n actualizá con: cd "${info.path}" && ancleto update`))
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function projectsCommand(args) {
|
|
375
|
+
const sub = args.find((a) => !a.startsWith('-')) || 'list'
|
|
376
|
+
const flags = args.filter((a) => a.startsWith('-'))
|
|
377
|
+
const asJson = flags.includes('--json')
|
|
378
|
+
const dryRun = flags.includes('--dry-run')
|
|
379
|
+
const currentVersion = await packageVersion()
|
|
380
|
+
switch (sub) {
|
|
381
|
+
case 'list': {
|
|
382
|
+
await projectsList(await readProjectsRegistry(), { asJson, currentVersion })
|
|
383
|
+
break
|
|
384
|
+
}
|
|
385
|
+
case 'scan': {
|
|
386
|
+
const idx = args.indexOf('scan')
|
|
387
|
+
const root = args[idx + 1] && !args[idx + 1].startsWith('-') ? args[idx + 1] : null
|
|
388
|
+
await projectsScan(root, { asJson, dryRun })
|
|
389
|
+
break
|
|
390
|
+
}
|
|
391
|
+
case 'prune': {
|
|
392
|
+
await projectsPrune({ asJson, dryRun })
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
case 'info': {
|
|
396
|
+
const idx = args.indexOf('info')
|
|
397
|
+
const target = args[idx + 1] && !args[idx + 1].startsWith('-') ? args[idx + 1] : null
|
|
398
|
+
await projectsInfo(target, { asJson, currentVersion })
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
default:
|
|
402
|
+
console.error(`ancleto: subcomando desconocido: projects ${sub}`)
|
|
403
|
+
console.error('ancleto: uso: ancleto projects [list|scan <raiz>|prune|info <ruta>] [--json] [--dry-run]')
|
|
404
|
+
process.exit(1)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
104
409
|
function binName(base) {
|
|
105
410
|
return process.platform === 'win32' ? `${base}.exe` : base
|
|
106
411
|
}
|
|
@@ -413,22 +718,28 @@ async function copyTemplates(projectDir) {
|
|
|
413
718
|
|
|
414
719
|
async function install(args) {
|
|
415
720
|
const pi = args.indexOf('--project')
|
|
416
|
-
const
|
|
721
|
+
const explicitProject = pi >= 0 ? args[pi + 1] : null
|
|
417
722
|
const withMcp = !args.includes('--no-mcp')
|
|
418
723
|
let mcpMap = withMcp ? buildDefaultMcp({ withEngram: args.includes('--with-engram') }) : {}
|
|
419
724
|
|
|
420
725
|
const agentFlag = scanAgentFlag(args)
|
|
421
726
|
let tier = scanTierFlag(args)
|
|
422
727
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
728
|
+
let projectDir
|
|
729
|
+
if (explicitProject) {
|
|
730
|
+
projectDir = resolve(explicitProject)
|
|
731
|
+
if (!(await exists(projectDir))) {
|
|
732
|
+
console.error(`ancleto: el directorio no existe: ${projectDir}`)
|
|
427
733
|
process.exit(1)
|
|
428
734
|
}
|
|
735
|
+
} else if (!args.includes('--global') && (await exists(join(process.cwd(), '.ancletorc')))) {
|
|
736
|
+
// Estar parado en un proyecto ancleto implica operar sobre ese proyecto.
|
|
737
|
+
// `--global` fuerza el alcance global cuando el repo tambien tiene .ancletorc.
|
|
738
|
+
projectDir = process.cwd()
|
|
739
|
+
} else {
|
|
740
|
+
projectDir = null
|
|
429
741
|
}
|
|
430
742
|
|
|
431
|
-
const projectDir = project ? resolve(project) : null
|
|
432
743
|
const target = projectDir ? join(projectDir, '.opencode') : globalConfigDir()
|
|
433
744
|
const existingRc = projectDir ? await readAncletorc(projectDir) : null
|
|
434
745
|
const storedTier = (await exists(tierStatePath(target)))
|
|
@@ -494,6 +805,8 @@ async function install(args) {
|
|
|
494
805
|
await applyTier(join(target, 'agents'), tier, models)
|
|
495
806
|
await writeFile(tierStatePath(target), tier + '\n')
|
|
496
807
|
|
|
808
|
+
if (projectDir) await registerProject(projectDir, { agent, tier })
|
|
809
|
+
|
|
497
810
|
let azureMcp = false
|
|
498
811
|
if (projectDir && withMcp) {
|
|
499
812
|
const rc = await readAncletorc(projectDir)
|
|
@@ -591,6 +904,7 @@ async function initProject(args) {
|
|
|
591
904
|
await copyTemplates(projectDir)
|
|
592
905
|
await scaffoldAspec(projectDir)
|
|
593
906
|
await refreshWorkingContext(projectDir)
|
|
907
|
+
await registerProject(projectDir, { agent, tier: tier || null })
|
|
594
908
|
if (azure.enabled) console.log(AZURE_MCP_NOTICE)
|
|
595
909
|
console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'} (Agente: ${agent})${tier ? ` (Tier: ${tier})` : ''}`)
|
|
596
910
|
}
|
|
@@ -1363,6 +1677,17 @@ switch (cmd) {
|
|
|
1363
1677
|
case 'stats':
|
|
1364
1678
|
await statsCommand(rest)
|
|
1365
1679
|
break
|
|
1680
|
+
case 'projects':
|
|
1681
|
+
await projectsCommand(rest)
|
|
1682
|
+
break
|
|
1683
|
+
case 'list':
|
|
1684
|
+
// Alias conveniente: `ancleto list --projects`
|
|
1685
|
+
if (rest.includes('--projects')) await projectsCommand(rest.filter((a) => a !== '--projects'))
|
|
1686
|
+
else {
|
|
1687
|
+
console.error('ancleto: uso: ancleto list --projects [--json] (o ancleto projects)')
|
|
1688
|
+
process.exit(1)
|
|
1689
|
+
}
|
|
1690
|
+
break
|
|
1366
1691
|
case 'doctor':
|
|
1367
1692
|
await doctorCommand()
|
|
1368
1693
|
break
|