@ancleto/spec 0.6.1 → 0.6.4

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.
Files changed (32) hide show
  1. package/README.md +184 -91
  2. package/agents/spec-writer.md +2 -2
  3. package/commands/{opsx-apply.md → cleto-apply.md} +4 -4
  4. package/commands/{opsx-archive.md → cleto-archive.md} +3 -3
  5. package/commands/{opsx-bulk-archive.md → cleto-bulk-archive.md} +1 -1
  6. package/commands/{opsx-continue.md → cleto-continue.md} +3 -3
  7. package/commands/{opsx-explore.md → cleto-explore.md} +1 -1
  8. package/commands/{opsx-ff.md → cleto-ff.md} +2 -2
  9. package/commands/{opsx-new.md → cleto-new.md} +4 -4
  10. package/commands/{opsx-onboard.md → cleto-onboard.md} +22 -22
  11. package/commands/{opsx-propose.md → cleto-propose.md} +3 -3
  12. package/commands/{opsx-recall.md → cleto-recall.md} +6 -6
  13. package/commands/{opsx-sync.md → cleto-sync.md} +1 -1
  14. package/commands/{opsx-verify.md → cleto-verify.md} +1 -1
  15. package/package.json +1 -1
  16. package/skills/{openspec-apply → ancleto-apply}/SKILL.md +2 -2
  17. package/skills/{openspec-archive → ancleto-archive}/SKILL.md +1 -1
  18. package/skills/{openspec-bulk-archive → ancleto-bulk-archive}/SKILL.md +1 -1
  19. package/skills/{openspec-continue → ancleto-continue}/SKILL.md +4 -4
  20. package/skills/{openspec-explore → ancleto-explore}/SKILL.md +1 -1
  21. package/skills/{openspec-ff → ancleto-ff}/SKILL.md +2 -2
  22. package/skills/{openspec-new → ancleto-new}/SKILL.md +2 -2
  23. package/skills/{openspec-onboard → ancleto-onboard}/SKILL.md +11 -11
  24. package/skills/{openspec-propose → ancleto-propose}/SKILL.md +4 -4
  25. package/skills/{openspec-recall → ancleto-recall}/SKILL.md +3 -3
  26. package/skills/{openspec-sync-specs → ancleto-sync-specs}/SKILL.md +1 -1
  27. package/skills/ancleto-upgrade/SKILL.md +2 -2
  28. package/skills/{openspec-verify → ancleto-verify}/SKILL.md +1 -1
  29. package/skills/{openspec-workflow → ancleto-workflow}/SKILL.md +17 -17
  30. package/skills/triage-clarifier/SKILL.md +1 -1
  31. package/src/cli/index.js +81 -34
  32. package/src/cli/ui.js +181 -0
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
+ }