@pellux/goodvibes-tui 0.20.1 → 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,19 @@ 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
+
13
+ ## [0.20.2] — 2026-05-12
14
+
15
+ ### Changes
16
+ - Changed QEMU sandbox setup/bootstrap defaults to use `~/.goodvibes/tui/sandbox` instead of writing setup bundles into the current project.
17
+ - Made `/sandbox qemu bootstrap` perform the image build and guest runtime provisioning by default, with `--scaffold-only`, `--no-build`, and `--no-provision` escape hatches for manual workflows.
18
+ - Updated sandbox review guidance and QEMU documentation so first-run setup no longer points users at project-local sandbox paths.
19
+
7
20
  ## [0.20.1] — 2026-05-12
8
21
 
9
22
  ### 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.1-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
 
@@ -624,7 +624,7 @@ Isolation controls:
624
624
 
625
625
  The QEMU path includes:
626
626
 
627
- - setup bundle generation
627
+ - setup bundle generation under the user GoodVibes data directory by default
628
628
  - first-run bootstrap scaffolding
629
629
  - Debian cloud-image download, mutable qcow2 clone, resize, and NoCloud ISO creation through the generated `create-image.sh`
630
630
  - host-side wrapper generation
@@ -644,8 +644,8 @@ Key commands:
644
644
  - `/sandbox recommend`
645
645
  - `/sandbox doctor`
646
646
  - `/sandbox probe`
647
- - `/sandbox qemu setup <dir>`
648
- - `/sandbox qemu bootstrap <dir> [size-gb]`
647
+ - `/sandbox qemu setup [dir]`
648
+ - `/sandbox qemu bootstrap [dir] [size-gb] [--scaffold-only|--no-build|--no-provision]`
649
649
  - `/sandbox qemu inspect-setup <manifest>`
650
650
  - `/sandbox qemu apply-setup <manifest>`
651
651
  - `/sandbox session ...`
@@ -655,13 +655,13 @@ Key commands:
655
655
  Typical first-run path:
656
656
 
657
657
  ```sh
658
- /sandbox qemu bootstrap .goodvibes/tui/sandbox 20
659
- .goodvibes/tui/sandbox/create-image.sh .goodvibes/tui/sandbox/goodvibes-sandbox.qcow2 20G
660
- GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh .goodvibes/tui/sandbox/qemu-wrapper.sh bash -s < .goodvibes/tui/sandbox/guest-bootstrap.sh
658
+ /sandbox qemu bootstrap
661
659
  /sandbox doctor
662
660
  /sandbox guest-test eval-py
663
661
  ```
664
662
 
663
+ By default, QEMU setup files, the mutable qcow2 image, SSH keys, logs, and runtime state live in `~/.goodvibes/tui/sandbox`, not in the current project. Pass an explicit directory only when you intentionally want a non-default bundle location. Use `/sandbox qemu setup` or `/sandbox qemu bootstrap --scaffold-only` if you want to generate/review the bundle without building the image or provisioning guest runtimes.
664
+
665
665
  See [QEMU sandbox bootstrapping](docs/qemu-sandbox.md) for host prerequisites, generated files, first-boot behavior, guest runtime packages, and troubleshooting logs.
666
666
 
667
667
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.20.1",
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",
@@ -154,14 +154,12 @@ export function renderSetupSandboxReview(ctx: CommandContext, snapshot: SetupRev
154
154
  ' next:',
155
155
  ];
