@pixelbyte-software/pixcode 1.40.3 → 1.40.4

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.
@@ -0,0 +1,313 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { buildCliSpawnEnv, findExecutableOnPath } from './install-jobs.js';
6
+ const DEFAULT_RUNTIMES_HOME = path.join(os.homedir(), '.pixcode', 'runtimes');
7
+ const MANIFEST_FILE = 'pixcode-runtime.json';
8
+ const installLocks = new Map();
9
+ function runtimesHome(env = process.env) {
10
+ return env.PIXCODE_MANAGED_RUNTIMES_HOME || DEFAULT_RUNTIMES_HOME;
11
+ }
12
+ function runtimeDir(id, env = process.env) {
13
+ return path.join(runtimesHome(env), id);
14
+ }
15
+ function manifestPath(id, env = process.env) {
16
+ return path.join(runtimeDir(id, env), MANIFEST_FILE);
17
+ }
18
+ async function fileExists(filePath) {
19
+ try {
20
+ const stats = await fs.stat(filePath);
21
+ return stats.isFile();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ async function readManifest(id, env = process.env) {
28
+ try {
29
+ const content = await fs.readFile(manifestPath(id, env), 'utf8');
30
+ const manifest = JSON.parse(content);
31
+ if (manifest?.executablePath && await fileExists(manifest.executablePath)) {
32
+ return manifest;
33
+ }
34
+ }
35
+ catch {
36
+ // Missing or malformed manifests are treated as not installed.
37
+ }
38
+ return null;
39
+ }
40
+ function platformTokens() {
41
+ if (process.platform === 'win32')
42
+ return ['windows'];
43
+ if (process.platform === 'darwin')
44
+ return ['mac', 'darwin'];
45
+ if (process.platform === 'linux')
46
+ return ['linux'];
47
+ return [process.platform];
48
+ }
49
+ function archTokens() {
50
+ if (process.arch === 'x64')
51
+ return ['x86_64', 'amd64', 'x64'];
52
+ if (process.arch === 'arm64')
53
+ return ['aarch64', 'arm64'];
54
+ return [process.arch];
55
+ }
56
+ function scoreFrankenPhpAsset(assetName) {
57
+ const name = assetName.toLowerCase();
58
+ if (!name.includes('frankenphp'))
59
+ return -1;
60
+ if (name.includes('debug'))
61
+ return -1;
62
+ if (!platformTokens().some((token) => name.includes(token)))
63
+ return -1;
64
+ if (!archTokens().some((token) => name.includes(token)))
65
+ return -1;
66
+ let score = 10;
67
+ if (process.platform === 'win32' && name.endsWith('.zip'))
68
+ score += 10;
69
+ if (process.platform !== 'win32' && !name.endsWith('.zip'))
70
+ score += 10;
71
+ if (!name.includes('gnu'))
72
+ score += 2;
73
+ if (name.endsWith('.tar.gz') || name.endsWith('.tgz'))
74
+ score += 1;
75
+ return score;
76
+ }
77
+ function selectFrankenPhpAsset(release) {
78
+ const assets = Array.isArray(release?.assets) ? release.assets : [];
79
+ const candidates = assets
80
+ .map((asset) => ({ asset, score: scoreFrankenPhpAsset(asset.name || '') }))
81
+ .filter((entry) => entry.score >= 0)
82
+ .sort((a, b) => b.score - a.score);
83
+ return candidates[0]?.asset || null;
84
+ }
85
+ async function fetchJson(url, env = process.env) {
86
+ const headers = {
87
+ Accept: 'application/vnd.github+json',
88
+ 'User-Agent': 'Pixcode Live View',
89
+ };
90
+ if (env.GITHUB_TOKEN)
91
+ headers.Authorization = `Bearer ${env.GITHUB_TOKEN}`;
92
+ const response = await fetch(url, { headers });
93
+ if (!response.ok) {
94
+ throw new Error(`Runtime metadata request failed with HTTP ${response.status}`);
95
+ }
96
+ return response.json();
97
+ }
98
+ async function downloadFile(url, targetFile, env = process.env) {
99
+ const headers = { 'User-Agent': 'Pixcode Live View' };
100
+ if (env.GITHUB_TOKEN && url.includes('github.com'))
101
+ headers.Authorization = `Bearer ${env.GITHUB_TOKEN}`;
102
+ const response = await fetch(url, { headers });
103
+ if (!response.ok) {
104
+ throw new Error(`Runtime download failed with HTTP ${response.status}`);
105
+ }
106
+ const buffer = Buffer.from(await response.arrayBuffer());
107
+ await fs.writeFile(targetFile, buffer);
108
+ }
109
+ function runProcess(command, args, options = {}) {
110
+ return new Promise((resolve, reject) => {
111
+ let stderr = '';
112
+ const child = spawn(command, args, {
113
+ ...options,
114
+ stdio: ['ignore', 'ignore', 'pipe'],
115
+ windowsHide: true,
116
+ });
117
+ child.stderr.on('data', (chunk) => {
118
+ stderr += chunk.toString();
119
+ });
120
+ child.on('error', reject);
121
+ child.on('close', (code) => {
122
+ if (code === 0) {
123
+ resolve();
124
+ return;
125
+ }
126
+ reject(new Error(`${command} exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
127
+ });
128
+ });
129
+ }
130
+ async function extractZip(archivePath, targetDir, env = process.env) {
131
+ if (process.platform === 'win32') {
132
+ const shell = env.ComSpec || process.env.ComSpec || 'powershell.exe';
133
+ const isCmd = shell.toLowerCase().endsWith('cmd.exe');
134
+ if (isCmd) {
135
+ await runProcess('powershell.exe', [
136
+ '-NoProfile',
137
+ '-ExecutionPolicy',
138
+ 'Bypass',
139
+ '-Command',
140
+ 'Expand-Archive -Force -LiteralPath $args[0] -DestinationPath $args[1]',
141
+ archivePath,
142
+ targetDir,
143
+ ], { env });
144
+ return;
145
+ }
146
+ await runProcess(shell, [
147
+ '-NoProfile',
148
+ '-ExecutionPolicy',
149
+ 'Bypass',
150
+ '-Command',
151
+ 'Expand-Archive -Force -LiteralPath $args[0] -DestinationPath $args[1]',
152
+ archivePath,
153
+ targetDir,
154
+ ], { env });
155
+ return;
156
+ }
157
+ await runProcess('unzip', ['-q', archivePath, '-d', targetDir], { env });
158
+ }
159
+ async function extractTarGz(archivePath, targetDir, env = process.env) {
160
+ await runProcess('tar', ['-xzf', archivePath, '-C', targetDir], { env });
161
+ }
162
+ async function findRuntimeExecutable(searchRoot, binaryName) {
163
+ const expectedNames = process.platform === 'win32'
164
+ ? [`${binaryName}.exe`, binaryName]
165
+ : [binaryName];
166
+ const stack = [searchRoot];
167
+ while (stack.length > 0) {
168
+ const current = stack.pop();
169
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
170
+ for (const entry of entries) {
171
+ const full = path.join(current, entry.name);
172
+ if (entry.isDirectory()) {
173
+ stack.push(full);
174
+ continue;
175
+ }
176
+ if (entry.isFile() && expectedNames.includes(entry.name)) {
177
+ return full;
178
+ }
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+ async function installFrankenPhp(env = process.env) {
184
+ const releaseApiUrl = env.PIXCODE_FRANKENPHP_RELEASE_API
185
+ || 'https://api.github.com/repos/php/frankenphp/releases/latest';
186
+ const release = env.PIXCODE_FRANKENPHP_URL ? null : await fetchJson(releaseApiUrl, env);
187
+ const asset = env.PIXCODE_FRANKENPHP_URL
188
+ ? {
189
+ name: path.basename(new URL(env.PIXCODE_FRANKENPHP_URL).pathname),
190
+ browser_download_url: env.PIXCODE_FRANKENPHP_URL,
191
+ }
192
+ : selectFrankenPhpAsset(release);
193
+ if (!asset?.browser_download_url) {
194
+ throw new Error('No FrankenPHP binary is available for this operating system and CPU architecture.');
195
+ }
196
+ const baseDir = runtimeDir('frankenphp', env);
197
+ const stagingDir = path.join(baseDir, `.staging-${Date.now()}`);
198
+ const currentDir = path.join(baseDir, 'current');
199
+ await fs.mkdir(stagingDir, { recursive: true });
200
+ const archivePath = path.join(stagingDir, asset.name || 'frankenphp');
201
+ await downloadFile(asset.browser_download_url, archivePath, env);
202
+ let executablePath = archivePath;
203
+ const assetName = (asset.name || '').toLowerCase();
204
+ if (assetName.endsWith('.zip')) {
205
+ await extractZip(archivePath, stagingDir, env);
206
+ executablePath = await findRuntimeExecutable(stagingDir, 'frankenphp');
207
+ if (!executablePath) {
208
+ throw new Error('Downloaded FrankenPHP archive did not contain a frankenphp executable.');
209
+ }
210
+ }
211
+ else if (assetName.endsWith('.tar.gz') || assetName.endsWith('.tgz')) {
212
+ await extractTarGz(archivePath, stagingDir, env);
213
+ executablePath = await findRuntimeExecutable(stagingDir, 'frankenphp');
214
+ if (!executablePath) {
215
+ throw new Error('Downloaded FrankenPHP archive did not contain a frankenphp executable.');
216
+ }
217
+ }
218
+ if (process.platform !== 'win32') {
219
+ await fs.chmod(executablePath, 0o755).catch(() => undefined);
220
+ }
221
+ await fs.rm(currentDir, { recursive: true, force: true });
222
+ await fs.mkdir(currentDir, { recursive: true });
223
+ const finalName = process.platform === 'win32' ? 'frankenphp.exe' : 'frankenphp';
224
+ const finalExecutable = path.join(currentDir, finalName);
225
+ await fs.copyFile(executablePath, finalExecutable);
226
+ if (process.platform !== 'win32') {
227
+ await fs.chmod(finalExecutable, 0o755).catch(() => undefined);
228
+ }
229
+ const manifest = {
230
+ id: 'frankenphp',
231
+ label: 'Pixcode PHP runtime',
232
+ provider: 'FrankenPHP',
233
+ version: release?.tag_name || 'custom',
234
+ executablePath: finalExecutable,
235
+ sourceUrl: asset.browser_download_url,
236
+ installedAt: new Date().toISOString(),
237
+ };
238
+ await fs.writeFile(manifestPath('frankenphp', env), JSON.stringify(manifest, null, 2));
239
+ await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
240
+ return manifest;
241
+ }
242
+ export async function getManagedRuntimeStatus(id, options = {}) {
243
+ const env = options.env || process.env;
244
+ if (id !== 'frankenphp') {
245
+ return {
246
+ id,
247
+ status: 'unsupported',
248
+ installable: false,
249
+ reason: 'Pixcode does not have a managed runtime for this stack yet.',
250
+ };
251
+ }
252
+ const spawnEnv = buildCliSpawnEnv(env);
253
+ const systemExecutable = findExecutableOnPath('frankenphp', spawnEnv);
254
+ if (systemExecutable) {
255
+ return {
256
+ id,
257
+ label: 'FrankenPHP',
258
+ status: 'system',
259
+ installable: true,
260
+ executablePath: systemExecutable,
261
+ };
262
+ }
263
+ const manifest = await readManifest(id, env);
264
+ if (manifest) {
265
+ return {
266
+ id,
267
+ label: manifest.label || 'Pixcode PHP runtime',
268
+ status: 'installed',
269
+ installable: true,
270
+ executablePath: manifest.executablePath,
271
+ version: manifest.version,
272
+ };
273
+ }
274
+ return {
275
+ id,
276
+ label: 'Pixcode PHP runtime',
277
+ provider: 'FrankenPHP',
278
+ status: 'missing',
279
+ installable: true,
280
+ reason: 'Pixcode will prepare a local PHP runtime automatically before starting this project.',
281
+ };
282
+ }
283
+ export async function ensureManagedRuntime(id, options = {}) {
284
+ const env = options.env || process.env;
285
+ const status = await getManagedRuntimeStatus(id, { env });
286
+ if (status.executablePath)
287
+ return status;
288
+ if (!status.installable) {
289
+ throw new Error(status.reason || 'This runtime cannot be prepared automatically.');
290
+ }
291
+ const lockKey = `${runtimesHome(env)}:${id}`;
292
+ if (installLocks.has(lockKey))
293
+ return installLocks.get(lockKey);
294
+ const installPromise = (async () => {
295
+ if (id === 'frankenphp') {
296
+ const manifest = await installFrankenPhp(env);
297
+ return {
298
+ id,
299
+ label: manifest.label,
300
+ status: 'installed',
301
+ installable: true,
302
+ executablePath: manifest.executablePath,
303
+ version: manifest.version,
304
+ };
305
+ }
306
+ throw new Error(`Unsupported managed runtime: ${id}`);
307
+ })().finally(() => {
308
+ installLocks.delete(lockKey);
309
+ });
310
+ installLocks.set(lockKey, installPromise);
311
+ return installPromise;
312
+ }
313
+ //# sourceMappingURL=managed-runtimes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managed-runtimes.js","sourceRoot":"","sources":["../../../server/services/managed-runtimes.js"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelbyte-software/pixcode",
3
- "version": "1.40.3",
3
+ "version": "1.40.4",
4
4
  "description": "Self-hosted AI coding agent control room for Claude Code, Cursor CLI, OpenAI Codex, Gemini CLI, Qwen Code, and OpenCode with chat, files, shell, Git, orchestration, API keys, Telegram, MCP, plugins, themes, and desktop/server deployment.",
5
5
  "type": "module",
6
6
  "main": "dist-server/server/index.js",
@@ -83,11 +83,25 @@ assert.ok(
83
83
  liveViewPanel.includes('targetUnavailableReason') && liveViewPanel.includes('liveView.runnerUnavailable'),
84
84
  'Live View panel should show a clear unavailable-runner message before launching a missing runtime.',
85
85
  );
86
+ assert.ok(
87
+ liveViewPanel.includes('managedRuntime') && liveViewPanel.includes('liveView.managedRuntimePreparing'),
88
+ 'Live View panel should explain that Pixcode can prepare managed runtimes automatically.',
89
+ );
86
90
  assert.ok(
87
91
  liveViewPanel.includes("runAction('restart')"),
88
92
  'Live View panel should expose a restart action for failed process runners.',
89
93
  );
90
94
 
95
+ const managedRuntimes = await read('server/services/managed-runtimes.js');
96
+ assert.ok(
97
+ managedRuntimes.includes("process.platform === 'win32'") && managedRuntimes.includes("process.platform === 'darwin'") && managedRuntimes.includes("process.platform === 'linux'"),
98
+ 'Managed runtime selection should explicitly handle Windows, macOS, and Linux assets.',
99
+ );
100
+ assert.ok(
101
+ managedRuntimes.includes('extractZip') && managedRuntimes.includes('extractTarGz'),
102
+ 'Managed runtime installation should handle common Windows zip and macOS/Linux tarball assets.',
103
+ );
104
+
91
105
  const serverIndex = await read('server/index.js');
92
106
  assert.ok(
93
107
  serverIndex.includes("app.use('/api/live-view', authenticateToken, liveViewRoutes)"),
@@ -148,12 +162,14 @@ const phpMissingRuntimeTarget = await detectLiveViewTarget(phpProject, {
148
162
  PATH: '',
149
163
  },
150
164
  });
151
- assert.equal(phpMissingRuntimeTarget.available, false, 'PHP projects should not be marked runnable when php is missing from PATH.');
165
+ assert.equal(phpMissingRuntimeTarget.available, true, 'PHP projects should remain runnable through a Pixcode-managed runtime when php is missing from PATH.');
152
166
  assert.equal(phpMissingRuntimeTarget.framework, 'PHP', 'Missing PHP runtime diagnostics should keep the detected framework.');
153
- assert.match(
154
- phpMissingRuntimeTarget.reason || '',
155
- /php executable was not found/i,
156
- 'Missing PHP runtime diagnostics should explain that php is not available.',
167
+ assert.equal(phpMissingRuntimeTarget.managedRuntime?.id, 'frankenphp', 'Missing PHP should select the Pixcode-managed FrankenPHP runtime.');
168
+ assert.equal(phpMissingRuntimeTarget.managedRuntime?.status, 'missing', 'Missing PHP should report that the managed runtime still needs preparation.');
169
+ assert.equal(phpMissingRuntimeTarget.command?.id, 'frankenphp-php-server', 'Missing PHP should use a managed FrankenPHP server command.');
170
+ assert.ok(
171
+ !/PATH/i.test(phpMissingRuntimeTarget.reason || ''),
172
+ 'Missing PHP should use product language instead of exposing PATH setup as the primary message.',
157
173
  );
158
174
 
159
175
  const staticSession = await startLiveView('static-smoke', staticProject);
@@ -50,8 +50,8 @@ function buildLiveViewSuggestions(session, reason) {
50
50
  const suggestions = [];
51
51
 
52
52
  if (framework.includes('php')) {
53
- suggestions.push('Run `php --version` in the same machine and make sure the PHP executable is available in PATH.');
54
- suggestions.push('If PHP is installed outside PATH, use Live View custom command with the full php executable path.');
53
+ suggestions.push('Pixcode can prepare a local PHP runtime automatically and keep it under your user profile.');
54
+ suggestions.push('If the automatic runtime download fails, check the Live View panel logs and retry.');
55
55
  suggestions.push('Check that the project has an index.php or a valid PHP router file in the selected project root.');
56
56
  } else if (
57
57
  framework.includes('javascript')
@@ -4,6 +4,8 @@ import { promises as fs } from 'node:fs';
4
4
  import net from 'node:net';
5
5
  import path from 'node:path';
6
6
 
7
+ import { ensureManagedRuntime, getManagedRuntimeStatus } from './managed-runtimes.js';
8
+
7
9
  const sessionsByProject = new Map();
8
10
  const sessionsByShareId = new Map();
9
11
  const READY_TIMEOUT_MS = 12000;
@@ -92,17 +94,17 @@ function isPathLikeCommand(command) {
92
94
  }
93
95
 
94
96
  function runtimeMissingReason(command, framework) {
95
- const base = `${command} executable was not found in PATH.`;
97
+ const base = `${command} is not available on this machine.`;
96
98
  if (framework === 'PHP' || command === 'php') {
97
- return `${base} Install PHP and add php.exe to PATH, or use a custom command with the full PHP executable path.`;
99
+ return 'Pixcode can prepare a local PHP runtime automatically before starting this project.';
98
100
  }
99
101
  if (command === 'npm' || command === 'pnpm' || command === 'yarn' || command === 'bun') {
100
- return `${base} Install Node.js/package manager support or use a custom command with the full executable path.`;
102
+ return `${base} Pixcode will use its bundled Node runtime when possible; otherwise install the package manager or use a custom command.`;
101
103
  }
102
104
  if (command === 'python' || command === 'python3') {
103
- return `${base} Install Python and add it to PATH, or use a custom command with the full Python executable path.`;
105
+ return `${base} Pixcode does not have a managed Python runtime for this stack yet.`;
104
106
  }
105
- return `${base} Install ${framework || command} or use a custom command with the full executable path.`;
107
+ return `${base} Pixcode does not have a managed ${framework || command} runtime for this stack yet.`;
106
108
  }
107
109
 
108
110
  async function checkCommandAvailability(command, env = process.env) {
@@ -173,6 +175,25 @@ function buildPackageCommand(packageManager, scriptName, id, label, framework, e
173
175
  };
174
176
  }
175
177
 
178
+ function buildManagedPhpCommand(runtimeStatus) {
179
+ const executable = runtimeStatus?.executablePath || 'frankenphp';
180
+ return {
181
+ id: 'frankenphp-php-server',
182
+ label: 'Pixcode PHP runtime',
183
+ framework: 'PHP',
184
+ command: executable,
185
+ args: ['php-server', '-r', '.'],
186
+ displayCommand: `${executable} php-server -r .`,
187
+ env: {
188
+ SERVER_NAME: 'http://127.0.0.1:$PORT',
189
+ },
190
+ managedRuntime: {
191
+ id: 'frankenphp',
192
+ status: runtimeStatus?.status || 'missing',
193
+ },
194
+ };
195
+ }
196
+
176
197
  function detectPackageCommand(packageJson, packageManager) {
177
198
  const scripts = packageJson.scripts || {};
178
199
  const devScript = String(scripts.dev || '');
@@ -243,9 +264,22 @@ function withPort(command, port) {
243
264
  ...command,
244
265
  args: command.args.map((arg) => arg.replaceAll('$PORT', String(port))),
245
266
  displayCommand: command.displayCommand.replaceAll('$PORT', String(port)),
267
+ env: command.env
268
+ ? Object.fromEntries(Object.entries(command.env).map(([key, value]) => [
269
+ key,
270
+ String(value).replaceAll('$PORT', String(port)),
271
+ ]))
272
+ : undefined,
246
273
  };
247
274
  }
248
275
 
276
+ function shouldUseShell(command) {
277
+ if (command.shell) return true;
278
+ if (process.platform !== 'win32') return false;
279
+ if (path.isAbsolute(command.command) && command.command.toLowerCase().endsWith('.exe')) return false;
280
+ return true;
281
+ }
282
+
249
283
  async function detectStaticRoot(projectPath) {
250
284
  const candidates = [
251
285
  projectPath,
@@ -382,6 +416,22 @@ export async function detectLiveViewTarget(projectPath, options = {}) {
382
416
  if (processCommand) {
383
417
  const runtimeAvailable = await checkCommandAvailability(processCommand.command, options.env || process.env);
384
418
  if (!runtimeAvailable) {
419
+ if (processCommand.framework === 'PHP' || processCommand.command === 'php') {
420
+ const managedRuntime = await getManagedRuntimeStatus('frankenphp', { env: options.env || process.env });
421
+ const command = buildManagedPhpCommand(managedRuntime);
422
+ return {
423
+ available: true,
424
+ kind: 'process',
425
+ label: command.label,
426
+ framework: command.framework,
427
+ command,
428
+ managedRuntime,
429
+ reason: managedRuntime.status === 'missing'
430
+ ? 'Pixcode will prepare a local PHP runtime automatically before starting this project.'
431
+ : 'Pixcode will run this project with its managed PHP runtime.',
432
+ };
433
+ }
434
+
385
435
  return {
386
436
  available: false,
387
437
  kind: 'process',
@@ -483,6 +533,7 @@ function publicSession(session) {
483
533
  label: session.command.label,
484
534
  displayCommand: session.command.displayCommand,
485
535
  } : null,
536
+ managedRuntime: session.managedRuntime || null,
486
537
  port: session.port,
487
538
  upstreamUrl: session.upstreamUrl,
488
539
  startedAt: session.startedAt,
@@ -564,7 +615,16 @@ export async function startLiveView(projectName, projectPath, options = {}) {
564
615
  }
565
616
 
566
617
  const port = await findFreePort();
567
- const command = withPort(target.command, port);
618
+ let runtimeStatus = target.managedRuntime || target.command?.managedRuntime || null;
619
+ let targetCommand = target.command;
620
+ if (runtimeStatus?.id && runtimeStatus.status !== 'system' && runtimeStatus.status !== 'installed') {
621
+ runtimeStatus = await ensureManagedRuntime(runtimeStatus.id);
622
+ if (runtimeStatus.id === 'frankenphp') {
623
+ targetCommand = buildManagedPhpCommand(runtimeStatus);
624
+ }
625
+ }
626
+
627
+ const command = withPort(targetCommand, port);
568
628
  const session = {
569
629
  projectName,
570
630
  projectPath,
@@ -574,6 +634,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
574
634
  framework: target.framework,
575
635
  label: target.label,
576
636
  command,
637
+ managedRuntime: runtimeStatus,
577
638
  port,
578
639
  host: '127.0.0.1',
579
640
  upstreamUrl: `http://127.0.0.1:${port}`,
@@ -589,6 +650,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
589
650
 
590
651
  const env = {
591
652
  ...process.env,
653
+ ...(command.env || {}),
592
654
  PORT: String(port),
593
655
  HOST: '127.0.0.1',
594
656
  VITE_HOST: '127.0.0.1',
@@ -599,7 +661,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
599
661
  const child = spawn(command.command, command.args, {
600
662
  cwd: projectPath,
601
663
  env,
602
- shell: Boolean(command.shell) || process.platform === 'win32',
664
+ shell: shouldUseShell(command),
603
665
  stdio: ['ignore', 'pipe', 'pipe'],
604
666
  });
605
667