@pellux/goodvibes-tui 0.20.2 → 0.20.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.20.3] — 2026-05-13
8
+
9
+ ### Fixes
10
+ - Changed `/sandbox qemu bootstrap` to run image build and guest provisioning asynchronously so the TUI can clear the prompt, render progress, and remain responsive while long QEMU setup steps run.
11
+ - Updated generated QEMU wrappers to fail fast when the QEMU process exits before SSH is available, including a tail of the QEMU log for port conflicts and startup failures.
12
+
7
13
  ## [0.20.2] — 2026-05-12
8
14
 
9
15
  ### Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.20.2-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.20.3-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.20.2",
3
+ "version": "0.20.3",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import { spawnSync } from 'node:child_process';
2
+ import { spawn } from 'node:child_process';
3
3
  import type { CommandContext } from '../command-registry.ts';
4
4
  import {
5
5
  applySandboxQemuSetupManifest,
@@ -72,7 +72,17 @@ function tailText(value: string, maxLines = 40): string {
72
72
  return lines.slice(Math.max(0, lines.length - maxLines)).join('\n');
73
73
  }
74
74
 
75
- function runQemuBootstrapStep(
75
+ function appendBounded(current: string, chunk: Buffer | string, maxLength = 40_000): string {
76
+ const next = current + chunk.toString();
77
+ return next.length > maxLength ? next.slice(next.length - maxLength) : next;
78
+ }
79
+
80
+ function printProgress(ctx: CommandContext, message: string): void {
81
+ ctx.print(message);
82
+ ctx.renderRequest();
83
+ }
84
+
85
+ async function runQemuBootstrapStep(
76
86
  label: string,
77
87
  command: string,
78
88
  args: readonly string[],
@@ -82,23 +92,57 @@ function runQemuBootstrapStep(
82
92
  readonly input?: string;
83
93
  readonly timeoutMs: number;
84
94
  },
85
- ): void {
86
- const result = spawnSync(command, [...args], {
95
+ ): Promise<void> {
96
+ let stdout = '';
97
+ let stderr = '';
98
+ let timedOut = false;
99
+ const child = spawn(command, [...args], {
87
100
  cwd: options.cwd,
88
101
  env: { ...process.env, ...options.env },
89
- input: options.input,
90
- timeout: options.timeoutMs,
91
- encoding: 'utf8',
102
+ stdio: ['pipe', 'pipe', 'pipe'],
92
103
  windowsHide: true,
93
104
  });
105
+
106
+ child.stdout?.on('data', (chunk) => {
107
+ stdout = appendBounded(stdout, chunk);
108
+ });
109
+ child.stderr?.on('data', (chunk) => {
110
+ stderr = appendBounded(stderr, chunk);
111
+ });
112
+
113
+ if (options.input !== undefined) {
114
+ child.stdin?.end(options.input);
115
+ } else {
116
+ child.stdin?.end();
117
+ }
118
+
119
+ let killTimer: NodeJS.Timeout | undefined;
120
+ const timeout = setTimeout(() => {
121
+ timedOut = true;
122
+ child.kill('SIGTERM');
123
+ killTimer = setTimeout(() => child.kill('SIGKILL'), 5_000);
124
+ }, options.timeoutMs);
125
+
126
+ const result = await new Promise<{ status: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
127
+ child.once('error', reject);
128
+ child.once('close', (status, signal) => resolve({ status, signal }));
129
+ }).finally(() => {
130
+ clearTimeout(timeout);
131
+ if (killTimer) clearTimeout(killTimer);
132
+ });
133
+
94
134
  if (result.status === 0) return;
95
- const stderr = tailText(result.stderr || '');
96
- const stdout = tailText(result.stdout || '');
97
- const reason = result.error instanceof Error ? result.error.message : `exit ${result.status ?? 'unknown'}`;
135
+ const stderrTail = tailText(stderr);
136
+ const stdoutTail = tailText(stdout);
137
+ const reason = timedOut
138
+ ? `timed out after ${Math.round(options.timeoutMs / 1000)}s`
139
+ : result.signal
140
+ ? `signal ${result.signal}`
141
+ : `exit ${result.status ?? 'unknown'}`;
98
142
  throw new Error([
99
143
  `${label} failed (${reason}).`,
100
- stderr ? `stderr:\n${stderr}` : '',
101
- stdout ? `stdout:\n${stdout}` : '',
144
+ stderrTail ? `stderr:\n${stderrTail}` : '',
145
+ stdoutTail ? `stdout:\n${stdoutTail}` : '',
102
146
  ].filter(Boolean).join('\n'));
103
147
  }
104
148
 
@@ -158,8 +202,8 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
158
202
  ' applied: backend=qemu, qemu binary, wrapper path, image path, launch-per-command guest settings, shared VM isolation',
159
203
  ];
160
204
  if (parsed.buildImage) {
161
- ctx.print(`Building QEMU image at ${bundle.imagePath} (${parsed.sizeGb}G). This can download the Debian cloud image on first run.`);
162
- runQemuBootstrapStep('QEMU image build', bundle.imageCreateScriptPath, [bundle.imagePath, `${parsed.sizeGb}G`], {
205
+ printProgress(ctx, `Building QEMU image at ${bundle.imagePath} (${parsed.sizeGb}G). This can download the Debian cloud image on first run.`);
206
+ await runQemuBootstrapStep('QEMU image build', bundle.imageCreateScriptPath, [bundle.imagePath, `${parsed.sizeGb}G`], {
163
207
  cwd: bundle.directory,
164
208
  timeoutMs: 30 * 60 * 1000,
165
209
  });
@@ -173,8 +217,8 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
173
217
  lines.push(' guest provisioning: skipped because image does not exist');
174
218
  lines.push(` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} ${parsed.sizeGb}G`);
175
219
  } else {
176
- ctx.print(`Provisioning guest runtimes through ${bundle.wrapperPath}. First boot can take several minutes.`);
177
- runQemuBootstrapStep('QEMU guest runtime provisioning', bundle.wrapperPath, ['bash', '-s'], {
220
+ printProgress(ctx, `Provisioning guest runtimes through ${bundle.wrapperPath}. First boot can take several minutes.`);
221
+ await runQemuBootstrapStep('QEMU guest runtime provisioning', bundle.wrapperPath, ['bash', '-s'], {
178
222
  cwd: bundle.directory,
179
223
  env: {
180
224
  GV_SANDBOX_SYNC_WORKSPACE: '0',
@@ -49,6 +49,18 @@ wait_for_ssh() {
49
49
  local start now
50
50
  start="$(date +%s)"
51
51
  while true; do
52
+ if [[ -n "\${GV_SANDBOX_ACTIVE_QEMU_PIDFILE:-}" && -f "\${GV_SANDBOX_ACTIVE_QEMU_PIDFILE:-}" ]]; then
53
+ local qemu_pid
54
+ qemu_pid="$(<"\${GV_SANDBOX_ACTIVE_QEMU_PIDFILE:-}")"
55
+ if [[ -n "$qemu_pid" ]] && ! kill -0 "$qemu_pid" 2>/dev/null; then
56
+ echo "QEMU process exited before SSH became available." >&2
57
+ if [[ -n "\${GV_SANDBOX_ACTIVE_QEMU_LOG:-}" && -f "\${GV_SANDBOX_ACTIVE_QEMU_LOG:-}" ]]; then
58
+ echo "QEMU log tail:" >&2
59
+ tail -80 "\${GV_SANDBOX_ACTIVE_QEMU_LOG:-}" >&2 || true
60
+ fi
61
+ return 1
62
+ fi
63
+ fi
52
64
  if ssh "\${ssh_opts[@]}" "$user@$host" "true" >/dev/null 2>&1; then
53
65
  return 0
54
66
  fi
@@ -85,6 +97,9 @@ start_qemu() {
85
97
  local serial_log="$logs_dir/serial-$port.log"
86
98
  local qemu_log="$logs_dir/qemu-$port.log"
87
99
  local monitor_sock="$run_dir/monitor-$port.sock"
100
+ GV_SANDBOX_ACTIVE_QEMU_PIDFILE="$pidfile"
101
+ GV_SANDBOX_ACTIVE_QEMU_LOG="$qemu_log"
102
+ export GV_SANDBOX_ACTIVE_QEMU_PIDFILE GV_SANDBOX_ACTIVE_QEMU_LOG
88
103
  rm -f "$monitor_sock"
89
104
  if [[ -f "$pidfile" ]]; then
90
105
  local old_pid
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.20.2';
9
+ let _version = '0.20.3';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;