@pixelbyte-software/pixcode 1.40.2 → 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.2",
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",
@@ -79,11 +79,29 @@ assert.ok(
79
79
  liveViewPanel.includes('sessionError') && liveViewPanel.includes('status.session.error'),
80
80
  'Live View panel should show the actual runner error instead of only an error badge.',
81
81
  );
82
+ assert.ok(
83
+ liveViewPanel.includes('targetUnavailableReason') && liveViewPanel.includes('liveView.runnerUnavailable'),
84
+ 'Live View panel should show a clear unavailable-runner message before launching a missing runtime.',
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
+ );
82
90
  assert.ok(
83
91
  liveViewPanel.includes("runAction('restart')"),
84
92
  'Live View panel should expose a restart action for failed process runners.',
85
93
  );
86
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
+
87
105
  const serverIndex = await read('server/index.js');
88
106
  assert.ok(
89
107
  serverIndex.includes("app.use('/api/live-view', authenticateToken, liveViewRoutes)"),
@@ -108,6 +126,7 @@ const workspace = await mkdtemp(path.join(tmpdir(), 'pixcode-live-view-smoke-'))
108
126
  const staticProject = path.join(workspace, 'static');
109
127
  const viteProject = path.join(workspace, 'vite');
110
128
  const djangoProject = path.join(workspace, 'django');
129
+ const phpProject = path.join(workspace, 'php');
111
130
  await writeFile(path.join(staticProject, 'index.html'), '<main>hello</main>', { recursive: true }).catch(async (error) => {
112
131
  if (error.code !== 'ENOENT') throw error;
113
132
  const { mkdir } = await import('node:fs/promises');
@@ -122,6 +141,8 @@ await writeFile(path.join(viteProject, 'package.json'), JSON.stringify({
122
141
  }, null, 2));
123
142
  await mkdir(djangoProject, { recursive: true });
124
143
  await writeFile(path.join(djangoProject, 'manage.py'), '#!/usr/bin/env python\n');
144
+ await mkdir(phpProject, { recursive: true });
145
+ await writeFile(path.join(phpProject, 'index.php'), '<?php echo "hello";');
125
146
 
126
147
  const staticTarget = await detectLiveViewTarget(staticProject);
127
148
  assert.equal(staticTarget.available, true, 'Static HTML projects should be available.');
@@ -135,6 +156,22 @@ const djangoTarget = await detectLiveViewTarget(djangoProject);
135
156
  assert.equal(djangoTarget.available, true, 'Django projects should be detected from manage.py.');
136
157
  assert.equal(djangoTarget.command?.id, 'python-django', 'Django projects should get a runserver command.');
137
158
 
159
+ const phpMissingRuntimeTarget = await detectLiveViewTarget(phpProject, {
160
+ env: {
161
+ ...process.env,
162
+ PATH: '',
163
+ },
164
+ });
165
+ assert.equal(phpMissingRuntimeTarget.available, true, 'PHP projects should remain runnable through a Pixcode-managed runtime when php is missing from PATH.');
166
+ assert.equal(phpMissingRuntimeTarget.framework, 'PHP', 'Missing PHP runtime diagnostics should keep the detected framework.');
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.',
173
+ );
174
+
138
175
  const staticSession = await startLiveView('static-smoke', staticProject);
139
176
  assert.equal(staticSession.status, 'running', 'Static Live View should start without a child process.');
140
177
  assert.match(staticSession.sharePath, /^\/live\/[a-f0-9]{24}\/$/, 'Live View should expose a random public share path.');
@@ -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,10 +4,13 @@ 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;
10
12
  const LOG_LIMIT = 200;
13
+ const RUNTIME_CHECK_TIMEOUT_MS = 1800;
11
14
 
12
15
  const localUrlRegex = /https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[[^\]]+\])(?::(\d+))?[^\s"'<>]*/i;
13
16
 
@@ -78,6 +81,88 @@ function buildDisplayCommand(command, args) {
78
81
  return [command, ...args].join(' ');
79
82
  }
80
83
 
84
+ function quoteForPosixShell(value) {
85
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
86
+ }
87
+
88
+ function quoteForWindowsShell(value) {
89
+ return `"${String(value).replaceAll('"', '""')}"`;
90
+ }
91
+
92
+ function isPathLikeCommand(command) {
93
+ return path.isAbsolute(command) || command.includes('/') || command.includes('\\');
94
+ }
95
+
96
+ function runtimeMissingReason(command, framework) {
97
+ const base = `${command} is not available on this machine.`;
98
+ if (framework === 'PHP' || command === 'php') {
99
+ return 'Pixcode can prepare a local PHP runtime automatically before starting this project.';
100
+ }
101
+ if (command === 'npm' || command === 'pnpm' || command === 'yarn' || command === 'bun') {
102
+ return `${base} Pixcode will use its bundled Node runtime when possible; otherwise install the package manager or use a custom command.`;
103
+ }
104
+ if (command === 'python' || command === 'python3') {
105
+ return `${base} Pixcode does not have a managed Python runtime for this stack yet.`;
106
+ }
107
+ return `${base} Pixcode does not have a managed ${framework || command} runtime for this stack yet.`;
108
+ }
109
+
110
+ async function checkCommandAvailability(command, env = process.env) {
111
+ if (!command || command.includes('\n') || command.includes('\r')) return true;
112
+
113
+ if (isPathLikeCommand(command)) {
114
+ try {
115
+ await fs.access(command);
116
+ return true;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ const checker = process.platform === 'win32'
123
+ ? {
124
+ command: process.env.ComSpec || 'cmd.exe',
125
+ args: ['/d', '/s', '/c', `where ${quoteForWindowsShell(command)}`],
126
+ }
127
+ : {
128
+ command: '/bin/sh',
129
+ args: ['-lc', `command -v ${quoteForPosixShell(command)}`],
130
+ };
131
+
132
+ return new Promise((resolve) => {
133
+ let settled = false;
134
+ let child = null;
135
+ const finish = (available) => {
136
+ if (settled) return;
137
+ settled = true;
138
+ clearTimeout(timer);
139
+ resolve(available);
140
+ };
141
+
142
+ const timer = setTimeout(() => {
143
+ try {
144
+ child?.kill();
145
+ } catch {
146
+ // Ignore a raced process exit.
147
+ }
148
+ finish(true);
149
+ }, RUNTIME_CHECK_TIMEOUT_MS);
150
+
151
+ child = spawn(checker.command, checker.args, {
152
+ env,
153
+ stdio: 'ignore',
154
+ windowsHide: true,
155
+ });
156
+
157
+ child.on('error', (error) => {
158
+ finish(error?.code === 'ENOENT' ? false : true);
159
+ });
160
+ child.on('exit', (code) => {
161
+ finish(code === 0);
162
+ });
163
+ });
164
+ }
165
+
81
166
  function buildPackageCommand(packageManager, scriptName, id, label, framework, extraArgs = []) {
82
167
  const args = packageRunArgs(packageManager, scriptName, extraArgs);
83
168
  return {
@@ -90,6 +175,25 @@ function buildPackageCommand(packageManager, scriptName, id, label, framework, e
90
175
  };
91
176
  }
92
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
+
93
197
  function detectPackageCommand(packageJson, packageManager) {
94
198
  const scripts = packageJson.scripts || {};
95
199
  const devScript = String(scripts.dev || '');
@@ -160,9 +264,22 @@ function withPort(command, port) {
160
264
  ...command,
161
265
  args: command.args.map((arg) => arg.replaceAll('$PORT', String(port))),
162
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,
163
273
  };
164
274
  }
165
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
+
166
283
  async function detectStaticRoot(projectPath) {
167
284
  const candidates = [
168
285
  projectPath,
@@ -286,7 +403,7 @@ async function detectProcessCommand(projectPath) {
286
403
  return null;
287
404
  }
288
405
 
289
- export async function detectLiveViewTarget(projectPath) {
406
+ export async function detectLiveViewTarget(projectPath, options = {}) {
290
407
  if (!projectPath || !(await dirExists(projectPath))) {
291
408
  return {
292
409
  available: false,
@@ -297,6 +414,35 @@ export async function detectLiveViewTarget(projectPath) {
297
414
 
298
415
  const processCommand = await detectProcessCommand(projectPath);
299
416
  if (processCommand) {
417
+ const runtimeAvailable = await checkCommandAvailability(processCommand.command, options.env || process.env);
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
+
435
+ return {
436
+ available: false,
437
+ kind: 'process',
438
+ label: processCommand.label,
439
+ framework: processCommand.framework,
440
+ command: processCommand,
441
+ missingRuntime: processCommand.command,
442
+ reason: runtimeMissingReason(processCommand.command, processCommand.framework),
443
+ };
444
+ }
445
+
300
446
  return {
301
447
  available: true,
302
448
  kind: 'process',
@@ -387,6 +533,7 @@ function publicSession(session) {
387
533
  label: session.command.label,
388
534
  displayCommand: session.command.displayCommand,
389
535
  } : null,
536
+ managedRuntime: session.managedRuntime || null,
390
537
  port: session.port,
391
538
  upstreamUrl: session.upstreamUrl,
392
539
  startedAt: session.startedAt,
@@ -468,7 +615,16 @@ export async function startLiveView(projectName, projectPath, options = {}) {
468
615
  }
469
616
 
470
617
  const port = await findFreePort();
471
- 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);
472
628
  const session = {
473
629
  projectName,
474
630
  projectPath,
@@ -478,6 +634,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
478
634
  framework: target.framework,
479
635
  label: target.label,
480
636
  command,
637
+ managedRuntime: runtimeStatus,
481
638
  port,
482
639
  host: '127.0.0.1',
483
640
  upstreamUrl: `http://127.0.0.1:${port}`,
@@ -493,6 +650,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
493
650
 
494
651
  const env = {
495
652
  ...process.env,
653
+ ...(command.env || {}),
496
654
  PORT: String(port),
497
655
  HOST: '127.0.0.1',
498
656
  VITE_HOST: '127.0.0.1',
@@ -503,7 +661,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
503
661
  const child = spawn(command.command, command.args, {
504
662
  cwd: projectPath,
505
663
  env,
506
- shell: Boolean(command.shell) || process.platform === 'win32',
664
+ shell: shouldUseShell(command),
507
665
  stdio: ['ignore', 'pipe', 'pipe'],
508
666
  });
509
667