@ancleto/spec 0.6.1 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
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
@@ -10,6 +10,7 @@ import { createMemoryEngine, defaultMemoryDbPath } from '../core/memory/engine.j
10
10
  import { memoryDoctor } from '../core/memory/doctor.js'
11
11
  import { writeDiscoveryMap } from '../core/discovery.js'
12
12
  import { readProjectTier, buildRepomixArgs, tierTokenBudget } from '../core/repomix-tier.js'
13
+ import { showBanner, selectOption, stopBanner } from './ui.js'
13
14
 
14
15
  const __dirname = dirname(fileURLToPath(import.meta.url))
15
16
  const ROOT = join(__dirname, '..', '..')
@@ -29,9 +30,9 @@ Uso:
29
30
  ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (pregunta si no esta guardado)
30
31
  ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
31
32
  ancleto upgrade [--agent <nombre>] Re-aplica templates (LOCKED) y skills sobre el proyecto actual
32
- ancleto init [--with-azure] [--agent <nombre>]
33
+ ancleto init [--with-azure] [--agent <nombre>] [--tier <nivel>]
33
34
  Crea .ancletorc en el repositorio actual
34
- (Azure desactivado por defecto, agente: opencode)
35
+ (interactivo en TTY: banner + menu; Azure desactivado por defecto)
35
36
  ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
36
37
  ancleto discovery [--compress] [--include G] [--ignore G] [--token-budget N]
37
38
  Empaca el repo con Repomix y guarda estado
@@ -201,18 +202,31 @@ function askAgent() {
201
202
  })
202
203
  }
203
204
 
