@msn-control/liftoff 0.4.0 → 0.4.1

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/dist/terminal.js CHANGED
@@ -1,40 +1,146 @@
1
1
  import picocolors, { createColors } from 'picocolors';
2
+ export const TERMINAL_LAYOUT = {
3
+ compactColumns: 64,
4
+ fullColumns: 96,
5
+ maximumContentColumns: 92,
6
+ minimumColumns: 20,
7
+ indent: 2,
8
+ sectionSpacing: 1
9
+ };
10
+ export const TERMINAL_GLYPHS = {
11
+ topLeft: '┌',
12
+ topRight: '┐',
13
+ bottomLeft: '└',
14
+ bottomRight: '┘',
15
+ horizontal: '─',
16
+ vertical: '│',
17
+ leftJunction: '├',
18
+ rightJunction: '┤',
19
+ bullet: '•',
20
+ choice: '›',
21
+ selected: '●',
22
+ unselected: '○'
23
+ };
2
24
  export const LIFTOFF_WORDMARK = [
3
- ' _ ___ _____ _____ ___ _____ _____',
4
- '| | |_ _| ___|_ _/ _ \\| ___| ___|',
5
- '| | | || |_ | || | | | |_ | |_',
6
- '| |___ | || _| | || |_| | _| | _|',
7
- '|_____|___|_| |_| \\___/|_| |_|'
25
+ '██╗ ██╗███████╗████████╗ ██████╗ ███████╗███████╗',
26
+ '██║ ██║██╔════╝╚══██╔══╝██╔═══██╗██╔════╝██╔════╝',
27
+ '██║ ██║█████╗ ██║ ██║ ██║█████╗ █████╗',
28
+ '██║ ██║██╔══╝ ██║ ██║ ██║██╔══╝ ██╔══╝',
29
+ '███████╗██║██║ ██║ ╚██████╔╝██║ ██║',
30
+ '╚══════╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝'
31
+ ];
32
+ export const LIFTOFF_COMPACT_WORDMARK = [
33
+ '╻ ╻┏━╸╺┳╸┏━┓┏━╸┏━╸',
34
+ '┃ ┃┣╸ ┃ ┃ ┃┣╸ ┣╸',
35
+ '┗━╸╹╹ ╹ ┗━┛╹ ╹'
8
36
  ];
9
37
  const ANSI_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
