plazbot-cli 0.3.1 → 0.3.3

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.
@@ -1,17 +1,71 @@
1
1
  // @ts-ignore - marked-terminal no provee tipos
2
2
  import TerminalRenderer from 'marked-terminal';
3
3
  import { marked } from 'marked';
4
+ import chalk from 'chalk';
5
+ // @ts-ignore - cli-highlight no provee tipos completos
6
+ import { highlight, supportsLanguage } from 'cli-highlight';
4
7
  let configured = false;
8
+ const LANG_ALIASES = {
9
+ sh: 'bash',
10
+ shell: 'bash',
11
+ zsh: 'bash',
12
+ curl: 'bash',
13
+ jsx: 'javascript',
14
+ ts: 'typescript',
15
+ tsx: 'typescript',
16
+ yml: 'yaml',
17
+ html: 'xml',
18
+ text: 'plaintext',
19
+ txt: 'plaintext',
20
+ };
21
+ function resolveLang(raw) {
22
+ const k = (raw ?? '').toLowerCase().trim();
23
+ const aliased = LANG_ALIASES[k] ?? k;
24
+ if (!aliased)
25
+ return 'plaintext';
26
+ try {
27
+ return supportsLanguage(aliased) ? aliased : 'plaintext';
28
+ }
29
+ catch {
30
+ return 'plaintext';
31
+ }
32
+ }
33
+ function renderCodeBlock(code, rawLang) {
34
+ const lang = resolveLang(rawLang);
35
+ let body;
36
+ try {
37
+ body = highlight(code, { language: lang, ignoreIllegals: true });
38
+ }
39
+ catch {
40
+ body = code;
41
+ }
42
+ const label = (rawLang ?? '').trim() || lang;
43
+ const width = Math.max(20, Math.min(80, (process.stdout.columns || 80) - 8));
44
+ const headerLabel = ` ${label} `;
45
+ const headerRule = '─'.repeat(Math.max(0, width - headerLabel.length - 4));
46
+ const header = chalk.gray.dim(`──${headerLabel}${headerRule}`);
47
+ const footer = chalk.gray.dim('─'.repeat(width - 2));
48
+ // Indent del cuerpo para diferenciar del prosa
49
+ const indented = body
50
+ .split('\n')
51
+ .map((l) => ' ' + l)
52
+ .join('\n');
53
+ return `\n${header}\n${indented}\n${footer}\n`;
54
+ }
5
55
  function configure() {
6
56
  if (configured)
7
57
  return;
8
- marked.setOptions({
9
- renderer: new TerminalRenderer({
10
- width: Math.max(40, (process.stdout.columns || 80) - 8),
11
- reflowText: true,
12
- tab: 2,
13
- }),
58
+ const renderer = new TerminalRenderer({
59
+ width: Math.max(40, (process.stdout.columns || 80) - 8),
60
+ reflowText: true,
61
+ tab: 2,
62
+ // Inline code mejor diferenciado del texto normal
63
+ codespan: chalk.cyan,
14
64
  });
65
+ // Override del code block para syntax highlight + box delimitado.
66
+ // marked-terminal pasa (code, lang, escaped) al método `code`.
67
+ renderer.code = (code, lang) => renderCodeBlock(code, lang);
68
+ marked.setOptions({ renderer });
15
69
  configured = true;
16
70
  }
17
71
  /**
@@ -6,6 +6,12 @@ import { dirname, join } from 'node:path';
6
6
  import { App } from './components/App.js';
7
7
  import { getStoredCredentials } from '../utils/credentials.js';
8
8
  import { logger } from '../utils/logger.js';
9
+ // Alternate screen buffer (ANSI). Same trick used by vim, htop, claude code, lazygit.
10
+ // The TUI runs in a clean canvas and the original scrollback is restored on exit,
11
+ // which eliminates the "ghost frames" Ink leaves behind when the terminal is resized.
12
+ const ALT_SCREEN_ENTER = '\x1b[?1049h\x1b[H';
13
+ const ALT_SCREEN_EXIT = '\x1b[?1049l';
14
+ const SHOW_CURSOR = '\x1b[?25h';
9
15
  export async function runRepl(opts) {
10
16
  let creds;
11
17
  try {
@@ -30,6 +36,31 @@ export async function runRepl(opts) {
30
36
  dev: opts.dev,
31
37
  };
32
38
  const version = readVersion();
39
+ const useAltScreen = !!process.stdout.isTTY;
40
+ // Cleanup idempotente; lo registramos en varias señales para que la terminal
41
+ // nunca quede en alt screen ni con el cursor oculto si el proceso muere mal.
42
+ let cleanedUp = false;
43
+ const restoreTerminal = () => {
44
+ if (cleanedUp)
45
+ return;
46
+ cleanedUp = true;
47
+ if (useAltScreen) {
48
+ process.stdout.write(ALT_SCREEN_EXIT);
49
+ process.stdout.write(SHOW_CURSOR);
50
+ }
51
+ };
52
+ if (useAltScreen) {
53
+ process.stdout.write(ALT_SCREEN_ENTER);
54
+ }
55
+ process.on('exit', restoreTerminal);
56
+ process.on('SIGHUP', restoreTerminal);
57
+ process.on('SIGTERM', restoreTerminal);
58
+ process.on('uncaughtException', (err) => {
59
+ restoreTerminal();
60
+ // Re-emit so node prints the trace after restoring the terminal.
61
+ console.error(err);
62
+ process.exit(1);
63
+ });
33
64
  const { waitUntilExit } = render(_jsx(App, { version: version, stream: stream, initialAgentId: opts.agentId ?? null, dev: opts.dev, supportMode: supportMode }), {
34
65
  exitOnCtrlC: false,
35
66
  });
@@ -37,7 +68,12 @@ export async function runRepl(opts) {
37
68
  process.on('SIGINT', () => {
38
69
  // no-op aquí; App.tsx llama exit() al detectar Ctrl+C.
39
70
  });
40
- await waitUntilExit();
71
+ try {
72
+ await waitUntilExit();
73
+ }
74
+ finally {
75
+ restoreTerminal();
76
+ }
41
77
  }
42
78
  function readVersion() {
43
79
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plazbot-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "CLI para Plazbot SDK",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
@@ -39,6 +39,7 @@
39
39
  "axios": "^1.6.0",
40
40
  "boxen": "^7.1.1",
41
41
  "chalk": "^5.3.0",
42
+ "cli-highlight": "^2.1.11",
42
43
  "cli-table3": "^0.6.5",
43
44
  "commander": "^12.0.0",
44
45
  "figures": "^6.1.0",
@@ -1,18 +1,79 @@
1
1
  // @ts-ignore - marked-terminal no provee tipos
2
2
  import TerminalRenderer from 'marked-terminal';
3
3
  import { marked } from 'marked';
4
+ import chalk from 'chalk';
5
+ // @ts-ignore - cli-highlight no provee tipos completos
6
+ import { highlight, supportsLanguage } from 'cli-highlight';
4
7
 
5
8
  let configured = false;
6
9
 
10
+ const LANG_ALIASES: Record<string, string> = {
11
+ sh: 'bash',
12
+ shell: 'bash',
13
+ zsh: 'bash',
14
+ curl: 'bash',
15
+ jsx: 'javascript',
16
+ ts: 'typescript',
17
+ tsx: 'typescript',
18
+ yml: 'yaml',
19
+ html: 'xml',
20
+ text: 'plaintext',
21
+ txt: 'plaintext',
22
+ };
23
+
24
+ function resolveLang(raw: string | undefined): string {
25
+ const k = (raw ?? '').toLowerCase().trim();
26
+ const aliased = LANG_ALIASES[k] ?? k;
27
+ if (!aliased) return 'plaintext';
28
+ try {
29
+ return supportsLanguage(aliased) ? aliased : 'plaintext';
30
+ } catch {
31
+ return 'plaintext';
32
+ }
33
+ }
34
+
35
+ function renderCodeBlock(code: string, rawLang: string | undefined): string {
36
+ const lang = resolveLang(rawLang);
37
+ let body: string;
38
+ try {
39
+ body = highlight(code, { language: lang, ignoreIllegals: true });
40
+ } catch {
41
+ body = code;
42
+ }
43
+
44
+ const label = (rawLang ?? '').trim() || lang;
45
+ const width = Math.max(20, Math.min(80, (process.stdout.columns || 80) - 8));
46
+ const headerLabel = ` ${label} `;
47
+ const headerRule = '─'.repeat(Math.max(0, width - headerLabel.length - 4));
48
+ const header = chalk.gray.dim(`──${headerLabel}${headerRule}`);
49
+ const footer = chalk.gray.dim('─'.repeat(width - 2));
50
+
51
+ // Indent del cuerpo para diferenciar del prosa
52
+ const indented = body
53
+ .split('\n')
54
+ .map((l) => ' ' + l)
55
+ .join('\n');
56
+
57
+ return `\n${header}\n${indented}\n${footer}\n`;
58
+ }
59
+
7
60
  function configure(): void {
8
61
  if (configured) return;
9
- marked.setOptions({
10
- renderer: new TerminalRenderer({
11
- width: Math.max(40, (process.stdout.columns || 80) - 8),
12
- reflowText: true,
13
- tab: 2,
14
- }) as any,
15
- });
62
+
63
+ const renderer = new TerminalRenderer({
64
+ width: Math.max(40, (process.stdout.columns || 80) - 8),
65
+ reflowText: true,
66
+ tab: 2,
67
+ // Inline code mejor diferenciado del texto normal
68
+ codespan: chalk.cyan,
69
+ }) as any;
70
+
71
+ // Override del code block para syntax highlight + box delimitado.
72
+ // marked-terminal pasa (code, lang, escaped) al método `code`.
73
+ renderer.code = (code: string, lang: string | undefined): string =>
74
+ renderCodeBlock(code, lang);
75
+
76
+ marked.setOptions({ renderer });
16
77
  configured = true;
17
78
  }
18
79
 
@@ -15,6 +15,13 @@ interface RunReplOptions {
15
15
  workspaceOverride?: string;
16
16
  }
17
17
 
18
+ // Alternate screen buffer (ANSI). Same trick used by vim, htop, claude code, lazygit.
19
+ // The TUI runs in a clean canvas and the original scrollback is restored on exit,
20
+ // which eliminates the "ghost frames" Ink leaves behind when the terminal is resized.
21
+ const ALT_SCREEN_ENTER = '\x1b[?1049h\x1b[H';
22
+ const ALT_SCREEN_EXIT = '\x1b[?1049l';
23
+ const SHOW_CURSOR = '\x1b[?25h';
24
+
18
25
  export async function runRepl(opts: RunReplOptions): Promise<void> {
19
26
  let creds;
20
27
  try {
@@ -42,6 +49,32 @@ export async function runRepl(opts: RunReplOptions): Promise<void> {
42
49
  };
43
50
 
44
51
  const version = readVersion();
52
+ const useAltScreen = !!process.stdout.isTTY;
53
+
54
+ // Cleanup idempotente; lo registramos en varias señales para que la terminal
55
+ // nunca quede en alt screen ni con el cursor oculto si el proceso muere mal.
56
+ let cleanedUp = false;
57
+ const restoreTerminal = () => {
58
+ if (cleanedUp) return;
59
+ cleanedUp = true;
60
+ if (useAltScreen) {
61
+ process.stdout.write(ALT_SCREEN_EXIT);
62
+ process.stdout.write(SHOW_CURSOR);
63
+ }
64
+ };
65
+
66
+ if (useAltScreen) {
67
+ process.stdout.write(ALT_SCREEN_ENTER);
68
+ }
69
+ process.on('exit', restoreTerminal);
70
+ process.on('SIGHUP', restoreTerminal);
71
+ process.on('SIGTERM', restoreTerminal);
72
+ process.on('uncaughtException', (err) => {
73
+ restoreTerminal();
74
+ // Re-emit so node prints the trace after restoring the terminal.
75
+ console.error(err);
76
+ process.exit(1);
77
+ });
45
78
 
46
79
  const { waitUntilExit } = render(
47
80
  <App
@@ -61,7 +94,11 @@ export async function runRepl(opts: RunReplOptions): Promise<void> {
61
94
  // no-op aquí; App.tsx llama exit() al detectar Ctrl+C.
62
95
  });
63
96
 
64
- await waitUntilExit();
97
+ try {
98
+ await waitUntilExit();
99
+ } finally {
100
+ restoreTerminal();
101
+ }
65
102
  }
66
103
 
67
104
  function readVersion(): string {