204
- async function resolveAgent(args, existing) {
205
- const ai = args.indexOf('--agent')
206
- if (ai >= 0) {
207
- const name = args[ai + 1]
208
- if (!SUPPORTED_AGENTS.includes(name)) {
209
- console.error(`ancleto: agente invalido: ${name} (${SUPPORTED_AGENTS.join('/')})`)
210
- process.exit(1)
211
- }
212
- return name
205
+ function scanAgentFlag(args) {
206
+ const name = flagValue(args, '--agent')
207
+ if (!name) return null
208
+ if (!SUPPORTED_AGENTS.includes(name)) {
209
+ console.error(`ancleto: agente invalido: ${name} (${SUPPORTED_AGENTS.join('/')})`)
210
+ process.exit(1)
211
+ }
212
+ return name
213
+ }
214
+
215
+ function scanTierFlag(args) {
216
+ const t = flagValue(args, '--tier')
217
+ if (!t) return null
218
+ if (!TIERS[t]) {
219
+ console.error(`ancleto: tier invalido: ${t} (normal|minimo|gratis)`)
220
+ process.exit(1)
213
221
  }
222
+ return t
223
+ }
224
+
225
+ async function resolveAgent(args, existing, allowAsk = Boolean(process.stdin.isTTY)) {
226
+ const flag = scanAgentFlag(args)
227
+ if (flag) return flag
214
228
  if (existing && SUPPORTED_AGENTS.includes(existing)) return existing
215
- if (process.stdin.isTTY) return askAgent()
229
+ if (allowAsk) return askAgent()
216
230
  return DEFAULT_AGENT
217
231
  }
218
232
 
@@ -354,12 +368,7 @@ async function install(args) {
354
368
  const withMcp = !args.includes('--no-mcp')
355
369
  let mcpMap = withMcp ? buildDefaultMcp() : {}
356
370
 
357
- const ti = args.indexOf('--tier')
358
- let tier = ti >= 0 ? args[ti + 1] : null
359
- if (tier && !TIERS[tier]) {
360
- console.error(`ancleto: tier invalido: ${tier} (normal|minimo|gratis)`)
361
- process.exit(1)
362
- }
371
+ let tier = scanTierFlag(args)
363
372
 
364
373
  if (project) {
365
374
  const dir = resolve(project)
@@ -446,17 +455,45 @@ async function upgradeCmd(args) {
446
455
  }
447
456
 
448
457
  async function initProject(args) {
449
- const withAzure = args.includes('--with-azure')
450
458
  const projectDir = process.cwd()
459
+ const withAzure = args.includes('--with-azure')
460
+ const agentFlag = scanAgentFlag(args)
461
+ const tierFlag = scanTierFlag(args)
451
462
  const existing = await readAncletorc(projectDir)
452
463
  const azure = existing?.azure ?? { enabled: false }
453
- if (withAzure) azure.enabled = true
454
464
  const discovery = existing?.discovery ?? { outputDir: 'docs/technical-discovery', exclude: [] }
455
- const agent = await resolveAgent(args, existing?.agent)
465
+ const tierFile = join(projectDir, '.opencode', '.ancleto-tier')
466
+
467
+ let agent, tier
468
+ const isInteractive = Boolean(process.stdout.isTTY) && (!agentFlag || !tierFlag)
469
+ if (isInteractive) {
470
+ await showBanner()
471
+ const agentIdx = Math.max(0, SUPPORTED_AGENTS.indexOf(existing?.agent))
472
+ const storedTier = (await exists(tierFile)) ? (await readFile(tierFile, 'utf8')).trim() : null
473
+ const tierIdx = Math.max(0, Object.keys(TIERS).indexOf(TIERS[storedTier] ? storedTier : ''))
474
+ agent = agentFlag || await selectOption('Agente/IDE', SUPPORTED_AGENTS, agentIdx)
475
+ tier = tierFlag || await selectOption('Tier de costo', Object.keys(TIERS), tierIdx)
476
+ if (withAzure) {
477
+ azure.enabled = true
478
+ } else {
479
+ azure.enabled = (await selectOption('Habilitar Azure DevOps', ['No', 'Si'], azure.enabled ? 1 : 0)) === 'Si'
480
+ }
481
+ stopBanner()
482
+ } else {
483
+ agent = await resolveAgent(args, existing?.agent, false)
484
+ tier = tierFlag
485
+ if (withAzure) azure.enabled = true
486
+ }
487
+
488
+ if (tier) {
489
+ await mkdir(join(projectDir, '.opencode'), { recursive: true })
490
+ await writeFile(tierFile, tier + '\n')
491
+ }
492
+
456
493
  const manifest = await writeManifest(projectDir, { azure, discovery, agent })
457
494
  await scaffoldOpenSpec(projectDir)
458
495
  if (azure.enabled) console.log(AZURE_MCP_NOTICE)
459
- console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'} (Agente: ${agent})`)
496
+ console.log(`ancleto: .ancletorc actualizado en ${projectDir} (v${manifest.version})${azure.enabled ? ' (Azure habilitado)' : ' (Azure desactivado)'} (Agente: ${agent})${tier ? ` (Tier: ${tier})` : ''}`)
460
497
  }
461
498
 
462
499
  const DEFAULT_IGNORES = ['node_modules', '.git', 'dist']
package/src/cli/ui.js ADDED
@@ -0,0 +1,181 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises'
2
+
3
+ const CYAN = '\x1b[36m'
4
+ const BOLD = '\x1b[1m'
5
+ const RESET = '\x1b[0m'
6
+
7
+ const CUP = [
8
+ ' █████████████████████████████████',
9
+ ' █████████████████████████████████████',
10
+ ' ██ ██ ███',
11
+ ' ██ ██ ██',
12
+ ' ███ ███ ███',
13
+ ' ███ █████████',
14
+ ' ███ ███',
15
+ ' ██████████████████████████████████████',
16
+ ' ███████ █████████',
17
+ ' ██████████████████████████████████',
18
+ ' ██████████████████████████████'
19
+ ]
20
+
21
+ const LOGO = [
22
+ ' ██████ ██ ██ ██████ ██ ███████ ████████ ██████ ',
23
+ '██ ██ ███ ██ ██ ██ ██ ██ ██ ██',
24
+ '████████ ██ █ ██ ██ ██ █████ ██ ██ ██',
25
+ '██ ██ ██ █ ██ ██ ██ ██ ██ ██ ██',
26
+ '██ ██ ██ ███ ██████ ███████ ███████ ██ ██████ '
27
+ ]
28
+
29
+ const ART = [...CUP, '', ...LOGO, '']
30
+ const TEXT_START = CUP.length + 1
31
+
32
+ const CUP_PALETTE = ['\x1b[38;5;94m', '\x1b[38;5;130m', '\x1b[38;5;172m', '\x1b[38;5;208m', '\x1b[38;5;214m', '\x1b[38;5;220m', '\x1b[38;5;230m']
33
+ const TEXT_PALETTE = ['\x1b[38;5;27m', '\x1b[38;5;33m', '\x1b[38;5;45m', '\x1b[38;5;51m', '\x1b[38;5;201m']
34
+
35
+ let activeBanner = null
36
+
37
+ function waveColorIndex(x, y, t, size) {
38
+ const v = Math.sin((x + y) * 0.15 + t)
39
+ return Math.round(((v + 1) / 2) * (size - 1))
40
+ }
41
+
42
+ function renderWaveLine(line, y, t, palette) {
43
+ let out = ''
44
+ let last = -1
45
+ for (let x = 0; x < line.length; x++) {
46
+ const ch = line[x]
47
+ if (ch === ' ') {
48
+ out += ' '
49
+ last = -1
50
+ continue
51
+ }
52
+ const idx = waveColorIndex(x, y, t, palette.length)
53
+ if (idx !== last) {
54
+ out += palette[idx]
55
+ last = idx
56
+ }
57
+ out += ch
58
+ }
59
+ return out + RESET
60
+ }
61
+
62
+ export function stopBanner() {
63
+ if (activeBanner) activeBanner.stop()
64
+ }
65
+
66
+ export function showBanner(delay = 80, duration = 20000) {
67
+ const out = process.stdout
68
+
69
+ if (!out.isTTY) {
70
+ for (const line of ART) out.write(line ? `${CYAN}${line}${RESET}\n` : '\n')
71
+ out.write('\n')
72
+ return
73
+ }
74
+
75
+ stopBanner()
76
+ const state = { below: 0, stopped: false, hidden: false }
77
+ const restoreCursor = () => {
78
+ if (state.hidden) {
79
+ out.write('\x1b[?25h')
80
+ state.hidden = false
81
+ }
82
+ }
83
+ const stop = () => {
84
+ if (state.stopped) return
85
+ state.stopped = true
86
+ process.removeListener('SIGINT', onSigint)
87
+ restoreCursor()
88
+ if (activeBanner === state) activeBanner = null
89
+ }
90
+ const onSigint = () => {
91
+ stop()
92
+ process.exit(0)
93
+ }
94
+ state.stop = stop
95
+ activeBanner = state
96
+
97
+ ;(async () => {
98
+ out.write('\x1b[?25l')
99
+ state.hidden = true
100
+ process.once('SIGINT', onSigint)
101
+ const deadline = Date.now() + duration
102
+ let f = 0
103
+ try {
104
+ while (!state.stopped && Date.now() < deadline) {
105
+ if (f > 0) out.write(`\x1b[${ART.length + state.below}A`)
106
+ for (let y = 0; y < ART.length; y++) {
107
+ const line = ART[y]
108
+ const palette = y >= TEXT_START ? TEXT_PALETTE : CUP_PALETTE
109
+ out.write(`\x1b[2K${line ? renderWaveLine(line, y, f * 0.6, palette) : ''}\n`)
110
+ }
111
+ if (state.below > 0) out.write(`\x1b[${state.below}B`)
112
+ if (state.stopped) break
113
+ f++
114
+ await sleep(delay)
115
+ }
116
+ } finally {
117
+ stop()
118
+ }
119
+ })()
120
+ }
121
+
122
+ export async function selectOption(message, options, initial = 0) {
123
+ const stdin = process.stdin
124
+ const out = process.stdout
125
+ const start = Math.min(Math.max(Number(initial) || 0, 0), options.length - 1)
126
+ if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') return options[start]
127
+
128
+ let index = start
129
+ let rendered = 0
130
+ const draw = () => {
131
+ if (rendered > 0) out.write(`\x1b[${rendered}A`)
132
+ let buf = ''
133
+ for (let i = 0; i < options.length; i++) {
134
+ const pointer = i === index ? `${CYAN}❯${RESET}` : ' '
135
+ const label = i === index ? `${BOLD}${options[i]}${RESET}` : options[i]
136
+ buf += `\x1b[2K ${pointer} ${label}\n`
137
+ }
138
+ out.write(buf)
139
+ rendered = options.length
140
+ }
141
+
142
+ const SIGINT = Symbol('SIGINT')
143
+ let resolveKey
144
+ const key = new Promise((resolve) => { resolveKey = resolve })
145
+ const onData = (chunk) => {
146
+ const s = chunk.toString('utf8')
147
+ if (s === '\u0003') {
148
+ stopBanner()
149
+ resolveKey(SIGINT)
150
+ } else if (s === '\u001b[A') {
151
+ index = (index - 1 + options.length) % options.length
152
+ draw()
153
+ } else if (s === '\u001b[B') {
154
+ index = (index + 1) % options.length
155
+ draw()
156
+ } else if (s === '\r' || s === '\n') {
157
+ resolveKey(options[index])
158
+ }
159
+ }
160
+
161
+ let result
162
+ try {
163
+ out.write(`${CYAN}?${RESET} ${BOLD}${message}${RESET}\n`)
164
+ stdin.setRawMode(true)
165
+ stdin.resume()
166
+ stdin.on('data', onData)
167
+ draw()
168
+ if (activeBanner) activeBanner.below += 1 + options.length
169
+ result = await key
170
+ out.write('\n')
171
+ if (activeBanner) activeBanner.below += 1
172
+ } catch {
173
+ result = undefined
174
+ } finally {
175
+ stdin.removeListener('data', onData)
176
+ try { stdin.setRawMode(false) } catch {}
177
+ stdin.pause()
178
+ }
179
+ if (result === SIGINT) process.exit(0)
180
+ return result === undefined ? options[start] : result
181
+ }