156
156
  if (backend === 'local') {
157
- lines.push(' /sandbox qemu bootstrap .goodvibes/tui/sandbox 20');
158
- lines.push(' .goodvibes/tui/sandbox/create-image.sh .goodvibes/tui/sandbox/goodvibes-sandbox.qcow2 20G');
159
- lines.push(' GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh .goodvibes/tui/sandbox/qemu-wrapper.sh bash -s < .goodvibes/tui/sandbox/guest-bootstrap.sh');
157
+ lines.push(' /sandbox qemu bootstrap');
158
+ lines.push(' default bundle: ~/.goodvibes/tui/sandbox');
160
159
  lines.push(' /sandbox doctor');
161
160
  } else if (!image || !wrapper) {
162
- lines.push(' /sandbox qemu setup .goodvibes/tui/sandbox');
163
- lines.push(' .goodvibes/tui/sandbox/create-image.sh .goodvibes/tui/sandbox/goodvibes-sandbox.qcow2 20G');
164
- lines.push(' GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh .goodvibes/tui/sandbox/qemu-wrapper.sh bash -s < .goodvibes/tui/sandbox/guest-bootstrap.sh');
161
+ lines.push(' /sandbox qemu bootstrap');
162
+ lines.push(' default bundle: ~/.goodvibes/tui/sandbox');
165
163
  lines.push(' /sandbox doctor');
166
164
  } else {
167
165
  lines.push(' /sandbox guest-test eval-js');
@@ -73,7 +73,7 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
73
73
  ...(`${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'qemu' && !String(ctx.platform.configManager.get('sandbox.qemuImagePath')).trim()
74
74
  ? [' [WARN] sandbox: qemu backend selected without qemuImagePath'] : []),
75
75
  ...(`${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'qemu' && !String(ctx.platform.configManager.get('sandbox.qemuExecWrapper')).trim()
76
- ? [' [WARN] sandbox: qemu backend selected without qemuExecWrapper', ' next: /sandbox scaffold-qemu-wrapper .goodvibes/tui/qemu-wrapper.sh'] : []),
76
+ ? [' [WARN] sandbox: qemu backend selected without qemuExecWrapper', ' next: /sandbox qemu setup'] : []),
77
77
  ...(`${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'qemu' && String(ctx.platform.configManager.get('sandbox.qemuExecWrapper')).trim()
78
78
  ? [' [INFO] sandbox: wrapper bridge can be validated with GV_SANDBOX_WRAPPER_MODE=host-exec before wiring a real guest transport'] : []),
79
79
  ...(snapshot.serviceIssues.length > 0
@@ -1,5 +1,5 @@
1
- import { readFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
3
  import type { CommandContext } from '../command-registry.ts';
4
4
  import {
5
5
  applySandboxQemuSetupManifest,
@@ -11,6 +11,141 @@ import {
11
11
  import { requireShellPaths } from './runtime-services.ts';
12
12
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
13
13
 
14
+ const DEFAULT_QEMU_SIZE_GB = 20;
15
+
16
+ interface ParsedQemuSetupArgs {
17
+ readonly directory: string;
18
+ }
19
+
20
+ interface ParsedQemuBootstrapArgs extends ParsedQemuSetupArgs {
21
+ readonly sizeGb: number;
22
+ readonly buildImage: boolean;
23
+ readonly provisionGuest: boolean;
24
+ }
25
+
26
+ function defaultQemuSandboxDirectory(shellPaths: ReturnType<typeof requireShellPaths>): string {
27
+ return shellPaths.resolveUserPath('tui', 'sandbox');
28
+ }
29
+
30
+ function parseSetupArgs(args: string[], shellPaths: ReturnType<typeof requireShellPaths>): ParsedQemuSetupArgs {
31
+ const directory = args[2] ?? defaultQemuSandboxDirectory(shellPaths);
32
+ return {
33
+ directory,
34
+ };
35
+ }
36
+
37
+ function parseBootstrapArgs(args: string[], shellPaths: ReturnType<typeof requireShellPaths>): ParsedQemuBootstrapArgs {
38
+ let directory = defaultQemuSandboxDirectory(shellPaths);
39
+ let sizeGb = DEFAULT_QEMU_SIZE_GB;
40
+ let buildImage = true;
41
+ let provisionGuest = true;
42
+ for (const arg of args.slice(2)) {
43
+ if (arg === '--scaffold-only') {
44
+ buildImage = false;
45
+ provisionGuest = false;
46
+ continue;
47
+ }
48
+ if (arg === '--no-build') {
49
+ buildImage = false;
50
+ continue;
51
+ }
52
+ if (arg === '--no-provision') {
53
+ provisionGuest = false;
54
+ continue;
55
+ }
56
+ if (/^\d+$/.test(arg)) {
57
+ sizeGb = Number.parseInt(arg, 10);
58
+ continue;
59
+ }
60
+ directory = arg;
61
+ }
62
+ return {
63
+ directory,
64
+ sizeGb,
65
+ buildImage,
66
+ provisionGuest,
67
+ };
68
+ }
69
+
70
+ function tailText(value: string, maxLines = 40): string {
71
+ const lines = value.trim().split(/\r?\n/).filter(Boolean);
72
+ return lines.slice(Math.max(0, lines.length - maxLines)).join('\n');
73
+ }
74
+
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(
86
+ label: string,
87
+ command: string,
88
+ args: readonly string[],
89
+ options: {
90
+ readonly cwd: string;
91
+ readonly env?: NodeJS.ProcessEnv;
92
+ readonly input?: string;
93
+ readonly timeoutMs: number;
94
+ },
95
+ ): Promise<void> {
96
+ let stdout = '';
97
+ let stderr = '';
98
+ let timedOut = false;
99
+ const child = spawn(command, [...args], {
100
+ cwd: options.cwd,
101
+ env: { ...process.env, ...options.env },
102
+ stdio: ['pipe', 'pipe', 'pipe'],
103
+ windowsHide: true,
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
+
134
+ if (result.status === 0) return;
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'}`;
142
+ throw new Error([
143
+ `${label} failed (${reason}).`,
144
+ stderrTail ? `stderr:\n${stderrTail}` : '',
145
+ stdoutTail ? `stdout:\n${stdoutTail}` : '',
146
+ ].filter(Boolean).join('\n'));
147
+ }
148
+
14
149
  export async function handleSandboxQemuCommand(args: string[], ctx: CommandContext): Promise<boolean> {
15
150
  const shellPaths = requireShellPaths(ctx);
16
151
  const sub = (args[1] ?? '').toLowerCase();
@@ -20,12 +155,8 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
20
155
  return true;
21
156
  }
22
157
  if (sub === 'setup') {
23
- const dirArg = args[2];
24
- if (!dirArg) {
25
- ctx.print('Usage: /sandbox qemu setup <directory>');
26
- return true;
27
- }
28
- const bundle = scaffoldSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory, dirArg, { surfaceRoot: 'tui' });
158
+ const parsed = parseSetupArgs(args, shellPaths);
159
+ const bundle = scaffoldSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory, parsed.directory, { surfaceRoot: 'tui' });
29
160
  const manifest = loadSandboxQemuSetupManifest(shellPaths.workingDirectory, bundle.manifestPath);
