@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.6.30",
3
+ "version": "0.6.31",
4
4
  "description": "Orquestador SDD liviano con subagentes optimizados para costo/tokens",
5
5
  "type": "module",
6
6
  "bin": {
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
- await projectsList(await readProjectsRegistry(), { asJson, currentVersion })
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>] [--json] [--dry-run]')
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
+ }