@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 +1 -1
- package/src/cli/index.js +59 -22
- package/src/cli/ui.js +181 -0
package/package.json
CHANGED
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
|
|
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
|
-
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|