@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.6.30",
3
+ "version": "0.6.32",
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
  }
@@ -1000,7 +1073,9 @@ function flagValue(args, flag) {
1000
1073
  }
1001
1074
 
1002
1075
  async function checkDiscovery() {
1003
- const { outputDir } = await loadDiscoveryConfig()
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
+ }