@ancleto/spec 0.6.30 → 0.6.32
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 +8 -1
- package/package.json +1 -1
- package/src/cli/index.js +98 -5
- package/src/cli/ui.js +76 -0
package/README.md
CHANGED
|
@@ -148,8 +148,12 @@ ancleto projects # lista (o `ancleto list --projects`)
|
|
|
148
148
|
ancleto projects info # estado del proyecto actual
|
|
149
149
|
ancleto projects scan "D:/Repos D/Proyectos" # descubre y registra los que ya existen
|
|
150
150
|
ancleto projects prune # saca del registro los borrados/movidos
|
|
151
|
+
ancleto projects update # actualiza los desactualizados (selector interactivo)
|
|
152
|
+
ancleto projects update --all # los actualiza todos sin preguntar
|
|
151
153
|
```
|
|
152
154
|
|
|
155
|
+
`projects update` detecta qué proyectos quedaron atrás de la versión del CLI y te deja elegir cuáles actualizar (espacio para marcar, `a` para todos, enter para confirmar). Actualiza cada uno en un proceso aislado, así que un proyecto roto no afecta a los demás. Es el equivalente cómodo de `cd <ruta> && ancleto update` para cada uno. En una terminal sin TTY lista los desactualizados y te recuerda usar `--all`.
|
|
156
|
+
|
|
153
157
|
La lista muestra versión instalada, origen (por proyecto vs agentes globales), tier y agente:
|
|
154
158
|
|
|
155
159
|
```
|
|
@@ -205,7 +209,7 @@ ancleto init # Crea .ancletorc, aspec/, AGENTS.md y PRODUCT.md
|
|
|
205
209
|
### 2. Descubrir contexto técnico
|
|
206
210
|
|
|
207
211
|
```bash
|
|
208
|
-
ancleto discovery --check # Estado del technical seed (READY / STALE)
|
|
212
|
+
ancleto discovery --check # Estado del technical seed (READY / STALE / PARTIAL / MISSING) + config
|
|
209
213
|
ancleto discovery # Genera .discovery-map.json y empaqueta el repo
|
|
210
214
|
```
|
|
211
215
|
|
|
@@ -250,6 +254,9 @@ ancleto projects prune [--dry-run] [--json]
|
|
|
250
254
|
# Quita del registro los proyectos borrados o movidos
|
|
251
255
|
ancleto projects info [ruta] [--json]
|
|
252
256
|
# Estado de un proyecto (versión, memoria, seed, changes activos)
|
|
257
|
+
ancleto projects update [--all] [--json]
|
|
258
|
+
# Actualiza los desactualizados (selector interactivo);
|
|
259
|
+
# --all sin preguntar. Igual que "cd <ruta> && ancleto update"
|
|
253
260
|
|
|
254
261
|
ancleto doctor # Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
255
262
|
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { memoryDoctor } from '../core/memory/doctor.js'
|
|
|
13
13
|
import { serveMemoryMcp } from '../core/memory/mcp-server.js'
|
|
14
14
|
import { writeDiscoveryMap } from '../core/discovery.js'
|
|
15
15
|
import { readProjectTier, buildRepomixArgs, tierTokenBudget } from '../core/repomix-tier.js'
|
|
16
|
-
import { showBanner, selectOption, stopBanner } from './ui.js'
|
|
16
|
+
import { showBanner, selectOption, selectMultiple, stopBanner } from './ui.js'
|
|
17
17
|
import { tierModels, gratisModel, envGratisModel, isKnownGratisModel, MUSE_SPARK_MODEL, GRATIS_FALLBACK_MODEL } from '../core/tier-models.js'
|
|
18
18
|
|
|
19
19
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -56,6 +56,9 @@ Uso:
|
|
|
56
56
|
Quita del registro los proyectos borrados o movidos
|
|
57
57
|
ancleto projects info [ruta] [--json]
|
|
58
58
|
Estado de un proyecto (version, memoria, seed, changes activos)
|
|
59
|
+
ancleto projects update [--all] [--json]
|
|
60
|
+
Actualiza los proyectos desactualizados (selector interactivo;
|
|
61
|
+
--all sin preguntar); equivalente a "cd <ruta> && ancleto update"
|
|
59
62
|
ancleto list --projects [--json] Alias de "ancleto projects list"
|
|
60
63
|
ancleto memory context [--scope X] [--out file]
|
|
61
64
|
Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
|
|
@@ -371,15 +374,81 @@ async function projectsInfo(target, { asJson, currentVersion }) {
|
|
|
371
374
|
if (info.outdated) console.log(c.dim(`\n actualizá con: cd "${info.path}" && ancleto update`))
|
|
372
375
|
}
|
|
373
376
|
|
|
377
|
+
// Actualiza proyectos desde el registro. Spawnea `ancleto update` por proyecto
|
|
378
|
+
// (proceso aislado: un fallo no contamina a los demas y la salida queda por proyecto).
|
|
379
|
+
async function projectsUpdate(reg, { asJson, all }) {
|
|
380
|
+
const currentVersion = await packageVersion()
|
|
381
|
+
const rows = await Promise.all(Object.values(reg.projects).map(projectStatus))
|
|
382
|
+
const dead = rows.filter((r) => !r.exists)
|
|
383
|
+
const outdated = rows.filter((r) => r.exists && r.version && r.version !== currentVersion)
|
|
384
|
+
const hintDead = () => {
|
|
385
|
+
if (dead.length) console.log(c.dim(` (${dead.length} entrada(s) muerta(s) en el registro: corre \`ancleto projects prune\`)`))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (outdated.length === 0) {
|
|
389
|
+
if (asJson) console.log(JSON.stringify({ ok: true, cliVersion: currentVersion, updated: [], dead: dead.map((r) => r.path), reason: 'nada desactualizado' }, null, 2))
|
|
390
|
+
else {
|
|
391
|
+
console.log(c.green('ancleto: todos los proyectos activos estan al dia'))
|
|
392
|
+
hintDead()
|
|
393
|
+
}
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let targets
|
|
398
|
+
if (all) {
|
|
399
|
+
targets = outdated
|
|
400
|
+
} else if (!process.stdout.isTTY) {
|
|
401
|
+
if (asJson) console.log(JSON.stringify({ ok: true, cliVersion: currentVersion, outdated: outdated.map((r) => ({ path: r.path, version: r.version })), dead: dead.map((r) => r.path), action: 'usa --all para actualizar sin interaccion' }, null, 2))
|
|
402
|
+
else {
|
|
403
|
+
console.log(`${c.bold('ancleto: proyectos desactualizados')} ${c.dim(`(${outdated.length})`)}`)
|
|
404
|
+
for (const r of outdated) console.log(` ${c.red(r.version)} → ${c.green(currentVersion)} ${r.path}`)
|
|
405
|
+
console.log(c.dim('\n sin TTY no puedo preguntar: corre `ancleto projects update --all` para actualizarlos todos'))
|
|
406
|
+
hintDead()
|
|
407
|
+
}
|
|
408
|
+
return
|
|
409
|
+
} else {
|
|
410
|
+
if (dead.length) {
|
|
411
|
+
console.log(c.dim(`ancleto: ${dead.length} entrada(s) muerta(s) en el registro (ignoradas; limpialas con \`ancleto projects prune\`)`))
|
|
412
|
+
}
|
|
413
|
+
const labels = outdated.map((r) => `${padVisible(r.path.split('/').filter(Boolean).pop() || r.path, 22)} ${c.red(r.version)} → ${c.green(currentVersion)} ${c.dim(r.path)}`)
|
|
414
|
+
const { cancelled, indices } = await selectMultiple(`Proyectos a actualizar (${outdated.length} desactualizados)`, labels, outdated.map((_, i) => i))
|
|
415
|
+
if (cancelled || indices.length === 0) {
|
|
416
|
+
console.log(c.dim('ancleto: nada seleccionado, no se actualizo ningun proyecto'))
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
targets = indices.map((i) => outdated[i])
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const selfCli = fileURLToPath(import.meta.url)
|
|
423
|
+
const results = []
|
|
424
|
+
console.log('')
|
|
425
|
+
for (const r of targets) {
|
|
426
|
+
process.stdout.write(` ${c.cyan('→')} ${r.path} ... `)
|
|
427
|
+
const res = spawnSync(process.execPath, [selfCli, 'update'], { cwd: r.path, encoding: 'utf8', env: process.env })
|
|
428
|
+
const okRun = res.status === 0
|
|
429
|
+
const out = `${res.stdout || ''}${res.stderr || ''}`.trim()
|
|
430
|
+
console.log(okRun ? c.green('ok') : c.red('fallo'))
|
|
431
|
+
results.push({ path: r.path, from: r.version, to: okRun ? currentVersion : null, ok: okRun, output: out.split('\n').slice(-3).join('\n') })
|
|
432
|
+
if (!okRun) console.log(c.dim(` ${out.split('\n').slice(-1)[0] || 'sin salida'}`))
|
|
433
|
+
}
|
|
434
|
+
const okCount = results.filter((x) => x.ok).length
|
|
435
|
+
console.log('')
|
|
436
|
+
console.log(`ancleto: ${okCount}/${results.length} proyectos actualizados a v${currentVersion}`)
|
|
437
|
+
if (asJson) console.log(JSON.stringify({ ok: okCount === results.length, cliVersion: currentVersion, updated: results }, null, 2))
|
|
438
|
+
}
|
|
439
|
+
|
|
374
440
|
async function projectsCommand(args) {
|
|
375
441
|
const sub = args.find((a) => !a.startsWith('-')) || 'list'
|
|
376
442
|
const flags = args.filter((a) => a.startsWith('-'))
|
|
377
443
|
const asJson = flags.includes('--json')
|
|
378
444
|
const dryRun = flags.includes('--dry-run')
|
|
445
|
+
const all = flags.includes('--all')
|
|
379
446
|
const currentVersion = await packageVersion()
|
|
380
447
|
switch (sub) {
|
|
381
448
|
case 'list': {
|
|
382
|
-
|
|
449
|
+
const reg = await readProjectsRegistry()
|
|
450
|
+
await projectsList(reg, { asJson, currentVersion })
|
|
451
|
+
if (flags.includes('--update')) await projectsUpdate(reg, { asJson, all })
|
|
383
452
|
break
|
|
384
453
|
}
|
|
385
454
|
case 'scan': {
|
|
@@ -398,9 +467,13 @@ async function projectsCommand(args) {
|
|
|
398
467
|
await projectsInfo(target, { asJson, currentVersion })
|
|
399
468
|
break
|
|
400
469
|
}
|
|
470
|
+
case 'update': {
|
|
471
|
+
await projectsUpdate(await readProjectsRegistry(), { asJson, all })
|
|
472
|
+
break
|
|
473
|
+
}
|
|
401
474
|
default:
|
|
402
475
|
console.error(`ancleto: subcomando desconocido: projects ${sub}`)
|
|
403
|
-
console.error('ancleto: uso: ancleto projects [list|scan <raiz>|prune|info <ruta
|
|
476
|
+
console.error('ancleto: uso: ancleto projects [list|scan <raiz>|prune|info <ruta>|update] [--update] [--all] [--json] [--dry-run]')
|
|
404
477
|
process.exit(1)
|
|
405
478
|
}
|
|
406
479
|
}
|
|
@@ -1000,7 +1073,9 @@ function flagValue(args, flag) {
|
|
|
1000
1073
|
}
|
|
1001
1074
|
|
|
1002
1075
|
async function checkDiscovery() {
|
|
1003
|
-
const
|
|
1076
|
+
const config = await loadDiscoveryConfig()
|
|
1077
|
+
const { outputDir } = config
|
|
1078
|
+
const tier = readProjectTier(process.cwd())
|
|
1004
1079
|
const present = await presentDocs(outputDir)
|
|
1005
1080
|
let state, action, message, missingDocs
|
|
1006
1081
|
if (present.length === 0) {
|
|
@@ -1043,7 +1118,12 @@ async function checkDiscovery() {
|
|
|
1043
1118
|
state,
|
|
1044
1119
|
recommendedAction: action,
|
|
1045
1120
|
message,
|
|
1046
|
-
missingDocs
|
|
1121
|
+
missingDocs,
|
|
1122
|
+
config: {
|
|
1123
|
+
outputDir,
|
|
1124
|
+
exclude: config.exclude,
|
|
1125
|
+
tier
|
|
1126
|
+
}
|
|
1047
1127
|
}, null, 2))
|
|
1048
1128
|
}
|
|
1049
1129
|
|
|
@@ -1102,6 +1182,19 @@ async function discovery(flags) {
|
|
|
1102
1182
|
await checkDiscovery()
|
|
1103
1183
|
return
|
|
1104
1184
|
}
|
|
1185
|
+
if (flags.includes('--help') || flags.includes('-h')) {
|
|
1186
|
+
console.log(`ancleto discovery [--compress] [--include G] [--ignore G] [--token-budget N]
|
|
1187
|
+
Genera el pack Repomix del repo y guarda el estado del seed.
|
|
1188
|
+
|
|
1189
|
+
--check Estado del seed en JSON (READY/STALE/PARTIAL/MISSING) + config
|
|
1190
|
+
--compress Pack comprimido (tree-sitter)
|
|
1191
|
+
--include <glob> Incluir solo paths que matcheen
|
|
1192
|
+
--ignore <glob> Excluir paths (ademas de los de .ancletorc)
|
|
1193
|
+
--token-budget N Presupuesto de tokens del tier
|
|
1194
|
+
|
|
1195
|
+
El seed en si lo genera la skill ancleto-technical-discovery a partir del pack.`)
|
|
1196
|
+
return
|
|
1197
|
+
}
|
|
1105
1198
|
await packDiscovery(flags)
|
|
1106
1199
|
}
|
|
1107
1200
|
|
package/src/cli/ui.js
CHANGED
|
@@ -179,3 +179,79 @@ export async function selectOption(message, options, initial = 0) {
|
|
|
179
179
|
if (result === SIGINT) process.exit(0)
|
|
180
180
|
return result === undefined ? options[start] : result
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
// Seleccion multiple: flechas para moverse, espacio para marcar/desmarcar,
|
|
184
|
+
// `a` para todos/ninguno, enter para confirmar. Devuelve indices marcados.
|
|
185
|
+
export async function selectMultiple(message, options, preselected = []) {
|
|
186
|
+
const stdin = process.stdin
|
|
187
|
+
const out = process.stdout
|
|
188
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
|
|
189
|
+
return { cancelled: false, indices: [...preselected].sort((a, b) => a - b) }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const selected = new Set(preselected)
|
|
193
|
+
let index = 0
|
|
194
|
+
let rendered = 0
|
|
195
|
+
const mark = (i) => (selected.has(i) ? `${CYAN}◉${RESET}` : `${BOLD}◯${RESET}`)
|
|
196
|
+
const draw = () => {
|
|
197
|
+
if (rendered > 0) out.write(`\x1b[${rendered}A`)
|
|
198
|
+
let buf = ''
|
|
199
|
+
for (let i = 0; i < options.length; i++) {
|
|
200
|
+
const pointer = i === index ? `${CYAN}❯${RESET}` : ' '
|
|
201
|
+
const label = i === index ? `${BOLD}${options[i]}${RESET}` : options[i]
|
|
202
|
+
buf += `\x1b[2K ${pointer} ${mark(i)} ${label}\n`
|
|
203
|
+
}
|
|
204
|
+
buf += `\x1b[2K ${'\x1b[2m'}espacio marca · a todos · enter confirma${RESET}\n`
|
|
205
|
+
out.write(buf)
|
|
206
|
+
rendered = options.length + 1
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const SIGINT = Symbol('SIGINT')
|
|
210
|
+
let resolveKey
|
|
211
|
+
const key = new Promise((resolve) => { resolveKey = resolve })
|
|
212
|
+
const onData = (chunk) => {
|
|
213
|
+
const s = chunk.toString('utf8')
|
|
214
|
+
if (s === '\u0003') {
|
|
215
|
+
stopBanner()
|
|
216
|
+
resolveKey(SIGINT)
|
|
217
|
+
} else if (s === '\u001b[A') {
|
|
218
|
+
index = (index - 1 + options.length) % options.length
|
|
219
|
+
draw()
|
|
220
|
+
} else if (s === '\u001b[B') {
|
|
221
|
+
index = (index + 1) % options.length
|
|
222
|
+
draw()
|
|
223
|
+
} else if (s === ' ') {
|
|
224
|
+
if (selected.has(index)) selected.delete(index)
|
|
225
|
+
else selected.add(index)
|
|
226
|
+
draw()
|
|
227
|
+
} else if (s === 'a' || s === 'A') {
|
|
228
|
+
if (selected.size === options.length) selected.clear()
|
|
229
|
+
else for (let i = 0; i < options.length; i++) selected.add(i)
|
|
230
|
+
draw()
|
|
231
|
+
} else if (s === '\r' || s === '\n') {
|
|
232
|
+
resolveKey('confirm')
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let result
|
|
237
|
+
try {
|
|
238
|
+
out.write(`${CYAN}?${RESET} ${BOLD}${message}${RESET}\n`)
|
|
239
|
+
stdin.setRawMode(true)
|
|
240
|
+
stdin.resume()
|
|
241
|
+
stdin.on('data', onData)
|
|
242
|
+
draw()
|
|
243
|
+
if (activeBanner) activeBanner.below += 2 + options.length
|
|
244
|
+
result = await key
|
|
245
|
+
out.write('\n')
|
|
246
|
+
if (activeBanner) activeBanner.below += 1
|
|
247
|
+
} catch {
|
|
248
|
+
result = undefined
|
|
249
|
+
} finally {
|
|
250
|
+
stdin.removeListener('data', onData)
|
|
251
|
+
try { stdin.setRawMode(false) } catch {}
|
|
252
|
+
stdin.pause()
|
|
253
|
+
}
|
|
254
|
+
if (result === SIGINT) process.exit(0)
|
|
255
|
+
if (result === undefined) return { cancelled: true, indices: [] }
|
|
256
|
+
return { cancelled: false, indices: [...selected].sort((a, b) => a - b) }
|
|
257
|
+
}
|