claude-connect 0.1.0

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.
@@ -0,0 +1,350 @@
1
+ import process from 'node:process';
2
+ import readline from 'node:readline';
3
+ import { colorize, colors, padRight, truncate, visibleWidth } from './theme.js';
4
+
5
+ const KEY_NAMES = new Set(['up', 'down', 'return', 'escape', 'backspace', 'tab']);
6
+ export const navigation = {
7
+ BACK: '__CLAUDE_CONNECT_BACK__',
8
+ EXIT: '__CLAUDE_CONNECT_EXIT__'
9
+ };
10
+
11
+ export function assertInteractiveTerminal() {
12
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
13
+ throw new Error('Este CLI requiere una terminal interactiva.');
14
+ }
15
+ }
16
+
17
+ export function openAppScreen() {
18
+ process.stdout.write('\x1b[?1049h\x1b[?25l');
19
+ }
20
+
21
+ export function closeAppScreen() {
22
+ process.stdout.write('\x1b[?25h\x1b[?1049l');
23
+ }
24
+
25
+ export function clearScreen() {
26
+ process.stdout.write('\x1b[2J\x1b[H');
27
+ }
28
+
29
+ export function renderScreen(lines) {
30
+ clearScreen();
31
+ process.stdout.write(`${lines.join('\n')}\n`);
32
+ }
33
+
34
+ function frameLine(content, width) {
35
+ return `│ ${padRight(truncate(content, width - 4), width - 4)} │`;
36
+ }
37
+
38
+ export function buildFrame({ eyebrow, title, subtitle, body = [], footer = [] }) {
39
+ const width = Math.min(88, Math.max(72, process.stdout.columns || 80));
40
+ const innerWidth = width - 4;
41
+ const lines = [];
42
+
43
+ lines.push(colorize(`╭${'─'.repeat(width - 2)}╮`, colors.accentSoft));
44
+ lines.push(frameLine(colorize(eyebrow, colors.bold, colors.accent), width));
45
+ lines.push(frameLine(colorize(title, colors.bold, colors.text), width));
46
+ lines.push(frameLine(colorize(subtitle, colors.soft), width));
47
+ lines.push(colorize(`├${'─'.repeat(width - 2)}┤`, colors.accentSoft));
48
+
49
+ for (const line of body) {
50
+ lines.push(frameLine(line, width));
51
+ }
52
+
53
+ if (footer.length > 0) {
54
+ lines.push(colorize(`├${'─'.repeat(width - 2)}┤`, colors.accentSoft));
55
+ for (const line of footer) {
56
+ lines.push(frameLine(line, width));
57
+ }
58
+ }
59
+
60
+ lines.push(colorize(`╰${'─'.repeat(width - 2)}╯`, colors.accentSoft));
61
+
62
+ return lines.map((line) => truncate(line, visibleWidth(line) > innerWidth + 4 ? width : width));
63
+ }
64
+
65
+ export function waitForAnyKey(message = 'Presiona una tecla para continuar.') {
66
+ return new Promise((resolve, reject) => {
67
+ readline.emitKeypressEvents(process.stdin);
68
+ process.stdin.setRawMode(true);
69
+ let escapePending = false;
70
+
71
+ const cleanup = () => {
72
+ process.stdin.removeListener('keypress', onKeypress);
73
+ process.stdin.setRawMode(false);
74
+ };
75
+
76
+ const onKeypress = (_input, key = {}) => {
77
+ if (key.ctrl && key.name === 'c') {
78
+ cleanup();
79
+ resolve(navigation.EXIT);
80
+ return;
81
+ }
82
+
83
+ if (key.name === 'escape') {
84
+ if (escapePending) {
85
+ cleanup();
86
+ resolve(navigation.EXIT);
87
+ return;
88
+ }
89
+
90
+ escapePending = true;
91
+ process.stdout.write(`\n${colorize('Presiona Esc otra vez para salir.', colors.warning)}\n`);
92
+ return;
93
+ }
94
+
95
+ cleanup();
96
+ resolve(message);
97
+ };
98
+
99
+ process.stdin.on('keypress', onKeypress);
100
+ });
101
+ }
102
+
103
+ export function selectFromList({
104
+ step,
105
+ totalSteps,
106
+ title,
107
+ subtitle,
108
+ items,
109
+ detailBuilder,
110
+ footerHint,
111
+ allowBack = false
112
+ }) {
113
+ return new Promise((resolve, reject) => {
114
+ const options = allowBack
115
+ ? [
116
+ ...items,
117
+ {
118
+ label: 'Volver',
119
+ description: 'Regresa a la pantalla anterior.',
120
+ value: navigation.BACK
121
+ }
122
+ ]
123
+ : items;
124
+ let selectedIndex = 0;
125
+ let escapePending = false;
126
+
127
+ readline.emitKeypressEvents(process.stdin);
128
+ process.stdin.setRawMode(true);
129
+
130
+ const cleanup = () => {
131
+ process.stdin.removeListener('keypress', onKeypress);
132
+ process.stdin.setRawMode(false);
133
+ };
134
+
135
+ const render = () => {
136
+ const selected = options[selectedIndex];
137
+ const detailLines = selected.value === navigation.BACK
138
+ ? ['Regresa a la pantalla anterior.']
139
+ : detailBuilder(selected);
140
+ const body = [
141
+ colorize(`Paso ${step}/${totalSteps}`, colors.warning),
142
+ '',
143
+ ...options.flatMap((item, index) => {
144
+ const active = index === selectedIndex;
145
+ const prefix = active
146
+ ? colorize('›', colors.bold, colors.accent)
147
+ : colorize(' ', colors.muted);
148
+ const label = active
149
+ ? colorize(item.label, colors.bold, colors.text)
150
+ : colorize(item.label, colors.text);
151
+ const description = active
152
+ ? colorize(item.description, colors.soft)
153
+ : colorize(item.description, colors.muted);
154
+ return [`${prefix} ${label}`, ` ${description}`];
155
+ }),
156
+ '',
157
+ colorize('Detalle', colors.bold, colors.accentSoft),
158
+ ...detailLines.map((line) => colorize(line, colors.soft))
159
+ ];
160
+
161
+ const footer = [
162
+ colorize(
163
+ escapePending
164
+ ? `Esc otra vez salir · Enter seleccionar${allowBack ? ' · Tab volver' : ''}`
165
+ : footerHint ?? `↑/↓ mover · Enter seleccionar${allowBack ? ' · Tab volver' : ''} · Esc salir`,
166
+ colors.dim,
167
+ escapePending ? colors.warning : colors.muted
168
+ )
169
+ ];
170
+
171
+ renderScreen(
172
+ buildFrame({
173
+ eyebrow: 'CLAUDE CONNECT',
174
+ title,
175
+ subtitle,
176
+ body,
177
+ footer
178
+ })
179
+ );
180
+ };
181
+
182
+ const onKeypress = (_input, key = {}) => {
183
+ if (key.ctrl && key.name === 'c') {
184
+ cleanup();
185
+ resolve(navigation.EXIT);
186
+ return;
187
+ }
188
+
189
+ if (escapePending && key.name !== 'escape') {
190
+ escapePending = false;
191
+ }
192
+
193
+ if (key.name === 'up') {
194
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
195
+ render();
196
+ return;
197
+ }
198
+
199
+ if (key.name === 'down') {
200
+ selectedIndex = (selectedIndex + 1) % options.length;
201
+ render();
202
+ return;
203
+ }
204
+
205
+ if (key.name === 'escape') {
206
+ if (escapePending) {
207
+ cleanup();
208
+ resolve(navigation.EXIT);
209
+ return;
210
+ }
211
+
212
+ escapePending = true;
213
+ render();
214
+ return;
215
+ }
216
+
217
+ if (key.name === 'tab' && allowBack) {
218
+ cleanup();
219
+ resolve(navigation.BACK);
220
+ return;
221
+ }
222
+
223
+ if (key.name === 'return') {
224
+ const selected = options[selectedIndex];
225
+ cleanup();
226
+ resolve(selected.value);
227
+ }
228
+ };
229
+
230
+ process.stdin.on('keypress', onKeypress);
231
+ render();
232
+ });
233
+ }
234
+
235
+ export function promptText({
236
+ step,
237
+ totalSteps,
238
+ title,
239
+ subtitle,
240
+ label,
241
+ defaultValue = '',
242
+ placeholder = '',
243
+ secret = false,
244
+ allowBack = false
245
+ }) {
246
+ return new Promise((resolve, reject) => {
247
+ let value = '';
248
+ let escapePending = false;
249
+
250
+ readline.emitKeypressEvents(process.stdin);
251
+ process.stdin.setRawMode(true);
252
+
253
+ const cleanup = () => {
254
+ process.stdin.removeListener('keypress', onKeypress);
255
+ process.stdin.setRawMode(false);
256
+ };
257
+
258
+ const render = () => {
259
+ const displayValue = value.length === 0
260
+ ? colorize(placeholder || 'Escribe aqui...', colors.muted)
261
+ : secret
262
+ ? colorize('•'.repeat(value.length), colors.text)
263
+ : colorize(value, colors.text);
264
+
265
+ const body = [
266
+ colorize(`Paso ${step}/${totalSteps}`, colors.warning),
267
+ '',
268
+ colorize(label, colors.bold, colors.text),
269
+ displayValue,
270
+ '',
271
+ colorize('Sugerencia', colors.bold, colors.accentSoft),
272
+ colorize(defaultValue ? `Enter usa el valor por defecto: ${defaultValue}` : 'Enter confirma el valor actual.', colors.soft)
273
+ ];
274
+
275
+ const footer = [
276
+ colorize(
277
+ escapePending
278
+ ? `Esc otra vez salir · Enter confirmar${allowBack ? ' · Tab volver' : ''}`
279
+ : `Escribe para editar · Backspace borrar · Enter confirmar${allowBack ? ' · Tab volver' : ''} · Esc salir`,
280
+ colors.dim,
281
+ escapePending ? colors.warning : colors.muted
282
+ )
283
+ ];
284
+
285
+ renderScreen(
286
+ buildFrame({
287
+ eyebrow: 'CLAUDE CONNECT',
288
+ title,
289
+ subtitle,
290
+ body,
291
+ footer
292
+ })
293
+ );
294
+ };
295
+
296
+ const onKeypress = (input = '', key = {}) => {
297
+ if (key.ctrl && key.name === 'c') {
298
+ cleanup();
299
+ resolve(navigation.EXIT);
300
+ return;
301
+ }
302
+
303
+ if (key.name === 'escape') {
304
+ if (escapePending) {
305
+ cleanup();
306
+ resolve(navigation.EXIT);
307
+ return;
308
+ }
309
+
310
+ escapePending = true;
311
+ render();
312
+ return;
313
+ }
314
+
315
+ if (escapePending) {
316
+ escapePending = false;
317
+ }
318
+
319
+ if (key.name === 'tab' && allowBack) {
320
+ cleanup();
321
+ resolve(navigation.BACK);
322
+ return;
323
+ }
324
+
325
+ if (key.name === 'return') {
326
+ cleanup();
327
+ resolve(value.trim() || defaultValue.trim());
328
+ return;
329
+ }
330
+
331
+ if (key.name === 'backspace') {
332
+ value = value.slice(0, -1);
333
+ render();
334
+ return;
335
+ }
336
+
337
+ if (KEY_NAMES.has(key.name)) {
338
+ return;
339
+ }
340
+
341
+ if (!key.ctrl && !key.meta && input) {
342
+ value += input;
343
+ render();
344
+ }
345
+ };
346
+
347
+ process.stdin.on('keypress', onKeypress);
348
+ render();
349
+ });
350
+ }
@@ -0,0 +1,44 @@
1
+ const RESET = '\x1b[0m';
2
+
3
+ export const colors = {
4
+ reset: RESET,
5
+ panel: '\x1b[48;2;12;18;30m',
6
+ surface: '\x1b[48;2;18;27;44m',
7
+ soft: '\x1b[38;2;148;163;184m',
8
+ text: '\x1b[38;2;226;232;240m',
9
+ muted: '\x1b[38;2;100;116;139m',
10
+ accent: '\x1b[38;2;56;189;248m',
11
+ accentSoft: '\x1b[38;2;125;211;252m',
12
+ accentBg: '\x1b[48;2;10;71;104m',
13
+ success: '\x1b[38;2;74;222;128m',
14
+ warning: '\x1b[38;2;251;191;36m',
15
+ danger: '\x1b[38;2;248;113;113m',
16
+ dim: '\x1b[2m',
17
+ bold: '\x1b[1m'
18
+ };
19
+
20
+ export function colorize(text, ...tokens) {
21
+ return `${tokens.join('')}${text}${RESET}`;
22
+ }
23
+
24
+ export function stripAnsi(value) {
25
+ return value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
26
+ }
27
+
28
+ export function visibleWidth(value) {
29
+ return stripAnsi(value).length;
30
+ }
31
+
32
+ export function padRight(value, width) {
33
+ const padding = Math.max(0, width - visibleWidth(value));
34
+ return `${value}${' '.repeat(padding)}`;
35
+ }
36
+
37
+ export function truncate(value, width) {
38
+ if (visibleWidth(value) <= width) {
39
+ return value;
40
+ }
41
+
42
+ const plain = stripAnsi(value);
43
+ return `${plain.slice(0, Math.max(0, width - 1))}…`;
44
+ }