38
+ const ANSI_ANY_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/;
39
+ const ANSI_AT_START_PATTERN = /^\u001B\[[0-?]*[ -/]*[@-~]/;
40
+ export function stripAnsi(value) {
41
+ return value.replace(ANSI_PATTERN, '');
42
+ }
43
+ function codePointWidth(character) {
44
+ const codePoint = character.codePointAt(0);
45
+ if (codePoint === undefined || codePoint === 0) {
46
+ return 0;
47
+ }
48
+ if (codePoint < 32 ||
49
+ codePoint >= 0x7f && codePoint < 0xa0 ||
50
+ codePoint >= 0x300 && codePoint <= 0x36f ||
51
+ codePoint >= 0x1ab0 && codePoint <= 0x1aff ||
52
+ codePoint >= 0x1dc0 && codePoint <= 0x1dff ||
53
+ codePoint >= 0x20d0 && codePoint <= 0x20ff ||
54
+ codePoint >= 0xfe20 && codePoint <= 0xfe2f ||
55
+ codePoint >= 0xfe00 && codePoint <= 0xfe0f ||
56
+ codePoint >= 0x1f3fb && codePoint <= 0x1f3ff) {
57
+ return 0;
58
+ }
59
+ if (codePoint >= 0x1100 && (codePoint <= 0x115f ||
60
+ codePoint === 0x2329 ||
61
+ codePoint === 0x232a ||
62
+ codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f ||
63
+ codePoint >= 0xac00 && codePoint <= 0xd7a3 ||
64
+ codePoint >= 0xf900 && codePoint <= 0xfaff ||
65
+ codePoint >= 0xfe10 && codePoint <= 0xfe19 ||
66
+ codePoint >= 0xfe30 && codePoint <= 0xfe6f ||
67
+ codePoint >= 0xff00 && codePoint <= 0xff60 ||
68
+ codePoint >= 0xffe0 && codePoint <= 0xffe6 ||
69
+ codePoint >= 0x1f300 && codePoint <= 0x1faff ||
70
+ codePoint >= 0x20000 && codePoint <= 0x3fffd)) {
71
+ return 2;
72
+ }
73
+ return 1;
74
+ }
10
75
  export function visibleLength(value) {
11
- return value.replace(ANSI_PATTERN, '').length;
76
+ return Array.from(stripAnsi(value)).reduce((total, character) => total + (character === '\t' ? 4 : codePointWidth(character)), 0);
12
77
  }
13
- function padVisible(value, width) {
78
+ export function padVisible(value, width) {
14
79
  return `${value}${' '.repeat(Math.max(0, width - visibleLength(value)))}`;
15
80
  }
16
- function wrapLine(value, width) {
17
- if (value.length <= width) {
81
+ function takeVisible(value, width) {
82
+ let output = '';
83
+ let used = 0;
84
+ for (const character of Array.from(value)) {
85
+ const characterWidth = codePointWidth(character);
86
+ if (used + characterWidth > width) {
87
+ break;
88
+ }
89
+ output += character;
90
+ used += characterWidth;
91
+ }
92
+ return output;
93
+ }
94
+ function truncateVisible(value, width) {
95
+ const plain = stripAnsi(value);
96
+ if (visibleLength(plain) <= width) {
97
+ return plain;
98
+ }
99
+ if (width <= 1) {
100
+ return takeVisible(plain, width);
101
+ }
102
+ return `${takeVisible(plain, width - 1)}…`;
103
+ }
104
+ function splitLongWord(word, width) {
105
+ const chunks = [];
106
+ let remaining = word;
107
+ while (remaining) {
108
+ const chunk = takeVisible(remaining, width);
109
+ if (!chunk) {
110
+ break;
111
+ }
112
+ chunks.push(chunk);
113
+ remaining = remaining.slice(chunk.length);
114
+ }
115
+ return chunks.length > 0 ? chunks : [''];
116
+ }
117
+ function wrapPlainLine(value, width, breakLongWords) {
118
+ if (visibleLength(value) <= width) {
18
119
  return [value];
19
120
  }
20
- const words = value.split(/\s+/);
121
+ const words = value.trim().split(/\s+/);
21
122
  const lines = [];
22
123
  let current = '';
23
124
  for (const word of words) {
24
- if (word.length > width) {
125
+ if (visibleLength(word) > width) {
25
126
  if (current) {
26
127
  lines.push(current);
27
128
  current = '';
28
129
  }
29
- for (let index = 0; index < word.length; index += width) {
30
- lines.push(word.slice(index, index + width));
130
+ if (breakLongWords) {
131
+ const chunks = splitLongWord(word, width);
132
+ lines.push(...chunks.slice(0, -1));
133
+ current = chunks.at(-1) ?? '';
134
+ }
135
+ else {
136
+ current = word;
31
137
  }
32
138
  continue;
33
139
  }
34
140
  if (!current) {
35
141
  current = word;
36
142
  }
37
- else if (current.length + word.length + 1 <= width) {
143
+ else if (visibleLength(`${current} ${word}`) <= width) {
38
144
  current += ` ${word}`;
39
145
  }
40
146
  else {
@@ -42,10 +148,68 @@ function wrapLine(value, width) {
42
148
  current = word;
43
149
  }
44
150
  }
45
- if (current) {
151
+ if (current || lines.length === 0) {
46
152
  lines.push(current);
47
153
  }
48
- return lines.length > 0 ? lines : [''];
154
+ return lines;
155
+ }
156
+ function wrapStyledLine(value, width) {
157
+ const lines = [];
158
+ let line = '';
159
+ let used = 0;
160
+ let index = 0;
161
+ while (index < value.length) {
162
+ const ansi = value.slice(index).match(ANSI_AT_START_PATTERN)?.[0];
163
+ if (ansi) {
164
+ line += ansi;
165
+ index += ansi.length;
166
+ continue;
167
+ }
168
+ const codePoint = value.codePointAt(index);
169
+ if (codePoint === undefined) {
170
+ break;
171
+ }
172
+ const character = String.fromCodePoint(codePoint);
173
+ const characterWidth = character === '\t' ? 4 : codePointWidth(character);
174
+ if (used > 0 && used + characterWidth > width) {
175
+ lines.push(line.trimEnd());
176
+ line = '';
177
+ used = 0;
178
+ if (/\s/.test(character)) {
179
+ index += character.length;
180
+ continue;
181
+ }
182
+ }
183
+ line += character;
184
+ used += characterWidth;
185
+ index += character.length;
186
+ }
187
+ if (line || lines.length === 0) {
188
+ lines.push(line.trimEnd());
189
+ }
190
+ return lines;
191
+ }
192
+ export function wrapVisible(value, width, breakLongWords = false) {
193
+ const safeWidth = Math.max(1, width);
194
+ return value.split(/\r?\n/).flatMap((line) => ANSI_ANY_PATTERN.test(line)
195
+ ? wrapStyledLine(line, safeWidth)
196
+ : wrapPlainLine(line, safeWidth, breakLongWords));
197
+ }
198
+ function fitColumnWidths(natural, available) {
199
+ const widths = natural.map((width) => Math.max(4, width));
200
+ while (widths.reduce((total, width) => total + width, 0) > available) {
201
+ let largestIndex = -1;
202
+ for (let index = 0; index < widths.length; index += 1) {
203
+ if (widths[index] > 4 && (largestIndex === -1 || widths[index] > widths[largestIndex])) {
204
+ largestIndex = index;
205
+ }
206
+ }
207
+ if (largestIndex === -1) {
208
+ break;
209
+ }
210
+ widths[largestIndex] -= 1;
211
+ }
212
+ return widths;
49
213
  }
50
214
  export class TerminalRenderer {
51
215
  options;
@@ -58,20 +222,20 @@ export class TerminalRenderer {
58
222
  this.options = options;
59
223
  const stream = options.stream;
60
224
  const env = options.env ?? process.env;
61
- this.columns = Math.max(20, options.columns ?? stream.columns ?? 80);
225
+ this.columns = Math.max(TERMINAL_LAYOUT.minimumColumns, options.columns ?? stream.columns ?? 80);
62
226
  this.jsonMode = options.json ?? false;
63
227
  const tty = stream.isTTY === true;
64
228
  const noColor = Object.hasOwn(env, 'NO_COLOR');
65
229
  this.colorEnabled = !this.jsonMode && !options.snapshot && !noColor &&
66
- (options.color ?? (tty && !noColor && picocolors.isColorSupported));
230
+ (options.color ?? (tty && picocolors.isColorSupported));
67
231
  this.colors = createColors(this.colorEnabled);
68
- this.layout = this.jsonMode || (!tty && !options.snapshot)
232
+ this.layout = options.layout ?? (this.jsonMode || (!tty && !options.snapshot)
69
233
  ? 'plain'
70
- : this.columns >= 96
234
+ : this.columns >= TERMINAL_LAYOUT.fullColumns
71
235
  ? 'full'
72
- : this.columns >= 64
236
+ : this.columns >= TERMINAL_LAYOUT.compactColumns
73
237
  ? 'compact'
74
- : 'plain';
238
+ : 'plain');
75
239
  }
76
240
  write(value) {
77
241
  if (value) {
@@ -81,102 +245,354 @@ export class TerminalRenderer {
81
245
  json(value) {
82
246
  return `${JSON.stringify(value, null, 2)}\n`;
83
247
  }
248
+ style(kind, value) {
249
+ switch (kind) {
250
+ case 'brand':
251
+ case 'info':
252
+ return this.colors.cyan(value);
253
+ case 'success':
254
+ return this.colors.green(value);
255
+ case 'warning':
256
+ return this.colors.yellow(value);
257
+ case 'error':
258
+ return this.colors.red(value);
259
+ case 'command':
260
+ return this.colors.magenta(value);
261
+ case 'pending':
262
+ case 'metadata':
263
+ return this.colors.dim(value);
264
+ }
265
+ }
266
+ contentWidth() {
267
+ return Math.max(1, Math.min(this.columns - 4, TERMINAL_LAYOUT.maximumContentColumns));
268
+ }
269
+ normalize(value) {
270
+ return this.options.normalize?.(value) ?? value;
271
+ }
84
272
  banner(subtitle = 'Project workstation and scaffold initializer') {
85
273
  if (this.jsonMode) {
86
274
  return '';
87
275
  }
88
276
  if (this.layout === 'plain') {
89
- return `${this.colors.bold('Liftoff')} - ${subtitle}\n`;
277
+ return `${this.colors.bold('Liftoff')} - ${subtitle}\n\n`;
90
278
  }
91
279
  if (this.layout === 'compact') {
92
- return `${this.colors.bold(this.colors.cyan('LIFTOFF'))}\n${this.colors.dim(subtitle)}\n`;
280
+ return [
281
+ ...LIFTOFF_COMPACT_WORDMARK.map((line) => this.colors.bold(this.style('brand', line))),
282
+ ...wrapVisible(subtitle, this.columns).map((line) => this.style('metadata', line)),
283
+ ''
284
+ ].join('\n');
93
285
  }
94
- const contentWidth = Math.min(this.columns - 4, 92);
95
- const border = `+${'-'.repeat(contentWidth + 2)}+`;
286
+ const width = this.contentWidth();
287
+ const border = `${TERMINAL_GLYPHS.topLeft}${TERMINAL_GLYPHS.horizontal.repeat(width + 2)}${TERMINAL_GLYPHS.topRight}`;
96
288
  const lines = [
97
- ...LIFTOFF_WORDMARK.map((line) => this.colors.cyan(this.colors.bold(line))),
289
+ ...LIFTOFF_WORDMARK.map((line) => this.colors.bold(this.style('brand', line))),
98
290
  '',
99
- this.colors.dim(subtitle)
291
+ ...wrapVisible(subtitle, width, true).map((line) => this.style('metadata', line))
100
292
  ];
101
293
  return [
102
294
  border,
103
- ...lines.map((line) => `| ${padVisible(line, contentWidth)} |`),
104
- border,
295
+ ...lines.map((line) => `${TERMINAL_GLYPHS.vertical} ${padVisible(line, width)} ${TERMINAL_GLYPHS.vertical}`),
296
+ `${TERMINAL_GLYPHS.bottomLeft}${TERMINAL_GLYPHS.horizontal.repeat(width + 2)}${TERMINAL_GLYPHS.bottomRight}`,
105
297
  ''
106
298
  ].join('\n');
107
299
  }
300
+ commandIdentity(command, description) {
301
+ if (this.jsonMode) {
302
+ return '';
303
+ }
304
+ const identity = `LIFTOFF / ${command.toUpperCase()}`;
305
+ if (this.layout === 'plain') {
306
+ return `${identity} - ${description}\n\n`;
307
+ }
308
+ if (this.layout === 'compact') {
309
+ return `${[
310
+ this.colors.bold(this.style('brand', identity)),
311
+ ...wrapVisible(description, this.columns).map((line) => this.style('metadata', line)),
312
+ ''
313
+ ].join('\n')}\n`;
314
+ }
315
+ return this.panel(identity, [this.style('metadata', description)]);
316
+ }
108
317
  heading(value) {
109
318
  if (this.jsonMode) {
110
319
  return '';
111
320
  }
112
- return this.layout === 'plain'
113
- ? `${this.colors.bold(value)}\n`
114
- : `${this.colors.bold(this.colors.cyan(value))}\n${this.colors.dim('-'.repeat(Math.min(visibleLength(value), this.columns)))}\n`;
321
+ if (this.layout === 'plain') {
322
+ return `${this.colors.bold(value)}\n`;
323
+ }
324
+ const title = this.colors.bold(this.style('brand', value));
325
+ return `${title}\n${this.style('metadata', TERMINAL_GLYPHS.horizontal.repeat(Math.min(visibleLength(value), this.columns)))}\n`;
326
+ }
327
+ stage(value, detail) {
328
+ if (this.jsonMode) {
329
+ return '';
330
+ }
331
+ const prefix = this.layout === 'plain' ? 'Stage' : '◆';
332
+ const label = this.colors.bold(this.style('info', `${prefix}: ${value}`));
333
+ return `${label}${detail ? `\n${this.style('metadata', detail)}` : ''}\n`;
115
334
  }
116
335
  panel(title, lines) {
117
336
  if (this.jsonMode) {
118
337
  return '';
119
338
  }
120
- if (this.layout !== 'full') {
121
- return `${this.heading(title)}${lines.map((line) => `${line}\n`).join('')}`;
339
+ const normalizedTitle = this.normalize(title);
340
+ const bodyLines = (lines.length > 0 ? lines : ['']).map((line) => this.normalize(line));
341
+ if (this.layout === 'plain') {
342
+ return `${this.heading(normalizedTitle)}${bodyLines.flatMap((line) => line.split(/\r?\n/)).map((line) => `${line}\n`).join('')}\n`;
343
+ }
344
+ if (this.layout === 'compact') {
345
+ return `${this.heading(normalizedTitle)}${bodyLines.flatMap((line) => wrapVisible(line, this.columns)).map((line) => `${line}\n`).join('')}\n`;
122
346
  }
123
- const width = Math.min(this.columns - 4, 92);
124
- const body = lines.flatMap((line) => wrapLine(line, width));
125
- const topLabel = ` ${title} `;
126
- const top = `+${this.colors.bold(this.colors.cyan(topLabel))}${'-'.repeat(Math.max(0, width + 2 - topLabel.length))}+`;
347
+ const width = this.contentWidth();
348
+ const wrapped = bodyLines.flatMap((line) => wrapVisible(line, width, true));
349
+ const safeTitle = truncateVisible(normalizedTitle, Math.max(1, width - 2));
350
+ const styledTitle = this.colors.bold(this.style('brand', safeTitle));
351
+ const topPrefix = `${TERMINAL_GLYPHS.horizontal} ${styledTitle} `;
352
+ const top = `${TERMINAL_GLYPHS.topLeft}${topPrefix}${TERMINAL_GLYPHS.horizontal.repeat(Math.max(0, width + 2 - visibleLength(topPrefix)))}${TERMINAL_GLYPHS.topRight}`;
127
353
  return [
128
354
  top,
129
- ...body.map((line) => `| ${padVisible(line, width)} |`),
130
- `+${'-'.repeat(width + 2)}+`,
355
+ ...wrapped.map((line) => `${TERMINAL_GLYPHS.vertical} ${padVisible(line, width)} ${TERMINAL_GLYPHS.vertical}`),
356
+ `${TERMINAL_GLYPHS.bottomLeft}${TERMINAL_GLYPHS.horizontal.repeat(width + 2)}${TERMINAL_GLYPHS.bottomRight}`,
131
357
  ''
132
358
  ].join('\n');
133
359
  }
134
- table(headers, rows) {
135
- if (this.jsonMode) {
136
- return '';
360
+ section(title, lines) {
361
+ return this.panel(title, lines);
362
+ }
363
+ definitionList(title, items) {
364
+ const labelWidth = Math.min(24, Math.max(0, ...items.map((item) => visibleLength(item.label))));
365
+ const lines = items.map((item) => {
366
+ return `${padVisible(item.label, labelWidth)} ${item.value}`;
367
+ });
368
+ if (this.layout === 'plain') {
369
+ return `${this.heading(title)}${items.map((item) => `${item.label}: ${item.value}\n`).join('')}\n`;
137
370
  }
138
- if (headers.length === 0) {
371
+ return this.panel(title, lines);
372
+ }
373
+ bulletList(title, items) {
374
+ const marker = this.layout === 'full' ? TERMINAL_GLYPHS.bullet : '-';
375
+ return this.panel(title, items.map((item) => `${marker} ${item}`));
376
+ }
377
+ choiceList(title, choices) {
378
+ const lines = choices.map((choice, index) => {
379
+ const marker = choice.selected
380
+ ? TERMINAL_GLYPHS.selected
381
+ : this.layout === 'full'
382
+ ? TERMINAL_GLYPHS.unselected
383
+ : `${index + 1}.`;
384
+ const number = this.layout === 'full' ? `${index + 1}.` : '';
385
+ const value = choice.value && choice.value !== choice.label
386
+ ? this.style('metadata', ` (${choice.value})`)
387
+ : '';
388
+ const state = [
389
+ choice.default ? this.style('info', 'default') : '',
390
+ choice.disabled ? this.style('warning', 'unavailable') : ''
391
+ ].filter(Boolean).join(', ');
392
+ return `${marker} ${number ? `${number} ` : ''}${choice.label}${value}${state ? ` [${state}]` : ''}`;
393
+ });
394
+ return this.panel(title, lines);
395
+ }
396
+ table(headers, rows) {
397
+ if (this.jsonMode || headers.length === 0) {
139
398
  return '';
140
399
  }
141
- if (this.layout === 'plain' || this.columns < headers.length * 18) {
142
- return rows.map((row) => row.map((cell, index) => `${headers[index] ?? `Column ${index + 1}`}: ${cell}`).join(' | ')).join('\n') + (rows.length ? '\n' : '');
400
+ const normalizedHeaders = headers.map((header) => this.normalize(header));
401
+ const normalizedRows = rows.map((row) => row.map((cell) => this.normalize(cell)));
402
+ if (this.layout === 'plain') {
403
+ return normalizedRows.map((row) => row.map((cell, index) => `${normalizedHeaders[index] ?? `Column ${index + 1}`}: ${cell}`).join(' | ')).join('\n') + (rows.length > 0 ? '\n' : '');
143
404
  }
144
- const natural = headers.map((header, index) => Math.max(visibleLength(header), ...rows.map((row) => visibleLength(row[index] ?? ''))));
145
- const separators = (headers.length - 1) * 3;
146
- const available = Math.max(headers.length * 8, this.columns - separators);
147
- const capped = natural.map((width) => Math.min(width, Math.floor(available / headers.length)));
148
- const renderRow = (row) => row.map((cell, index) => padVisible(cell, capped[index] ?? 8)).join(' | ');
405
+ const natural = normalizedHeaders.map((header, index) => Math.max(visibleLength(header), ...normalizedRows.map((row) => visibleLength(row[index] ?? ''))));
406
+ const separator = ' ';
407
+ const available = (this.layout === 'full' ? this.contentWidth() : this.columns) -
408
+ separator.length * (headers.length - 1);
409
+ const widths = fitColumnWidths(natural, Math.max(headers.length * 4, available));
410
+ const renderPhysicalRow = (row) => {
411
+ const wrapped = widths.map((width, index) => wrapVisible(row[index] ?? '', width, true));
412
+ const height = Math.max(...wrapped.map((cell) => cell.length));
413
+ return Array.from({ length: height }, (_, lineIndex) => wrapped.map((cell, index) => padVisible(cell[lineIndex] ?? '', widths[index])).join(separator).trimEnd());
414
+ };
415
+ const header = renderPhysicalRow(normalizedHeaders).map((line) => this.colors.bold(line));
416
+ const separatorLine = widths.map((width) => TERMINAL_GLYPHS.horizontal.repeat(width)).join(separator);
149
417
  return [
150
- this.colors.bold(renderRow(headers)),
151
- renderRow(capped.map((width) => '-'.repeat(width))),
152
- ...rows.map(renderRow),
418
+ ...header,
419
+ this.style('metadata', separatorLine),
420
+ ...normalizedRows.flatMap(renderPhysicalRow),
153
421
  ''
154
422
  ].join('\n');
155
423
  }
424
+ tableSection(title, headers, rows) {
425
+ const lines = this.table(headers, rows).trimEnd().split('\n');
426
+ return this.layout === 'full'
427
+ ? this.panel(title, lines)
428
+ : `${this.heading(title)}${lines.join('\n')}\n\n`;
429
+ }
156
430
  status(kind, label, detail) {
157
431
  if (this.jsonMode) {
158
432
  return '';
159
433
  }
160
- const tokens = {
161
- success: this.colors.green('[ok]'),
162
- info: this.colors.cyan('[info]'),
163
- warning: this.colors.yellow('[warn]'),
164
- error: this.colors.red('[error]'),
165
- pending: this.colors.dim('[....]')
434
+ const plainTokens = {
435
+ success: '[ok]',
436
+ info: '[info]',
437
+ warning: '[warn]',
438
+ error: '[error]',
439
+ pending: '[....]'
166
440
  };
167
- return `${tokens[kind]} ${this.colors.bold(label)}${detail ? ` - ${detail}` : ''}\n`;
441
+ const richTokens = {
442
+ success: '✓',
443
+ info: 'i',
444
+ warning: '!',
445
+ error: '×',
446
+ pending: '…'
447
+ };
448
+ const token = this.layout === 'plain' ? plainTokens[kind] : richTokens[kind];
449
+ return `${this.style(kind === 'pending' ? 'pending' : kind, token)} ${this.colors.bold(label)}${detail ? `: ${detail}` : ''}\n`;
168
450
  }
169
451
  command(value) {
170
- return this.jsonMode ? '' : `${this.colors.magenta('$')} ${this.colors.bold(value)}\n`;
452
+ return this.jsonMode
453
+ ? ''
454
+ : `${this.style('command', '$')} ${this.colors.bold(value)}\n`;
455
+ }
456
+ prompt(label, defaultValue) {
457
+ if (this.jsonMode) {
458
+ return '';
459
+ }
460
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
461
+ return `${this.style('warning', '?')} ${this.colors.bold(label)}${this.style('metadata', suffix)}: `;
462
+ }
463
+ promptContext(label, detail) {
464
+ return this.panel(label, detail ? [detail] : []);
465
+ }
466
+ remedy(value) {
467
+ if (this.jsonMode) {
468
+ return '';
469
+ }
470
+ return `${this.style('info', 'Remedy:')} ${value}\n`;
471
+ }
472
+ compactError(message, remedy) {
473
+ if (this.jsonMode) {
474
+ return '';
475
+ }
476
+ const lines = message.split(/\r?\n/).filter(Boolean);
477
+ if (this.layout === 'full') {
478
+ return this.panel('Error', [
479
+ ...lines.map((line) => this.style('error', line)),
480
+ ...(remedy ? [`${this.style('info', 'Remedy:')} ${remedy}`] : [])
481
+ ]);
482
+ }
483
+ return [
484
+ this.status('error', 'Error', lines[0] ?? message).trimEnd(),
485
+ ...lines.slice(1).map((line) => ` ${line}`),
486
+ ...(remedy ? [this.remedy(remedy).trimEnd()] : []),
487
+ ''
488
+ ].join('\n');
171
489
  }
172
490
  warning(value) {
173
491
  return this.status('warning', 'Warning', value);
174
492
  }
175
- error(value) {
176
- return this.status('error', 'Error', value);
493
+ error(value, remedy) {
494
+ return this.compactError(value, remedy);
495
+ }
496
+ cancellation(value) {
497
+ return this.status('info', 'Cancelled', value);
498
+ }
499
+ completion(label, detail, items = [], nextCommand) {
500
+ return [
501
+ this.status('success', label, detail),
502
+ ...(items.length > 0 ? [this.definitionList('Completion', items)] : []),
503
+ ...(nextCommand ? [this.command(nextCommand)] : [])
504
+ ].join('');
177
505
  }
178
506
  confirmation(value) {
179
- return this.jsonMode ? '' : `${this.colors.yellow('?')} ${this.colors.bold(value)}\n`;
507
+ return this.jsonMode ? '' : `${this.style('warning', '?')} ${this.colors.bold(value)}\n`;
508
+ }
509
+ }
510
+ export class PresentationSession {
511
+ stdout;
512
+ stderr;
513
+ stdoutStream;
514
+ stderrStream;
515
+ constructor(options) {
516
+ this.stdoutStream = options.stdout;
517
+ this.stderrStream = options.stderr;
518
+ const rendererOptions = {
519
+ columns: options.columns,
520
+ color: options.color,
521
+ snapshot: options.snapshot,
522
+ json: options.json,
523
+ env: options.env,
524
+ layout: options.layout,
525
+ normalize: options.normalize
526
+ };
527
+ this.stdout = new TerminalRenderer({ stream: options.stdout, ...rendererOptions });
528
+ this.stderr = new TerminalRenderer({ stream: options.stderr, ...rendererOptions });
529
+ }
530
+ identity(subtitle) {
531
+ this.stdout.write(this.stdout.banner(subtitle));
532
+ }
533
+ commandIdentity(command, description) {
534
+ this.stdout.write(this.stdout.commandIdentity(command, description));
535
+ }
536
+ stage(title, detail) {
537
+ this.stdout.write(this.stdout.stage(title, detail));
538
+ }
539
+ section(title, lines) {
540
+ this.stdout.write(this.stdout.section(title, lines));
541
+ }
542
+ definitions(title, items) {
543
+ this.stdout.write(this.stdout.definitionList(title, items));
544
+ }
545
+ bullets(title, items) {
546
+ this.stdout.write(this.stdout.bulletList(title, items));
547
+ }
548
+ choices(title, choices) {
549
+ this.stdout.write(this.stdout.choiceList(title, choices));
550
+ }
551
+ prompt(label, defaultValue) {
552
+ this.stdout.write(this.stdout.prompt(label, defaultValue));
553
+ }
554
+ table(title, headers, rows) {
555
+ this.stdout.write(this.stdout.tableSection(title, headers, rows));
556
+ }
557
+ status(kind, label, detail) {
558
+ this.stdout.write(this.stdout.status(kind, label, detail));
559
+ }
560
+ warning(value) {
561
+ this.stdout.write(this.stdout.warning(value));
562
+ }
563
+ command(value) {
564
+ this.stdout.write(this.stdout.command(value));
565
+ }
566
+ remedy(value) {
567
+ this.stdout.write(this.stdout.remedy(value));
568
+ }
569
+ cancellation(value) {
570
+ this.stdout.write(this.stdout.cancellation(value));
571
+ }
572
+ completion(label, detail, items = [], nextCommand) {
573
+ this.stdout.write(this.stdout.completion(label, detail, items, nextCommand));
574
+ }
575
+ error(message, remedy) {
576
+ if (this.stderr.jsonMode) {
577
+ this.stderrStream.write([
578
+ message.trimEnd(),
579
+ ...(remedy ? [`Remedy: ${remedy}`] : [])
580
+ ].join('\n') + '\n');
581
+ return;
582
+ }
583
+ this.stderr.write(this.stderr.compactError(message, remedy));
584
+ }
585
+ rawStdout(value) {
586
+ this.stdoutStream.write(value);
587
+ }
588
+ rawStderr(value) {
589
+ this.stderrStream.write(value);
590
+ }
591
+ childStreams() {
592
+ return {
593
+ stdout: this.stdoutStream,
594
+ stderr: this.stderrStream
595
+ };
180
596
  }
181
597
  }
182
598
  //# sourceMappingURL=terminal.js.map