@ancleto/spec 0.6.30 → 0.6.31
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 +7 -0
- package/package.json +1 -1
- package/src/cli/index.js +76 -3
- 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
|
```
|
|
@@ -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
|
}
|
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
|
+
}
|