@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.
- package/README.md +3 -0
- package/dist/assets/index-LZgOC7Q_.css +32 -0
- package/dist/assets/{index-D0U34gw1.js → index-pC1RxVq6.js} +55 -55
- package/dist/index.html +2 -2
- package/dist/landing.html +1 -1
- package/dist-server/server/routes/live-view.js +2 -2
- package/dist-server/server/routes/live-view.js.map +1 -1
- package/dist-server/server/services/live-view.js +151 -3
- package/dist-server/server/services/live-view.js.map +1 -1
- package/dist-server/server/services/managed-runtimes.js +313 -0
- package/dist-server/server/services/managed-runtimes.js.map +1 -0
- package/package.json +1 -1
- package/scripts/smoke/live-view-integration.mjs +37 -0
- package/server/routes/live-view.js +2 -2
- package/server/services/live-view.js +161 -3
- package/server/services/managed-runtimes.js +333 -0
- package/dist/assets/index-B0NLmkDr.css +0 -32
|
@@ -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
|
+
"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('
|
|
54
|
-
suggestions.push('If
|
|
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
|
-
|
|
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:
|
|
664
|
+
shell: shouldUseShell(command),
|
|
507
665
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
508
666
|
});
|
|
509
667
|
|