@ancleto/spec 0.6.28 → 0.6.29
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 +40 -0
- package/package.json +1 -1
- package/src/cli/index.js +317 -0
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:
|
|
@@ -209,6 +240,15 @@ ancleto specs check [--change <nombre>] [--json]
|
|
|
209
240
|
ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
|
|
210
241
|
# Tokens por sesión de opencode (rollup de subagentes; --session desglosa por agente)
|
|
211
242
|
|
|
243
|
+
ancleto projects [list] [--json]
|
|
244
|
+
# Lista los proyectos que usan ancleto (versión, tier, origen, ruta)
|
|
245
|
+
ancleto projects scan <raíz> [--dry-run] [--json]
|
|
246
|
+
# Descubre y registra proyectos ancleto bajo una raíz
|
|
247
|
+
ancleto projects prune [--dry-run] [--json]
|
|
248
|
+
# Quita del registro los proyectos borrados o movidos
|
|
249
|
+
ancleto projects info [ruta] [--json]
|
|
250
|
+
# Estado de un proyecto (versión, memoria, seed, changes activos)
|
|
251
|
+
|
|
212
252
|
ancleto doctor # Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
213
253
|
|
|
214
254
|
ancleto memory context [--scope <project|feature|task>] [--out <archivo>]
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -47,6 +47,14 @@ Uso:
|
|
|
47
47
|
ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
|
|
48
48
|
Tokens por sesion de opencode (entrada/salida/razonamiento/cache),
|
|
49
49
|
con rollup de subagentes y desglose por agente con --session
|
|
50
|
+
ancleto projects [list] [--json] Lista los proyectos que usan ancleto (version, tier, origen, ruta)
|
|
51
|
+
ancleto projects scan <raiz> [--dry-run] [--json]
|
|
52
|
+
Descubre y registra proyectos ancleto bajo una raiz
|
|
53
|
+
ancleto projects prune [--dry-run] [--json]
|
|
54
|
+
Quita del registro los proyectos borrados o movidos
|
|
55
|
+
ancleto projects info [ruta] [--json]
|
|
56
|
+
Estado de un proyecto (version, memoria, seed, changes activos)
|
|
57
|
+
ancleto list --projects [--json] Alias de "ancleto projects list"
|
|
50
58
|
ancleto memory context [--scope X] [--out file]
|
|
51
59
|
Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
|
|
52
60
|
ancleto memory list [--type X] [--scope X] [--all] [--json]
|
|
@@ -101,6 +109,301 @@ function globalConfigDir() {
|
|
|
101
109
|
: join(homedir(), '.config', 'opencode')
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
// Registro global de proyectos que usan ancleto (no depende de opencode).
|
|
113
|
+
function projectsRegistryPath() {
|
|
114
|
+
if (process.env.ANCLETO_PROJECTS_FILE) return process.env.ANCLETO_PROJECTS_FILE
|
|
115
|
+
const base = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, 'ancleto') : join(homedir(), '.config', 'ancleto')
|
|
116
|
+
return join(base, 'projects.json')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normKey(p) {
|
|
120
|
+
return normPath(resolve(p))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readProjectsRegistry() {
|
|
124
|
+
const file = projectsRegistryPath()
|
|
125
|
+
if (!(await exists(file))) return { schemaVersion: 1, projects: {} }
|
|
126
|
+
try {
|
|
127
|
+
const data = JSON.parse((await readFile(file, 'utf8')).replace(/^\uFEFF/, ''))
|
|
128
|
+
return { schemaVersion: 1, projects: data.projects || {} }
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.warn(`ancleto: registro de proyectos ilegible (${file}): ${err.message}`)
|
|
131
|
+
return { schemaVersion: 1, projects: {} }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function writeProjectsRegistry(reg) {
|
|
136
|
+
const file = projectsRegistryPath()
|
|
137
|
+
await mkdir(dirname(file), { recursive: true })
|
|
138
|
+
await writeFile(file, JSON.stringify(reg, null, 2) + '\n')
|
|
139
|
+
return file
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function registerProject(projectDir, { agent, tier } = {}) {
|
|
143
|
+
const abs = resolve(projectDir)
|
|
144
|
+
const rc = await readAncletorc(abs)
|
|
145
|
+
const tierFile = join(abs, '.opencode', '.ancleto-tier')
|
|
146
|
+
const localTier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
147
|
+
const hasScopedAgents = await exists(join(abs, '.opencode', 'agents'))
|
|
148
|
+
const entry = {
|
|
149
|
+
path: abs.replace(/\\/g, '/'),
|
|
150
|
+
version: await packageVersion(),
|
|
151
|
+
agent: agent || rc?.agent || 'opencode',
|
|
152
|
+
tier: tier || localTier || null,
|
|
153
|
+
scoped: hasScopedAgents,
|
|
154
|
+
registeredAt: new Date().toISOString()
|
|
155
|
+
}
|
|
156
|
+
const reg = await readProjectsRegistry()
|
|
157
|
+
const key = normKey(abs)
|
|
158
|
+
entry.registeredAt = reg.projects[key]?.registeredAt || entry.registeredAt
|
|
159
|
+
entry.lastSeen = new Date().toISOString()
|
|
160
|
+
reg.projects[key] = entry
|
|
161
|
+
await writeProjectsRegistry(reg)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function unregisterProject(projectDir) {
|
|
165
|
+
const reg = await readProjectsRegistry()
|
|
166
|
+
const key = normKey(projectDir)
|
|
167
|
+
if (!reg.projects[key]) return false
|
|
168
|
+
delete reg.projects[key]
|
|
169
|
+
await writeProjectsRegistry(reg)
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Color helpers (respetan NO_COLOR y TTY).
|
|
174
|
+
const useColor = () => process.stdout.isTTY && !process.env.NO_COLOR
|
|
175
|
+
function paint(code, s) {
|
|
176
|
+
return useColor() ? `\x1b[${code}m${s}\x1b[0m` : String(s)
|
|
177
|
+
}
|
|
178
|
+
const c = {
|
|
179
|
+
dim: (s) => paint('2', s),
|
|
180
|
+
bold: (s) => paint('1', s),
|
|
181
|
+
green: (s) => paint('32', s),
|
|
182
|
+
yellow: (s) => paint('33', s),
|
|
183
|
+
red: (s) => paint('31', s),
|
|
184
|
+
cyan: (s) => paint('36', s)
|
|
185
|
+
}
|
|
186
|
+
const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '')
|
|
187
|
+
function padVisible(s, width) {
|
|
188
|
+
const len = stripAnsi(s).length
|
|
189
|
+
return s + ' '.repeat(Math.max(0, width - len))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Descubre proyectos ancleto bajo una raiz (busca .ancletorc, con profundidad acotada).
|
|
193
|
+
async function discoverProjectsUnder(root, maxDepth = 3) {
|
|
194
|
+
const found = []
|
|
195
|
+
const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', 'vendor'])
|
|
196
|
+
async function walk(dir, depth) {
|
|
197
|
+
let entries
|
|
198
|
+
try {
|
|
199
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
200
|
+
} catch {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
if (entries.some((e) => e.isFile() && (e.name === '.ancletorc' || e.name === '.ancletorc'))) found.push(dir)
|
|
204
|
+
if (depth >= maxDepth) return
|
|
205
|
+
for (const e of entries) {
|
|
206
|
+
if (!e.isDirectory() || skip.has(e.name) || e.name.startsWith('.')) continue
|
|
207
|
+
await walk(join(dir, e.name), depth + 1)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
await walk(resolve(root), 0)
|
|
211
|
+
return found
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Lee el estado real de un proyecto registrado (o descubre que ya no existe).
|
|
215
|
+
async function projectStatus(entry) {
|
|
216
|
+
const abs = entry.path
|
|
217
|
+
if (!(await exists(abs))) return { ...entry, exists: false }
|
|
218
|
+
const rc = await readAncletorc(abs)
|
|
219
|
+
if (!rc) return { ...entry, exists: false, reason: 'sin .ancletorc' }
|
|
220
|
+
const tierFile = join(abs, '.opencode', '.ancleto-tier')
|
|
221
|
+
const tier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
|
|
222
|
+
const scoped = await exists(join(abs, '.opencode', 'agents'))
|
|
223
|
+
return {
|
|
224
|
+
...entry,
|
|
225
|
+
exists: true,
|
|
226
|
+
path: abs.replace(/\\/g, '/'),
|
|
227
|
+
version: rc.version || entry.version || null,
|
|
228
|
+
agent: rc.agent || entry.agent || null,
|
|
229
|
+
tier,
|
|
230
|
+
scoped
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function projectsList(reg, { asJson, currentVersion }) {
|
|
235
|
+
const rows = await Promise.all(Object.values(reg.projects).map(projectStatus))
|
|
236
|
+
rows.sort((a, b) => String(a.path).localeCompare(String(b.path)))
|
|
237
|
+
if (asJson) {
|
|
238
|
+
console.log(JSON.stringify({ ok: true, cliVersion: currentVersion, registry: projectsRegistryPath(), count: rows.length, projects: rows }, null, 2))
|
|
239
|
+
return rows
|
|
240
|
+
}
|
|
241
|
+
if (rows.length === 0) {
|
|
242
|
+
console.log('ancleto: no hay proyectos registrados todavia')
|
|
243
|
+
console.log(c.dim(' registra uno con: ancleto install --project <ruta> · o descubre con: ancleto projects scan <raiz>'))
|
|
244
|
+
return rows
|
|
245
|
+
}
|
|
246
|
+
const live = rows.filter((r) => r.exists)
|
|
247
|
+
console.log(`${c.bold('ancleto: proyectos')} ${c.dim(`(${live.length} activos de ${rows.length} registrados · CLI v${currentVersion})`)}`)
|
|
248
|
+
console.log('')
|
|
249
|
+
const header = ['', padVisible('PROYECTO', 22), padVisible('VERSIÓN', 10), padVisible('ORIGEN', 10), padVisible('TIER', 9), padVisible('AGENTE', 10), 'RUTA']
|
|
250
|
+
console.log(c.dim(` ${header.join(' ')}`))
|
|
251
|
+
for (const r of rows) {
|
|
252
|
+
const name = r.path.split('/').filter(Boolean).pop() || r.path
|
|
253
|
+
if (!r.exists) {
|
|
254
|
+
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)}`)
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
const outdated = r.version && currentVersion && r.version !== currentVersion
|
|
258
|
+
const marker = r.scoped ? c.green('●') : c.yellow('○')
|
|
259
|
+
const ver = outdated ? c.red(r.version) : c.green(r.version || '—')
|
|
260
|
+
const origin = r.scoped ? c.green('scoped') : c.yellow('global')
|
|
261
|
+
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}`)
|
|
262
|
+
}
|
|
263
|
+
console.log('')
|
|
264
|
+
console.log(c.dim(' ● instalación por proyecto ○ usa agentes globales versión en rojo = desactualizada'))
|
|
265
|
+
return rows
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function projectsScan(root, { asJson, dryRun }) {
|
|
269
|
+
if (!root) {
|
|
270
|
+
console.error('ancleto: uso: ancleto projects scan <raiz> [--dry-run] [--json]')
|
|
271
|
+
process.exit(1)
|
|
272
|
+
}
|
|
273
|
+
if (!(await exists(root))) {
|
|
274
|
+
console.error(`ancleto: no existe la raiz "${root}"`)
|
|
275
|
+
process.exit(1)
|
|
276
|
+
}
|
|
277
|
+
const found = await discoverProjectsUnder(root)
|
|
278
|
+
const reg = await readProjectsRegistry()
|
|
279
|
+
const added = []
|
|
280
|
+
const known = []
|
|
281
|
+
for (const dir of found) {
|
|
282
|
+
const key = normKey(dir)
|
|
283
|
+
if (reg.projects[key]) {
|
|
284
|
+
known.push(dir)
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
added.push(dir)
|
|
288
|
+
if (!dryRun) await registerProject(dir)
|
|
289
|
+
}
|
|
290
|
+
if (asJson) {
|
|
291
|
+
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))
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
console.log(`${c.bold('ancleto: scan')} ${c.dim(resolve(root).replace(/\\/g, '/'))}`)
|
|
295
|
+
console.log(` ${c.dim('encontrados:')} ${found.length} proyectos con .ancletorc`)
|
|
296
|
+
for (const p of added) console.log(` ${dryRun ? c.yellow('·') : c.green('+')} ${p.replace(/\\/g, '/')}`)
|
|
297
|
+
for (const p of known) console.log(` ${c.dim('=')} ${c.dim(p.replace(/\\/g, '/'))}`)
|
|
298
|
+
if (dryRun) console.log(c.dim('\n --dry-run: no se registro nada'))
|
|
299
|
+
else console.log(c.dim(`\n registrados: ${added.length} nuevos`))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function projectsPrune({ asJson, dryRun }) {
|
|
303
|
+
const reg = await readProjectsRegistry()
|
|
304
|
+
const removed = []
|
|
305
|
+
for (const [key, entry] of Object.entries(reg.projects)) {
|
|
306
|
+
const st = await projectStatus(entry)
|
|
307
|
+
if (!st.exists) {
|
|
308
|
+
removed.push({ key, path: entry.path, reason: st.reason || 'ruta inexistente' })
|
|
309
|
+
if (!dryRun) delete reg.projects[key]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!dryRun && removed.length) await writeProjectsRegistry(reg)
|
|
313
|
+
if (asJson) {
|
|
314
|
+
console.log(JSON.stringify({ ok: true, pruned: removed.length, removed, dryRun: !!dryRun }, null, 2))
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
if (removed.length === 0) {
|
|
318
|
+
console.log(c.dim('ancleto: no hay entradas muertas en el registro'))
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
console.log(`${c.bold('ancleto: prune')}${dryRun ? ` ${c.dim('(dry-run)')}` : ''}`)
|
|
322
|
+
for (const r of removed) console.log(` ${dryRun ? c.yellow('·') : c.red('−')} ${r.path} ${c.dim(`(${r.reason})`)}`)
|
|
323
|
+
console.log(dryRun ? c.dim(`\n --dry-run: se quitarian ${removed.length}`) : c.dim(`\n removidos: ${removed.length}`))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function projectsInfo(target, { asJson, currentVersion }) {
|
|
327
|
+
const abs = resolve(target || process.cwd())
|
|
328
|
+
const rc = await readAncletorc(abs)
|
|
329
|
+
if (!rc) {
|
|
330
|
+
console.error(`ancleto: "${abs}" no parece un proyecto ancleto (sin .ancletorc)`)
|
|
331
|
+
process.exit(1)
|
|
332
|
+
}
|
|
333
|
+
const st = await projectStatus({ path: abs, ...rc })
|
|
334
|
+
const hasMemory = await exists(join(abs, '.ancleto', 'memory.db'))
|
|
335
|
+
const hasSeedIndex = await exists(join(abs, rc.discovery?.outputDir || 'docs/technical-discovery', 'index.md'))
|
|
336
|
+
const hasWorkingContext = await exists(join(abs, '.ancleto', 'working-context.md'))
|
|
337
|
+
const activeChangesDir = join(abs, CHANGES_ROOT, 'changes')
|
|
338
|
+
let activeChanges = 0
|
|
339
|
+
if (await exists(activeChangesDir)) {
|
|
340
|
+
const entries = await readdir(activeChangesDir, { withFileTypes: true })
|
|
341
|
+
activeChanges = entries.filter((e) => e.isDirectory() && e.name !== 'archive').length
|
|
342
|
+
}
|
|
343
|
+
const info = {
|
|
344
|
+
path: abs.replace(/\\/g, '/'),
|
|
345
|
+
version: st.version,
|
|
346
|
+
outdated: st.version && currentVersion ? st.version !== currentVersion : false,
|
|
347
|
+
agent: st.agent,
|
|
348
|
+
tier: st.tier,
|
|
349
|
+
scoped: st.scoped,
|
|
350
|
+
azureEnabled: !!rc.azure?.enabled,
|
|
351
|
+
memoryDb: hasMemory,
|
|
352
|
+
technicalSeed: hasSeedIndex,
|
|
353
|
+
workingContext: hasWorkingContext,
|
|
354
|
+
activeChanges
|
|
355
|
+
}
|
|
356
|
+
if (asJson) {
|
|
357
|
+
console.log(JSON.stringify({ ok: true, cliVersion: currentVersion, ...info }, null, 2))
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
console.log(`${c.bold((abs.split(/[\\/]/).filter(Boolean).pop() || abs))} ${c.dim(info.path)}`)
|
|
361
|
+
console.log('')
|
|
362
|
+
console.log(` versión ${info.outdated ? c.red(`${info.version} → ${currentVersion} disponible`) : c.green(info.version || '—')}`)
|
|
363
|
+
console.log(` instalación ${info.scoped ? c.green('por proyecto (scoped)') : c.yellow('agentes globales')}`)
|
|
364
|
+
console.log(` tier / agente ${info.tier || c.dim('—')} / ${info.agent || '—'}`)
|
|
365
|
+
console.log(` Azure DevOps ${info.azureEnabled ? c.green('habilitado') : c.dim('deshabilitado')}`)
|
|
366
|
+
console.log(` memoria ${info.memoryDb ? c.green('sí (.ancleto/memory.db)') : c.dim('no')}`)
|
|
367
|
+
console.log(` seed técnico ${info.technicalSeed ? c.green('generado') : c.dim('sin generar')}`)
|
|
368
|
+
console.log(` changes ${info.activeChanges > 0 ? c.cyan(`${info.activeChanges} activo(s)`) : c.dim('ninguno')}`)
|
|
369
|
+
if (info.outdated) console.log(c.dim(`\n actualizá con: cd "${info.path}" && ancleto update`))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function projectsCommand(args) {
|
|
373
|
+
const sub = args.find((a) => !a.startsWith('-')) || 'list'
|
|
374
|
+
const flags = args.filter((a) => a.startsWith('-'))
|
|
375
|
+
const asJson = flags.includes('--json')
|
|
376
|
+
const dryRun = flags.includes('--dry-run')
|
|
377
|
+
const currentVersion = await packageVersion()
|
|
378
|
+
switch (sub) {
|
|
379
|
+
case 'list': {
|
|
380
|
+
await projectsList(await readProjectsRegistry(), { asJson, currentVersion })
|
|
381
|
+
break
|
|
382
|
+
}
|
|
383
|
+
case 'scan': {
|
|
384
|
+
const idx = args.indexOf('scan')
|
|
385
|
+
const root = args[idx + 1] && !args[idx + 1].startsWith('-') ? args[idx + 1] : null
|
|
386
|
+
await projectsScan(root, { asJson, dryRun })
|
|
387
|
+
break
|
|
388
|
+
}
|
|
389
|
+
case 'prune': {
|
|
390
|
+
await projectsPrune({ asJson, dryRun })
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
case 'info': {
|
|
394
|
+
const idx = args.indexOf('info')
|
|
395
|
+
const target = args[idx + 1] && !args[idx + 1].startsWith('-') ? args[idx + 1] : null
|
|
396
|
+
await projectsInfo(target, { asJson, currentVersion })
|
|
397
|
+
break
|
|
398
|
+
}
|
|
399
|
+
default:
|
|
400
|
+
console.error(`ancleto: subcomando desconocido: projects ${sub}`)
|
|
401
|
+
console.error('ancleto: uso: ancleto projects [list|scan <raiz>|prune|info <ruta>] [--json] [--dry-run]')
|
|
402
|
+
process.exit(1)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
104
407
|
function binName(base) {
|
|
105
408
|
return process.platform === 'win32' ? `${base}.exe` : base
|
|
106
409
|
}
|
|
@@ -494,6 +797,8 @@ async function install(args) {
|
|
|
494
797
|
await applyTier(join(target, 'agents'), tier, models)
|
|
495
798
|
await writeFile(tierStatePath(target), tier + '\n')
|
|
496
799
|
|
|
800
|
+
if (projectDir) await registerProject(projectDir, { agent, tier })
|
|
801
|
+
|
|
497
802
|
let azureMcp = false
|
|
498
803
|
if (projectDir && withMcp) {
|
|
499
804
|
const rc = await readAncletorc(projectDir)
|
|
@@ -591,6 +896,7 @@ async function initProject(args) {
|
|
|
591
896
|
await copyTemplates(projectDir)
|
|
592
897
|
await scaffoldAspec(projectDir)
|
|
593
898
|
await refreshWorkingContext(projectDir)
|
|
899
|
+
await registerProject(projectDir, { agent, tier: tier || null })
|
|
594
900
|
if (azure.enabled) console.log(AZURE_MCP_NOTICE)
|
|
595
901
|
console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'} (Agente: ${agent})${tier ? ` (Tier: ${tier})` : ''}`)
|
|
596
902
|
}
|
|
@@ -1363,6 +1669,17 @@ switch (cmd) {
|
|
|
1363
1669
|
case 'stats':
|
|
1364
1670
|
await statsCommand(rest)
|
|
1365
1671
|
break
|
|
1672
|
+
case 'projects':
|
|
1673
|
+
await projectsCommand(rest)
|
|
1674
|
+
break
|
|
1675
|
+
case 'list':
|
|
1676
|
+
// Alias conveniente: `ancleto list --projects`
|
|
1677
|
+
if (rest.includes('--projects')) await projectsCommand(rest.filter((a) => a !== '--projects'))
|
|
1678
|
+
else {
|
|
1679
|
+
console.error('ancleto: uso: ancleto list --projects [--json] (o ancleto projects)')
|
|
1680
|
+
process.exit(1)
|
|
1681
|
+
}
|
|
1682
|
+
break
|
|
1366
1683
|
case 'doctor':
|
|
1367
1684
|
await doctorCommand()
|
|
1368
1685
|
break
|