30
161
  applySandboxQemuSetupManifest(ctx.platform.configManager, manifest);
31
162
  ctx.platform.configManager.setDynamic('sandbox.replIsolation', 'shared-vm');
@@ -45,19 +176,21 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
45
176
  ' applied: backend=qemu, qemu binary, wrapper path, image path, launch-per-command guest settings, shared VM isolation',
46
177
  ` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} 20G`,
47
178
  ` then provision runtimes: GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh ${bundle.wrapperPath} bash -s < ${bundle.guestBootstrapScriptPath}`,
179
+ '',
180
+ `Default setup location: ${defaultQemuSandboxDirectory(shellPaths)}`,
181
+ 'Pass an explicit directory only when you intentionally want a non-default bundle location.',
48
182
  ].join('\n'));
49
183
  return true;
50
184
  }
51
185
  if (sub === 'bootstrap') {
52
- const dirArg = args[2];
53
- const sizeGb = Number.parseInt(args[3] ?? '20', 10);
54
- if (!dirArg || !Number.isInteger(sizeGb) || sizeGb < 1) {
55
- ctx.print('Usage: /sandbox qemu bootstrap <directory> [size-gb]');
186
+ const parsed = parseBootstrapArgs(args, shellPaths);
187
+ if (!Number.isInteger(parsed.sizeGb) || parsed.sizeGb < 1) {
188
+ ctx.print('Usage: /sandbox qemu bootstrap [directory] [size-gb] [--scaffold-only|--no-build|--no-provision]');
56
189
  return true;
57
190
  }
58
191
  try {
59
- const bundle = bootstrapSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory, dirArg, sizeGb, { surfaceRoot: 'tui' });
60
- ctx.print([
192
+ const bundle = bootstrapSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory, parsed.directory, parsed.sizeGb, { surfaceRoot: 'tui' });
193
+ const lines = [
61
194
  `Bootstrapped QEMU sandbox in ${bundle.directory}`,
62
195
  ` wrapper: ${bundle.wrapperPath}`,
63
196
  ` image path: ${bundle.imagePath}`,
@@ -67,10 +200,45 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
67
200
  ` projection policy: ${bundle.projectionPolicyPath}`,
68
201
  ` manifest: ${bundle.manifestPath}`,
69
202
  ' applied: backend=qemu, qemu binary, wrapper path, image path, launch-per-command guest settings, shared VM isolation',
70
- ` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} ${sizeGb}G`,
71
- ` then provision runtimes: GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh ${bundle.wrapperPath} bash -s < ${bundle.guestBootstrapScriptPath}`,
72
- ' then: /sandbox guest-test eval-py',
73
- ].join('\n'));
203
+ ];
204
+ if (parsed.buildImage) {
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`], {
207
+ cwd: bundle.directory,
208
+ timeoutMs: 30 * 60 * 1000,
209
+ });
210
+ lines.push(' image build: complete');
211
+ } else {
212
+ lines.push(` image build: skipped`);
213
+ lines.push(` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} ${parsed.sizeGb}G`);
214
+ }
215
+ if (parsed.provisionGuest) {
216
+ if (!existsSync(bundle.imagePath)) {
217
+ lines.push(' guest provisioning: skipped because image does not exist');
218
+ lines.push(` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} ${parsed.sizeGb}G`);
219
+ } else {
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'], {
222
+ cwd: bundle.directory,
223
+ env: {
224
+ GV_SANDBOX_SYNC_WORKSPACE: '0',
225
+ GV_SANDBOX_WRAPPER_MODE: 'launch-qemu-ssh',
226
+ },
227
+ input: readFileSync(bundle.guestBootstrapScriptPath, 'utf8'),
228
+ timeoutMs: 45 * 60 * 1000,
229
+ });
230
+ lines.push(' guest provisioning: complete');
231
+ }
232
+ } else {
233
+ lines.push(` guest provisioning: skipped`);
234
+ lines.push(` next: GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh ${bundle.wrapperPath} bash -s < ${bundle.guestBootstrapScriptPath}`);
235
+ }
236
+ lines.push(' verify: /sandbox doctor');
237
+ lines.push(' verify: /sandbox guest-test eval-py');
238
+ lines.push('');
239
+ lines.push(`Default setup location: ${defaultQemuSandboxDirectory(shellPaths)}`);
240
+ lines.push('Pass an explicit directory only when you intentionally want a non-default bundle location.');
241
+ ctx.print(lines.join('\n'));
74
242
  } catch (error) {
75
243
  ctx.print(summarizeError(error));
76
244
  }
@@ -114,6 +282,6 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
114
282
  ctx.print(`Applied QEMU sandbox setup from ${shellPaths.resolveWorkspacePath(pathArg)}.`);
115
283
  return true;
116
284
  }
117
- ctx.print('Usage: /sandbox qemu <setup <directory>|bootstrap <directory> [size-gb]|recover <session-id>|inspect-setup <setup-manifest.json>|apply-setup <setup-manifest.json>>');
285
+ ctx.print('Usage: /sandbox qemu <setup [directory]|bootstrap [directory] [size-gb] [--scaffold-only|--no-build|--no-provision]|recover <session-id>|inspect-setup <setup-manifest.json>|apply-setup <setup-manifest.json>>');
118
286
  return true;
119
287
  }
@@ -93,7 +93,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
93
93
  registry.register({
94
94
  name: 'sandbox',
95
95
  description: 'Review and configure VM isolation policy for MCP and evaluation runtimes',
96
- usage: '[open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup|bootstrap|recover|inspect-setup|apply-setup> ...|session ...|bundle ...|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]',
96
+ usage: '[open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup [dir]|bootstrap [dir] [size-gb]|recover|inspect-setup|apply-setup> ...|session ...|bundle ...|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]',
97
97
  async handler(args, ctx) {
98
98
  const shellPaths = requireShellPaths(ctx);
99
99
  const sub = args[0] ?? 'open';
@@ -144,7 +144,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
144
144
  windowsMode: `${ctx.platform.configManager.get('sandbox.windowsMode')}`,
145
145
  secureSandboxReady: renderSandboxReview(ctx.platform.configManager).includes('available'),
146
146
  recommendedCommand: `${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'local'
147
- ? '/sandbox qemu bootstrap .goodvibes/tui/sandbox 20'
147
+ ? '/sandbox qemu bootstrap'
148
148
  : '/sandbox doctor',
149
149
  };
