singleton-pipeline 0.4.0-beta.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,542 @@
1
+ import blessed from 'blessed';
2
+
3
+ // ── Pastel theme ────────────────────────────────────────────────
4
+ export const C = {
5
+ violet: '#C084FC', // accent principal
6
+ pink: '#F9A8D4', // secondaire
7
+ blue: '#93C5FD', // tertiaire
8
+ mint: '#6EE7B7', // success
9
+ peach: '#FDBA74', // warning
10
+ salmon: '#FCA5A5', // erreur
11
+ dimV: '#b58eb8', // violet clair (texte muted)
12
+ line: '#4A4060', // separators
13
+ ghost: '#797C81', // gris discret lisible sur fond sombre
14
+ };
15
+
16
+ export function createShell() {
17
+ const screen = blessed.screen({ smartCSR: true, title: 'Singleton' });
18
+ const inputHints = [
19
+ '/help to start',
20
+ '/scan to scan agents',
21
+ '/new to create an agent',
22
+ '/run to execute a pipeline',
23
+ '/commit-last to commit the last run',
24
+ ];
25
+
26
+ // ── Normal mode ────────────────────────────────────────────────
27
+ const content = blessed.log({
28
+ top: 0, left: 0,
29
+ width: '100%', height: '100%-4',
30
+ scrollable: true, alwaysScroll: true,
31
+ tags: true,
32
+ padding: { left: 2, top: 1, right: 2 },
33
+ scrollbar: {
34
+ ch: '│',
35
+ style: { fg: C.line }
36
+ }
37
+ });
38
+
39
+ // ── Pipeline mode (single column) ──────────────────────────────
40
+ const pipelineLog = blessed.log({
41
+ top: 0, left: 0,
42
+ width: '100%', height: '100%-10',
43
+ tags: true,
44
+ hidden: true,
45
+ scrollable: true,
46
+ alwaysScroll: true,
47
+ padding: { left: 2, top: 1, right: 2 },
48
+ scrollbar: {
49
+ ch: '│',
50
+ style: { fg: C.line }
51
+ }
52
+ });
53
+
54
+ const pipelineSep = blessed.line({
55
+ orientation: 'horizontal',
56
+ bottom: 8, left: 0, width: '100%',
57
+ style: { fg: C.line }
58
+ });
59
+
60
+ const pipelineStatus = blessed.box({
61
+ bottom: 4, left: 0,
62
+ width: '100%', height: 4,
63
+ tags: true,
64
+ hidden: true,
65
+ padding: { left: 2, right: 2 }
66
+ });
67
+
68
+ // ── Shell bar (toujours visible) ───────────────────────────────
69
+ const sep1 = blessed.line({
70
+ orientation: 'horizontal',
71
+ bottom: 3, left: 0, width: '100%',
72
+ style: { fg: C.line }
73
+ });
74
+
75
+ const suggestBox = blessed.box({
76
+ bottom: 4, left: 0,
77
+ width: '100%', height: 5,
78
+ tags: true,
79
+ hidden: true,
80
+ padding: { left: 2, right: 2 },
81
+ style: { bg: 'default' }
82
+ });
83
+
84
+ const promptBox = blessed.box({
85
+ bottom: 2, left: 0,
86
+ width: '100%', height: 1,
87
+ padding: { left: 2 }, tags: true
88
+ });
89
+
90
+ const sep2 = blessed.line({
91
+ orientation: 'horizontal',
92
+ bottom: 1, left: 0, width: '100%',
93
+ style: { fg: C.line }
94
+ });
95
+
96
+ const footerLeftBox = blessed.box({
97
+ bottom: 0, left: 0,
98
+ width: '70%', height: 1,
99
+ padding: { left: 2 },
100
+ tags: true
101
+ });
102
+
103
+ const footerRightBox = blessed.box({
104
+ bottom: 0, right: 2,
105
+ width: '30%', height: 1,
106
+ align: 'right',
107
+ tags: true
108
+ });
109
+
110
+ const footerCenterBox = blessed.box({
111
+ bottom: 0, left: '30%',
112
+ width: '40%', height: 1,
113
+ align: 'center',
114
+ tags: true
115
+ });
116
+
117
+ screen.append(content);
118
+ screen.append(pipelineLog);
119
+ screen.append(pipelineSep);
120
+ screen.append(pipelineStatus);
121
+ screen.append(suggestBox);
122
+ screen.append(sep1);
123
+ screen.append(promptBox);
124
+ screen.append(sep2);
125
+ screen.append(footerLeftBox);
126
+ screen.append(footerRightBox);
127
+ screen.append(footerCenterBox);
128
+
129
+ pipelineSep.hide();
130
+ pipelineStatus.hide();
131
+
132
+ // ── Input state ─────────────────────────────────────────────────
133
+ let buffer = '';
134
+ let inputEnabled = true;
135
+ let onSubmit = null;
136
+ let promptMode = null; // { resolve, message }
137
+ let completer = null;
138
+ let suggestions = [];
139
+ let suggestIndex = 0;
140
+ let completeSeq = 0;
141
+ let pipelineMode = false;
142
+ let history = [];
143
+ let historyIndex = -1;
144
+ let draftBuffer = '';
145
+ let hintIndex = 0;
146
+ let footerLeft = '';
147
+ let footerRight = '';
148
+ let footerCenter = '';
149
+ function stripTags(s) {
150
+ return String(s || '').replace(/\{[^}]+\}/g, '');
151
+ }
152
+
153
+ function hideSuggestions() {
154
+ suggestions = [];
155
+ suggestIndex = 0;
156
+ suggestBox.hide();
157
+ }
158
+
159
+ function renderSuggestions() {
160
+ if (!suggestions.length || promptMode) {
161
+ suggestBox.hide();
162
+ return;
163
+ }
164
+
165
+ const maxItems = Math.min(5, suggestions.length);
166
+ const start = Math.min(
167
+ Math.max(0, suggestIndex - maxItems + 1),
168
+ Math.max(0, suggestions.length - maxItems)
169
+ );
170
+ const width = Math.max(40, (screen.width ?? 100) - 6);
171
+ const lines = suggestions.slice(start, start + maxItems).map((item, idx) => {
172
+ const active = start + idx === suggestIndex;
173
+ const marker = active ? `{${C.violet}-fg}›{/}` : `{${C.ghost}-fg} {/}`;
174
+ const label = active
175
+ ? `{${C.pink}-fg}${item.label}{/}`
176
+ : `{${C.dimV}-fg}${item.label}{/}`;
177
+ const desc = item.description ? ` {${C.ghost}-fg}${item.description}{/}` : '';
178
+ const visible = stripTags(`${marker} ${item.label}${item.description ? ` ${item.description}` : ''}`);
179
+ const clippedDesc = visible.length > width ? '' : desc;
180
+ return `${marker} ${label}${clippedDesc}`;
181
+ });
182
+
183
+ suggestBox.setContent(lines.join('\n'));
184
+ suggestBox.show();
185
+ }
186
+
187
+ async function refreshSuggestions({ applySingle = false } = {}) {
188
+ if (!completer || promptMode) return false;
189
+
190
+ const seq = ++completeSeq;
191
+ const result = await completer({ buffer, cursor: buffer.length });
192
+ if (seq !== completeSeq) return false;
193
+
194
+ suggestions = Array.isArray(result)
195
+ ? result.filter((s) => s && typeof s.value === 'string' && typeof s.label === 'string')
196
+ : [];
197
+ suggestIndex = 0;
198
+
199
+ if (applySingle && suggestions.length === 1) {
200
+ applySuggestion(suggestions[0]);
201
+ return true;
202
+ }
203
+
204
+ renderSuggestions();
205
+ screen.render();
206
+ return suggestions.length > 0;
207
+ }
208
+
209
+ function applySuggestion(item = suggestions[suggestIndex]) {
210
+ if (!item) return false;
211
+ buffer = item.value;
212
+ hideSuggestions();
213
+ updatePrompt();
214
+ return true;
215
+ }
216
+
217
+ function updatePrompt() {
218
+ if (promptMode) {
219
+ const message = String(promptMode.message || '');
220
+ const renderedMessage = message.includes('{')
221
+ ? message
222
+ : `{${C.dimV}-fg}${message}{/}`;
223
+ const marker = message.includes('Debug action')
224
+ ? ''
225
+ : `{${C.pink}-fg}?{/} `;
226
+ promptBox.setContent(
227
+ `${marker}${renderedMessage} {${C.dimV}-fg}›{/} ${buffer}{${C.violet}-fg}▌{/}`
228
+ );
229
+ } else {
230
+ if (buffer) {
231
+ promptBox.setContent(`{${C.dimV}-fg}›{/} ${buffer}{${C.violet}-fg}▌{/}`);
232
+ } else {
233
+ const hint = history.length === 0 ? inputHints[0] : inputHints[hintIndex];
234
+ promptBox.setContent(`{${C.dimV}-fg}›{/} {${C.violet}-fg}▌{/}{#797C81-fg}${hint}{/}`);
235
+ }
236
+ }
237
+ screen.render();
238
+ }
239
+
240
+ function renderFooter() {
241
+ footerLeftBox.setContent(footerLeft ? `{${C.dimV}-fg}${String(footerLeft)}{/}` : '');
242
+ footerRightBox.setContent(footerRight ? `{${C.dimV}-fg}${String(footerRight)}{/}` : '');
243
+ footerCenterBox.setContent(footerCenter || '');
244
+ }
245
+
246
+ function setFooter(left = '', right = '') {
247
+ footerLeft = left;
248
+ footerRight = right;
249
+ renderFooter();
250
+ screen.render();
251
+ }
252
+
253
+ function setFooterCenter(text = '') {
254
+ footerCenter = text;
255
+ renderFooter();
256
+ screen.render();
257
+ }
258
+
259
+ screen.on('resize', () => {
260
+ renderSuggestions();
261
+ screen.render();
262
+ });
263
+
264
+ function resetHistoryNav() {
265
+ historyIndex = -1;
266
+ draftBuffer = '';
267
+ }
268
+
269
+ const hintTicker = setInterval(() => {
270
+ if (promptMode || pipelineMode || buffer || history.length === 0) return;
271
+ hintIndex = (hintIndex + 1) % inputHints.length;
272
+ updatePrompt();
273
+ }, 3000);
274
+
275
+ screen.on('keypress', async (ch, key) => {
276
+ if (key.full === 'C-c') {
277
+ log(`{${C.dimV}-fg}See you soon.{/}`);
278
+ screen.render();
279
+ setTimeout(() => { screen.destroy(); process.exit(0); }, 200);
280
+ }
281
+
282
+ if (pipelineMode) {
283
+ if (key.name === 'up') {
284
+ pipelineLog.scroll(-1);
285
+ screen.render();
286
+ return;
287
+ }
288
+ if (key.name === 'down') {
289
+ pipelineLog.scroll(1);
290
+ screen.render();
291
+ return;
292
+ }
293
+ if (key.name === 'pageup') {
294
+ pipelineLog.scroll(-(pipelineLog.height - 2));
295
+ screen.render();
296
+ return;
297
+ }
298
+ if (key.name === 'pagedown') {
299
+ pipelineLog.scroll(pipelineLog.height - 2);
300
+ screen.render();
301
+ return;
302
+ }
303
+ if (key.name === 'home') {
304
+ pipelineLog.setScroll(0);
305
+ screen.render();
306
+ return;
307
+ }
308
+ if (key.name === 'end') {
309
+ pipelineLog.setScrollPerc(100);
310
+ screen.render();
311
+ return;
312
+ }
313
+ }
314
+
315
+ if (!pipelineMode && !promptMode && !suggestions.length) {
316
+ if (buffer === '' && key.name === 'up') {
317
+ content.scroll(-1);
318
+ screen.render();
319
+ return;
320
+ }
321
+ if (buffer === '' && key.name === 'down') {
322
+ content.scroll(1);
323
+ screen.render();
324
+ return;
325
+ }
326
+ if (key.name === 'pageup') {
327
+ content.scroll(-(content.height - 2));
328
+ screen.render();
329
+ return;
330
+ }
331
+ if (key.name === 'pagedown') {
332
+ content.scroll(content.height - 2);
333
+ screen.render();
334
+ return;
335
+ }
336
+ if (key.name === 'home') {
337
+ content.setScroll(0);
338
+ screen.render();
339
+ return;
340
+ }
341
+ if (key.name === 'end') {
342
+ content.setScrollPerc(100);
343
+ screen.render();
344
+ return;
345
+ }
346
+ }
347
+
348
+ if (!inputEnabled && !promptMode) return;
349
+
350
+ if (promptMode && key.name === 'escape') {
351
+ const { resolve, message } = promptMode;
352
+ promptMode = null;
353
+ buffer = '';
354
+ log(`{${C.ghost}-fg}↩ cancelled{/} {${C.dimV}-fg}${message}{/}`);
355
+ updatePrompt();
356
+ resolve('__SINGLETON_ESC__');
357
+ return;
358
+ }
359
+
360
+ if (!promptMode && key.name === 'tab') {
361
+ if (suggestions.length > 1) {
362
+ suggestIndex = (suggestIndex + 1) % suggestions.length;
363
+ renderSuggestions();
364
+ screen.render();
365
+ return;
366
+ }
367
+ await refreshSuggestions({ applySingle: true });
368
+ return;
369
+ }
370
+
371
+ if (!promptMode && suggestions.length && (key.name === 'down' || key.name === 'up')) {
372
+ const dir = key.name === 'down' ? 1 : -1;
373
+ suggestIndex = (suggestIndex + dir + suggestions.length) % suggestions.length;
374
+ renderSuggestions();
375
+ screen.render();
376
+ return;
377
+ }
378
+
379
+ if (!promptMode && suggestions.length && key.name === 'escape') {
380
+ hideSuggestions();
381
+ updatePrompt();
382
+ return;
383
+ }
384
+
385
+ if (!promptMode && suggestions.length && (key.name === 'right' || key.name === 'enter' || key.name === 'return')) {
386
+ applySuggestion();
387
+ return;
388
+ }
389
+
390
+ if (!promptMode && !suggestions.length && (key.full === 'C-p' || key.full === 'C-n')) {
391
+ if (history.length === 0) return;
392
+
393
+ if (key.full === 'C-p') {
394
+ if (historyIndex === -1) {
395
+ draftBuffer = buffer;
396
+ historyIndex = history.length - 1;
397
+ } else if (historyIndex > 0) {
398
+ historyIndex -= 1;
399
+ }
400
+ buffer = history[historyIndex] || '';
401
+ } else {
402
+ if (historyIndex === -1) return;
403
+ if (historyIndex < history.length - 1) {
404
+ historyIndex += 1;
405
+ buffer = history[historyIndex] || '';
406
+ } else {
407
+ historyIndex = -1;
408
+ buffer = draftBuffer;
409
+ }
410
+ }
411
+
412
+ updatePrompt();
413
+ return;
414
+ }
415
+
416
+ if (key.name === 'enter' || key.name === 'return') {
417
+ const value = buffer.trim();
418
+ buffer = '';
419
+ hideSuggestions();
420
+ if (promptMode) {
421
+ const { resolve, message } = promptMode;
422
+ promptMode = null;
423
+ log(`{${C.pink}-fg}?{/} {${C.dimV}-fg}${message}{/} ${value}`);
424
+ updatePrompt();
425
+ resolve(value);
426
+ } else {
427
+ updatePrompt();
428
+ if (value) {
429
+ if (history[history.length - 1] !== value) history.push(value);
430
+ if (history.length > 200) history = history.slice(-200);
431
+ resetHistoryNav();
432
+ if (onSubmit) onSubmit(value);
433
+ } else {
434
+ resetHistoryNav();
435
+ }
436
+ }
437
+ } else if (key.name === 'backspace') {
438
+ buffer = buffer.slice(0, -1);
439
+ hideSuggestions();
440
+ resetHistoryNav();
441
+ updatePrompt();
442
+ } else if (ch && !key.ctrl && !key.meta) {
443
+ buffer += ch;
444
+ hideSuggestions();
445
+ resetHistoryNav();
446
+ updatePrompt();
447
+ }
448
+ });
449
+
450
+ function log(text) {
451
+ content.log(text);
452
+ screen.render();
453
+ }
454
+
455
+ updatePrompt();
456
+
457
+ return {
458
+ log,
459
+ logMuted(text) { log(`{${C.dimV}-fg}${text}{/}`); },
460
+ logAccent(text) { log(`{${C.violet}-fg}${text}{/}`); },
461
+ setFooter,
462
+ setFooterCenter,
463
+
464
+ clear() { content.setContent(''); screen.render(); },
465
+ onCommand(fn) { onSubmit = fn; },
466
+ setCompleter(fn) { completer = fn; },
467
+
468
+ // Shimmer animation overlay (e.g. for "Welcome back")
469
+ createShimmer(text, top, left) {
470
+ const box = blessed.box({
471
+ top, left,
472
+ width: text.length,
473
+ height: 1,
474
+ tags: true
475
+ });
476
+ screen.append(box);
477
+
478
+ let peak = -4;
479
+ function render() {
480
+ const content = text.split('').map((ch, i) => {
481
+ const dist = Math.abs(i - peak);
482
+ let color;
483
+ if (dist === 0) color = '#FFFFFF';
484
+ else if (dist === 1) color = '#EDD9FF';
485
+ else if (dist === 2) color = '#D4B0FE';
486
+ else color = C.violet;
487
+ return `{${color}-fg}{bold}${ch}{/}`;
488
+ }).join('');
489
+ box.setContent(content);
490
+ screen.render();
491
+ peak++;
492
+ if (peak > text.length + 4) peak = -4;
493
+ }
494
+
495
+ render();
496
+ const iv = setInterval(render, 80);
497
+ return () => { clearInterval(iv); screen.remove(box); screen.render(); };
498
+ },
499
+ disableInput() { inputEnabled = false; hideSuggestions(); resetHistoryNav(); screen.render(); },
500
+ enableInput() { inputEnabled = true; buffer = ''; hideSuggestions(); resetHistoryNav(); updatePrompt(); },
501
+
502
+ prompt(message) {
503
+ return new Promise((resolve) => {
504
+ promptMode = { resolve, message };
505
+ buffer = '';
506
+ hideSuggestions();
507
+ resetHistoryNav();
508
+ updatePrompt();
509
+ });
510
+ },
511
+
512
+ pipelineWidgets: { screen, logPanel: pipelineLog, statusBox: pipelineStatus },
513
+
514
+ enterPipelineMode() {
515
+ pipelineMode = true;
516
+ content.hide();
517
+ pipelineLog.setContent('');
518
+ pipelineStatus.setContent('');
519
+ pipelineLog.show();
520
+ pipelineSep.show();
521
+ pipelineStatus.show();
522
+ promptBox.setContent(`{${C.dimV}-fg}scroll: ↑ ↓ pgup pgdn home end{/}`);
523
+ screen.render();
524
+ },
525
+
526
+ exitPipelineMode() {
527
+ pipelineMode = false;
528
+ pipelineLog.hide();
529
+ pipelineSep.hide();
530
+ pipelineStatus.hide();
531
+ content.show();
532
+ updatePrompt();
533
+ screen.render();
534
+ },
535
+
536
+ screen,
537
+ destroy() {
538
+ clearInterval(hintTicker);
539
+ screen.destroy();
540
+ }
541
+ };
542
+ }
@@ -0,0 +1,46 @@
1
+ import chalk from 'chalk';
2
+
3
+ // ============================================================
4
+ // CLI theme — semantic tokens. Change palette here.
5
+ // ============================================================
6
+
7
+ const accent = chalk.hex('#AF87FF');
8
+
9
+ // -- Semantic styles ----------------------------------------
10
+ export const style = {
11
+ // Informational
12
+ title: (s) => accent.bold(s),
13
+ heading: (s) => chalk.bold(s),
14
+ muted: (s) => chalk.hex('#676498')(s),
15
+ dim: (s) => chalk.gray.italic(s),
16
+
17
+ // Status
18
+ success: (s) => chalk.green(s),
19
+ warn: (s) => chalk.yellow(s),
20
+ error: (s) => chalk.red(s),
21
+ info: (s) => accent(s),
22
+
23
+ // Data accents
24
+ accent: (s) => accent(s),
25
+ id: (s) => accent.bold(s),
26
+ path: (s) => chalk.gray(s),
27
+ value: (s) => chalk.white(s),
28
+ code: (s) => chalk.magenta(s)
29
+ };
30
+
31
+ // -- Semantic markers (prefix glyphs) -----------------------
32
+ export const mark = {
33
+ success: '✓',
34
+ error: '✕',
35
+ warn: '!',
36
+ info: '›',
37
+ bullet: '·'
38
+ };
39
+
40
+ // -- Pre-composed line helpers ------------------------------
41
+ export const line = {
42
+ success: (msg) => `${style.success(mark.success)} ${msg}`,
43
+ error: (msg) => `${style.error(mark.error)} ${msg}`,
44
+ warn: (msg) => `${style.warn(mark.warn)} ${msg}`,
45
+ info: (msg) => `${style.info(mark.info)} ${msg}`
46
+ };