@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 +6 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/input/commands/platform-sandbox-qemu.ts +60 -16
- package/src/runtime/sandbox-qemu-templates.ts +15 -0
- package/src/version.ts +1 -1
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
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
96
|
-
const
|
|
97
|
-
const reason =
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
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
|
|
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.
|
|
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;
|