150
150
  ctx.print([inspectSandboxProbe(probe), ...backendProbe.warnings.map((warning: string) => ` warning: ${warning}`)].join('\n'));
@@ -207,7 +207,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
207
207
  ` wrapper: ${bundle.wrapperPath}`,
208
208
  ` guest bundle: ${bundle.guestBundlePath}`,
209
209
  ` readme: ${bundle.readmePath}`,
210
- ' next: /sandbox qemu setup <dir> for the full first-run setup path',
210
+ ' next: /sandbox qemu setup for the full default setup path',
211
211
  ].join('\n'));
212
212
  return;
213
213
  }
@@ -402,7 +402,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
402
402
  ctx.print([`Scaffolded QEMU wrapper to ${targetPath}`, ' bridge test mode: GV_SANDBOX_WRAPPER_MODE=host-exec', ' next: /sandbox set-qemu-wrapper ' + targetPath].join('\n'));
403
403
  return;
404
404
  }
405
- ctx.print('Usage: /sandbox [open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup|bootstrap|recover|inspect-setup|apply-setup> ...|session ...|bundle export <path>|bundle inspect <path>|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]');
405
+ ctx.print('Usage: /sandbox [open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup [dir]|bootstrap [dir] [size-gb]|recover|inspect-setup|apply-setup> ...|session ...|bundle export <path>|bundle inspect <path>|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]');
406
406
  },
407
407
  });
408
408
  }
@@ -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.1';
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;