@ancleto/spec 0.6.29 → 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 +9 -0
- package/package.json +1 -1
- package/src/cli/index.js +90 -9
- 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
|
```
|
|
@@ -229,6 +233,8 @@ ancleto install [--project <dir>] [--tier <nivel>] [--agent <nombre>] [--no-mcp]
|
|
|
229
233
|
# Instala agentes, skills y templates
|
|
230
234
|
|
|
231
235
|
ancleto update # Re-instala la última versión sobre lo existente
|
|
236
|
+
# Parado en un proyecto con .ancletorc opera sobre ESE proyecto;
|
|
237
|
+
# usá --global para forzar el alcance global
|
|
232
238
|
|
|
233
239
|
ancleto upgrade # Re-aplica templates y skills respetando tus personalizaciones
|
|
234
240
|
|
|
@@ -248,6 +254,9 @@ ancleto projects prune [--dry-run] [--json]
|
|
|
248
254
|
# Quita del registro los proyectos borrados o movidos
|
|
249
255
|
ancleto projects info [ruta] [--json]
|
|
250
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"
|
|
251
260
|
|
|
252
261
|
ancleto doctor # Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
253
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))
|
|
@@ -34,6 +34,8 @@ Uso:
|
|
|
34
34
|
ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
|
|
35
35
|
ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (wizard si no esta guardado)
|
|
36
36
|
ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
|
|
37
|
+
Parado en un proyecto con .ancletorc opera sobre ese proyecto;
|
|
38
|
+
usa --global para forzar el alcance global
|
|
37
39
|
ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
|
|
38
40
|
ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>]
|
|
39
41
|
Crea .ancletorc en el repositorio actual
|
|
@@ -54,6 +56,9 @@ Uso:
|
|
|
54
56
|
Quita del registro los proyectos borrados o movidos
|
|
55
57
|
ancleto projects info [ruta] [--json]
|
|
56
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"
|
|
57
62
|
ancleto list --projects [--json] Alias de "ancleto projects list"
|
|
58
63
|
ancleto memory context [--scope X] [--out file]
|
|
59
64
|
Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
|
|
@@ -369,15 +374,81 @@ async function projectsInfo(target, { asJson, currentVersion }) {
|
|
|
369
374
|
if (info.outdated) console.log(c.dim(`\n actualizá con: cd "${info.path}" && ancleto update`))
|
|
370
375
|
}
|
|
371
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
|
+
|
|
372
440
|
async function projectsCommand(args) {
|
|
373
441
|
const sub = args.find((a) => !a.startsWith('-')) || 'list'
|
|
374
442
|
const flags = args.filter((a) => a.startsWith('-'))
|
|
375
443
|
const asJson = flags.includes('--json')
|
|
376
444
|
const dryRun = flags.includes('--dry-run')
|
|
445
|
+
const all = flags.includes('--all')
|
|
377
446
|
const currentVersion = await packageVersion()
|
|
378
447
|
switch (sub) {
|
|
379
448
|
case 'list': {
|
|
380
|
-
|
|
449
|
+
const reg = await readProjectsRegistry()
|
|
450
|
+
await projectsList(reg, { asJson, currentVersion })
|
|
451
|
+
if (flags.includes('--update')) await projectsUpdate(reg, { asJson, all })
|
|
381
452
|
break
|
|
382
453
|
}
|
|
383
454
|
case 'scan': {
|
|
@@ -396,9 +467,13 @@ async function projectsCommand(args) {
|
|
|
396
467
|
await projectsInfo(target, { asJson, currentVersion })
|
|
397
468
|
break
|
|
398
469
|
}
|
|
470
|
+
case 'update': {
|
|
471
|
+
await projectsUpdate(await readProjectsRegistry(), { asJson, all })
|
|
472
|
+
break
|
|
473
|
+
}
|
|
399
474
|
default:
|
|
400
475
|
console.error(`ancleto: subcomando desconocido: projects ${sub}`)
|
|
401
|
-
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]')
|
|
402
477
|
process.exit(1)
|
|
403
478
|
}
|
|
404
479
|
}
|
|
@@ -716,22 +791,28 @@ async function copyTemplates(projectDir) {
|
|
|
716
791
|
|
|
717
792
|
async function install(args) {
|
|
718
793
|
const pi = args.indexOf('--project')
|
|
719
|
-
const
|
|
794
|
+
const explicitProject = pi >= 0 ? args[pi + 1] : null
|
|
720
795
|
const withMcp = !args.includes('--no-mcp')
|
|
721
796
|
let mcpMap = withMcp ? buildDefaultMcp({ withEngram: args.includes('--with-engram') }) : {}
|
|
722
797
|
|
|
723
798
|
const agentFlag = scanAgentFlag(args)
|
|
724
799
|
let tier = scanTierFlag(args)
|
|
725
800
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
801
|
+
let projectDir
|
|
802
|
+
if (explicitProject) {
|
|
803
|
+
projectDir = resolve(explicitProject)
|
|
804
|
+
if (!(await exists(projectDir))) {
|
|
805
|
+
console.error(`ancleto: el directorio no existe: ${projectDir}`)
|
|
730
806
|
process.exit(1)
|
|
731
807
|
}
|
|
808
|
+
} else if (!args.includes('--global') && (await exists(join(process.cwd(), '.ancletorc')))) {
|
|
809
|
+
// Estar parado en un proyecto ancleto implica operar sobre ese proyecto.
|
|
810
|
+
// `--global` fuerza el alcance global cuando el repo tambien tiene .ancletorc.
|
|
811
|
+
projectDir = process.cwd()
|
|
812
|
+
} else {
|
|
813
|
+
projectDir = null
|
|
732
814
|
}
|
|
733
815
|
|
|
734
|
-
const projectDir = project ? resolve(project) : null
|
|
735
816
|
const target = projectDir ? join(projectDir, '.opencode') : globalConfigDir()
|
|
736
817
|
const existingRc = projectDir ? await readAncletorc(projectDir) : null
|
|
737
818
|
const storedTier = (await exists(tierStatePath(target)))
|
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
|
+
}
|