@pixelbyte-software/pixcode 1.40.3 → 1.40.5
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-Cc_ji8dI.js → index-DROaSsD_.js} +50 -50
- package/dist/assets/index-LZgOC7Q_.css +32 -0
- 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/install-jobs.js +1 -1
- package/dist-server/server/services/install-jobs.js.map +1 -1
- package/dist-server/server/services/live-view.js +125 -8
- package/dist-server/server/services/live-view.js.map +1 -1
- package/dist-server/server/services/managed-runtimes.js +409 -0
- package/dist-server/server/services/managed-runtimes.js.map +1 -0
- package/package.json +1 -1
- package/scripts/smoke/live-view-integration.mjs +67 -6
- package/server/routes/live-view.js +2 -2
- package/server/services/install-jobs.js +1 -1
- package/server/services/live-view.js +130 -8
- package/server/services/managed-runtimes.js +439 -0
- package/dist/assets/index-I_EBn-XU.css +0 -32
|
@@ -4,6 +4,9 @@ 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 { buildCliSpawnEnv } from './install-jobs.js';
|
|
8
|
+
import { ensureManagedRuntime, getManagedRuntimeStatus } from './managed-runtimes.js';
|
|
9
|
+
|
|
7
10
|
const sessionsByProject = new Map();
|
|
8
11
|
const sessionsByShareId = new Map();
|
|
9
12
|
const READY_TIMEOUT_MS = 12000;
|
|
@@ -92,17 +95,17 @@ function isPathLikeCommand(command) {
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
function runtimeMissingReason(command, framework) {
|
|
95
|
-
const base = `${command}
|
|
98
|
+
const base = `${command} is not available on this machine.`;
|
|
96
99
|
if (framework === 'PHP' || command === 'php') {
|
|
97
|
-
return
|
|
100
|
+
return 'Pixcode can prepare a local PHP runtime automatically before starting this project.';
|
|
98
101
|
}
|
|
99
102
|
if (command === 'npm' || command === 'pnpm' || command === 'yarn' || command === 'bun') {
|
|
100
|
-
return `${base}
|
|
103
|
+
return `${base} Pixcode can prepare a local Node package runner automatically before starting this project.`;
|
|
101
104
|
}
|
|
102
105
|
if (command === 'python' || command === 'python3') {
|
|
103
|
-
return `${base}
|
|
106
|
+
return `${base} Pixcode does not have a managed Python runtime for this stack yet.`;
|
|
104
107
|
}
|
|
105
|
-
return `${base}
|
|
108
|
+
return `${base} Pixcode does not have a managed ${framework || command} runtime for this stack yet.`;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
async function checkCommandAvailability(command, env = process.env) {
|
|
@@ -167,12 +170,63 @@ function buildPackageCommand(packageManager, scriptName, id, label, framework, e
|
|
|
167
170
|
id,
|
|
168
171
|
label,
|
|
169
172
|
framework,
|
|
173
|
+
packageManager,
|
|
174
|
+
scriptName,
|
|
175
|
+
extraArgs,
|
|
170
176
|
command: packageManager,
|
|
171
177
|
args,
|
|
172
178
|
displayCommand: buildDisplayCommand(packageManager, args),
|
|
173
179
|
};
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
function isPackageManagerCommand(command) {
|
|
183
|
+
return command === 'npm' || command === 'pnpm' || command === 'yarn' || command === 'bun';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildManagedPackageCommand(command, runtimeStatus) {
|
|
187
|
+
const npmArgs = command.scriptName
|
|
188
|
+
? packageRunArgs('npm', command.scriptName, command.extraArgs || [])
|
|
189
|
+
: command.args;
|
|
190
|
+
const runtimeExecutable = runtimeStatus?.executablePath || null;
|
|
191
|
+
const commandExecutable = runtimeExecutable
|
|
192
|
+
? (runtimeStatus?.runner === 'node' || runtimeExecutable.endsWith('.js') ? process.execPath : runtimeExecutable)
|
|
193
|
+
: command.command;
|
|
194
|
+
const args = runtimeExecutable && (runtimeStatus?.runner === 'node' || runtimeExecutable.endsWith('.js'))
|
|
195
|
+
? [runtimeExecutable, ...npmArgs]
|
|
196
|
+
: npmArgs;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...command,
|
|
200
|
+
packageManager: 'npm',
|
|
201
|
+
command: commandExecutable,
|
|
202
|
+
args,
|
|
203
|
+
displayCommand: buildDisplayCommand('npm', npmArgs),
|
|
204
|
+
managedRuntime: {
|
|
205
|
+
id: 'npm',
|
|
206
|
+
status: runtimeStatus?.status || 'missing',
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildManagedPhpCommand(runtimeStatus) {
|
|
212
|
+
const executable = runtimeStatus?.executablePath || 'frankenphp';
|
|
213
|
+
return {
|
|
214
|
+
id: 'frankenphp-php-server',
|
|
215
|
+
label: 'Pixcode PHP runtime',
|
|
216
|
+
framework: 'PHP',
|
|
217
|
+
command: executable,
|
|
218
|
+
args: ['php-server', '-r', '.'],
|
|
219
|
+
displayCommand: `${executable} php-server -r .`,
|
|
220
|
+
env: {
|
|
221
|
+
SERVER_NAME: 'http://127.0.0.1:$PORT',
|
|
222
|
+
},
|
|
223
|
+
managedRuntime: {
|
|
224
|
+
id: 'frankenphp',
|
|
225
|
+
status: runtimeStatus?.status || 'missing',
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
176
230
|
function detectPackageCommand(packageJson, packageManager) {
|
|
177
231
|
const scripts = packageJson.scripts || {};
|
|
178
232
|
const devScript = String(scripts.dev || '');
|
|
@@ -243,9 +297,22 @@ function withPort(command, port) {
|
|
|
243
297
|
...command,
|
|
244
298
|
args: command.args.map((arg) => arg.replaceAll('$PORT', String(port))),
|
|
245
299
|
displayCommand: command.displayCommand.replaceAll('$PORT', String(port)),
|
|
300
|
+
env: command.env
|
|
301
|
+
? Object.fromEntries(Object.entries(command.env).map(([key, value]) => [
|
|
302
|
+
key,
|
|
303
|
+
String(value).replaceAll('$PORT', String(port)),
|
|
304
|
+
]))
|
|
305
|
+
: undefined,
|
|
246
306
|
};
|
|
247
307
|
}
|
|
248
308
|
|
|
309
|
+
function shouldUseShell(command) {
|
|
310
|
+
if (command.shell) return true;
|
|
311
|
+
if (process.platform !== 'win32') return false;
|
|
312
|
+
if (path.isAbsolute(command.command) && command.command.toLowerCase().endsWith('.exe')) return false;
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
249
316
|
async function detectStaticRoot(projectPath) {
|
|
250
317
|
const candidates = [
|
|
251
318
|
projectPath,
|
|
@@ -380,6 +447,44 @@ export async function detectLiveViewTarget(projectPath, options = {}) {
|
|
|
380
447
|
|
|
381
448
|
const processCommand = await detectProcessCommand(projectPath);
|
|
382
449
|
if (processCommand) {
|
|
450
|
+
if (isPackageManagerCommand(processCommand.command)) {
|
|
451
|
+
const managedRuntime = await getManagedRuntimeStatus('npm', {
|
|
452
|
+
env: options.env || process.env,
|
|
453
|
+
preferManaged: true,
|
|
454
|
+
});
|
|
455
|
+
const command = buildManagedPackageCommand(processCommand, managedRuntime);
|
|
456
|
+
return {
|
|
457
|
+
available: true,
|
|
458
|
+
kind: 'process',
|
|
459
|
+
label: processCommand.label,
|
|
460
|
+
framework: processCommand.framework,
|
|
461
|
+
command,
|
|
462
|
+
managedRuntime,
|
|
463
|
+
reason: managedRuntime.status === 'missing'
|
|
464
|
+
? 'Pixcode will prepare a local Node package runner automatically before starting this project.'
|
|
465
|
+
: 'Pixcode will run this project with its managed Node package runner.',
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (processCommand.framework === 'PHP' || processCommand.command === 'php') {
|
|
470
|
+
const managedRuntime = await getManagedRuntimeStatus('frankenphp', {
|
|
471
|
+
env: options.env || process.env,
|
|
472
|
+
preferManaged: true,
|
|
473
|
+
});
|
|
474
|
+
const command = buildManagedPhpCommand(managedRuntime);
|
|
475
|
+
return {
|
|
476
|
+
available: true,
|
|
477
|
+
kind: 'process',
|
|
478
|
+
label: command.label,
|
|
479
|
+
framework: command.framework,
|
|
480
|
+
command,
|
|
481
|
+
managedRuntime,
|
|
482
|
+
reason: managedRuntime.status === 'missing'
|
|
483
|
+
? 'Pixcode will prepare a local PHP runtime automatically before starting this project.'
|
|
484
|
+
: 'Pixcode will run this project with its managed PHP runtime.',
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
383
488
|
const runtimeAvailable = await checkCommandAvailability(processCommand.command, options.env || process.env);
|
|
384
489
|
if (!runtimeAvailable) {
|
|
385
490
|
return {
|
|
@@ -483,6 +588,7 @@ function publicSession(session) {
|
|
|
483
588
|
label: session.command.label,
|
|
484
589
|
displayCommand: session.command.displayCommand,
|
|
485
590
|
} : null,
|
|
591
|
+
managedRuntime: session.managedRuntime || null,
|
|
486
592
|
port: session.port,
|
|
487
593
|
upstreamUrl: session.upstreamUrl,
|
|
488
594
|
startedAt: session.startedAt,
|
|
@@ -564,7 +670,20 @@ export async function startLiveView(projectName, projectPath, options = {}) {
|
|
|
564
670
|
}
|
|
565
671
|
|
|
566
672
|
const port = await findFreePort();
|
|
567
|
-
|
|
673
|
+
let runtimeStatus = target.managedRuntime || target.command?.managedRuntime || null;
|
|
674
|
+
let targetCommand = target.command;
|
|
675
|
+
if (runtimeStatus?.id && runtimeStatus.status !== 'system' && runtimeStatus.status !== 'installed') {
|
|
676
|
+
runtimeStatus = await ensureManagedRuntime(runtimeStatus.id, {
|
|
677
|
+
preferManaged: runtimeStatus.id === 'frankenphp' || runtimeStatus.id === 'npm',
|
|
678
|
+
});
|
|
679
|
+
if (runtimeStatus.id === 'frankenphp') {
|
|
680
|
+
targetCommand = buildManagedPhpCommand(runtimeStatus);
|
|
681
|
+
} else if (runtimeStatus.id === 'npm') {
|
|
682
|
+
targetCommand = buildManagedPackageCommand(targetCommand, runtimeStatus);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const command = withPort(targetCommand, port);
|
|
568
687
|
const session = {
|
|
569
688
|
projectName,
|
|
570
689
|
projectPath,
|
|
@@ -574,6 +693,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
|
|
|
574
693
|
framework: target.framework,
|
|
575
694
|
label: target.label,
|
|
576
695
|
command,
|
|
696
|
+
managedRuntime: runtimeStatus,
|
|
577
697
|
port,
|
|
578
698
|
host: '127.0.0.1',
|
|
579
699
|
upstreamUrl: `http://127.0.0.1:${port}`,
|
|
@@ -588,7 +708,9 @@ export async function startLiveView(projectName, projectPath, options = {}) {
|
|
|
588
708
|
};
|
|
589
709
|
|
|
590
710
|
const env = {
|
|
591
|
-
...process.env,
|
|
711
|
+
...buildCliSpawnEnv(process.env),
|
|
712
|
+
...(command.env || {}),
|
|
713
|
+
...(process.versions.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
|
592
714
|
PORT: String(port),
|
|
593
715
|
HOST: '127.0.0.1',
|
|
594
716
|
VITE_HOST: '127.0.0.1',
|
|
@@ -599,7 +721,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
|
|
|
599
721
|
const child = spawn(command.command, command.args, {
|
|
600
722
|
cwd: projectPath,
|
|
601
723
|
env,
|
|
602
|
-
shell:
|
|
724
|
+
shell: shouldUseShell(command),
|
|
603
725
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
604
726
|
});
|
|
605
727
|
|
|
@@ -0,0 +1,439 @@
|
|
|
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
|
+
|
|
6
|
+
import * as tar from 'tar';
|
|
7
|
+
|
|
8
|
+
import { buildCliSpawnEnv, findExecutableOnPath, resolveNpmCommand } from './install-jobs.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_RUNTIMES_HOME = path.join(os.homedir(), '.pixcode', 'runtimes');
|
|
11
|
+
const MANIFEST_FILE = 'pixcode-runtime.json';
|
|
12
|
+
const installLocks = new Map();
|
|
13
|
+
|
|
14
|
+
function runtimesHome(env = process.env) {
|
|
15
|
+
return env.PIXCODE_MANAGED_RUNTIMES_HOME || DEFAULT_RUNTIMES_HOME;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function runtimeDir(id, env = process.env) {
|
|
19
|
+
return path.join(runtimesHome(env), id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function manifestPath(id, env = process.env) {
|
|
23
|
+
return path.join(runtimeDir(id, env), MANIFEST_FILE);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fileExists(filePath) {
|
|
27
|
+
try {
|
|
28
|
+
const stats = await fs.stat(filePath);
|
|
29
|
+
return stats.isFile();
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readManifest(id, env = process.env) {
|
|
36
|
+
try {
|
|
37
|
+
const content = await fs.readFile(manifestPath(id, env), 'utf8');
|
|
38
|
+
const manifest = JSON.parse(content);
|
|
39
|
+
if (manifest?.executablePath && await fileExists(manifest.executablePath)) {
|
|
40
|
+
return manifest;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Missing or malformed manifests are treated as not installed.
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function platformTokens() {
|
|
49
|
+
if (process.platform === 'win32') return ['windows'];
|
|
50
|
+
if (process.platform === 'darwin') return ['mac', 'darwin'];
|
|
51
|
+
if (process.platform === 'linux') return ['linux'];
|
|
52
|
+
return [process.platform];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function archTokens() {
|
|
56
|
+
if (process.arch === 'x64') return ['x86_64', 'amd64', 'x64'];
|
|
57
|
+
if (process.arch === 'arm64') return ['aarch64', 'arm64'];
|
|
58
|
+
return [process.arch];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function scoreFrankenPhpAsset(assetName) {
|
|
62
|
+
const name = assetName.toLowerCase();
|
|
63
|
+
if (!name.includes('frankenphp')) return -1;
|
|
64
|
+
if (name.includes('debug')) return -1;
|
|
65
|
+
if (!platformTokens().some((token) => name.includes(token))) return -1;
|
|
66
|
+
if (!archTokens().some((token) => name.includes(token))) return -1;
|
|
67
|
+
|
|
68
|
+
let score = 10;
|
|
69
|
+
if (process.platform === 'win32' && name.endsWith('.zip')) score += 10;
|
|
70
|
+
if (process.platform !== 'win32' && !name.endsWith('.zip')) score += 10;
|
|
71
|
+
if (!name.includes('gnu')) score += 2;
|
|
72
|
+
if (name.endsWith('.tar.gz') || name.endsWith('.tgz')) score += 1;
|
|
73
|
+
return score;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function selectFrankenPhpAsset(release) {
|
|
77
|
+
const assets = Array.isArray(release?.assets) ? release.assets : [];
|
|
78
|
+
const candidates = assets
|
|
79
|
+
.map((asset) => ({ asset, score: scoreFrankenPhpAsset(asset.name || '') }))
|
|
80
|
+
.filter((entry) => entry.score >= 0)
|
|
81
|
+
.sort((a, b) => b.score - a.score);
|
|
82
|
+
|
|
83
|
+
return candidates[0]?.asset || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function fetchJson(url, env = process.env) {
|
|
87
|
+
const headers = {
|
|
88
|
+
Accept: 'application/vnd.github+json',
|
|
89
|
+
'User-Agent': 'Pixcode Live View',
|
|
90
|
+
};
|
|
91
|
+
if (env.GITHUB_TOKEN) headers.Authorization = `Bearer ${env.GITHUB_TOKEN}`;
|
|
92
|
+
|
|
93
|
+
const response = await fetch(url, { headers });
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`Runtime metadata request failed with HTTP ${response.status}`);
|
|
96
|
+
}
|
|
97
|
+
return response.json();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function downloadFile(url, targetFile, env = process.env) {
|
|
101
|
+
const headers = { 'User-Agent': 'Pixcode Live View' };
|
|
102
|
+
if (env.GITHUB_TOKEN && url.includes('github.com')) headers.Authorization = `Bearer ${env.GITHUB_TOKEN}`;
|
|
103
|
+
|
|
104
|
+
const response = await fetch(url, { headers });
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`Runtime download failed with HTTP ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
109
|
+
await fs.writeFile(targetFile, buffer);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function runProcess(command, args, options = {}) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
let stderr = '';
|
|
115
|
+
const child = spawn(command, args, {
|
|
116
|
+
...options,
|
|
117
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
});
|
|
120
|
+
child.stderr.on('data', (chunk) => {
|
|
121
|
+
stderr += chunk.toString();
|
|
122
|
+
});
|
|
123
|
+
child.on('error', reject);
|
|
124
|
+
child.on('close', (code) => {
|
|
125
|
+
if (code === 0) {
|
|
126
|
+
resolve();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
reject(new Error(`${command} exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function extractZip(archivePath, targetDir, env = process.env) {
|
|
135
|
+
if (process.platform === 'win32') {
|
|
136
|
+
const shell = env.ComSpec || process.env.ComSpec || 'powershell.exe';
|
|
137
|
+
const isCmd = shell.toLowerCase().endsWith('cmd.exe');
|
|
138
|
+
if (isCmd) {
|
|
139
|
+
await runProcess('powershell.exe', [
|
|
140
|
+
'-NoProfile',
|
|
141
|
+
'-ExecutionPolicy',
|
|
142
|
+
'Bypass',
|
|
143
|
+
'-Command',
|
|
144
|
+
'Expand-Archive -Force -LiteralPath $args[0] -DestinationPath $args[1]',
|
|
145
|
+
archivePath,
|
|
146
|
+
targetDir,
|
|
147
|
+
], { env });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await runProcess(shell, [
|
|
151
|
+
'-NoProfile',
|
|
152
|
+
'-ExecutionPolicy',
|
|
153
|
+
'Bypass',
|
|
154
|
+
'-Command',
|
|
155
|
+
'Expand-Archive -Force -LiteralPath $args[0] -DestinationPath $args[1]',
|
|
156
|
+
archivePath,
|
|
157
|
+
targetDir,
|
|
158
|
+
], { env });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await runProcess('unzip', ['-q', archivePath, '-d', targetDir], { env });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function extractTarGz(archivePath, targetDir, env = process.env) {
|
|
166
|
+
void env;
|
|
167
|
+
await tar.x({
|
|
168
|
+
file: archivePath,
|
|
169
|
+
cwd: targetDir,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function findRuntimeExecutable(searchRoot, binaryName) {
|
|
174
|
+
const expectedNames = process.platform === 'win32'
|
|
175
|
+
? [`${binaryName}.exe`, binaryName]
|
|
176
|
+
: [binaryName];
|
|
177
|
+
const stack = [searchRoot];
|
|
178
|
+
|
|
179
|
+
while (stack.length > 0) {
|
|
180
|
+
const current = stack.pop();
|
|
181
|
+
const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
182
|
+
for (const entry of entries) {
|
|
183
|
+
const full = path.join(current, entry.name);
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
stack.push(full);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (entry.isFile() && expectedNames.includes(entry.name)) {
|
|
189
|
+
return full;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function installFrankenPhp(env = process.env) {
|
|
198
|
+
const releaseApiUrl = env.PIXCODE_FRANKENPHP_RELEASE_API
|
|
199
|
+
|| 'https://api.github.com/repos/php/frankenphp/releases/latest';
|
|
200
|
+
const release = env.PIXCODE_FRANKENPHP_URL ? null : await fetchJson(releaseApiUrl, env);
|
|
201
|
+
const asset = env.PIXCODE_FRANKENPHP_URL
|
|
202
|
+
? {
|
|
203
|
+
name: path.basename(new URL(env.PIXCODE_FRANKENPHP_URL).pathname),
|
|
204
|
+
browser_download_url: env.PIXCODE_FRANKENPHP_URL,
|
|
205
|
+
}
|
|
206
|
+
: selectFrankenPhpAsset(release);
|
|
207
|
+
|
|
208
|
+
if (!asset?.browser_download_url) {
|
|
209
|
+
throw new Error('No FrankenPHP binary is available for this operating system and CPU architecture.');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const baseDir = runtimeDir('frankenphp', env);
|
|
213
|
+
const stagingDir = path.join(baseDir, `.staging-${Date.now()}`);
|
|
214
|
+
const currentDir = path.join(baseDir, 'current');
|
|
215
|
+
await fs.mkdir(stagingDir, { recursive: true });
|
|
216
|
+
|
|
217
|
+
const archivePath = path.join(stagingDir, asset.name || 'frankenphp');
|
|
218
|
+
await downloadFile(asset.browser_download_url, archivePath, env);
|
|
219
|
+
|
|
220
|
+
let executablePath = archivePath;
|
|
221
|
+
const assetName = (asset.name || '').toLowerCase();
|
|
222
|
+
if (assetName.endsWith('.zip')) {
|
|
223
|
+
await extractZip(archivePath, stagingDir, env);
|
|
224
|
+
executablePath = await findRuntimeExecutable(stagingDir, 'frankenphp');
|
|
225
|
+
if (!executablePath) {
|
|
226
|
+
throw new Error('Downloaded FrankenPHP archive did not contain a frankenphp executable.');
|
|
227
|
+
}
|
|
228
|
+
} else if (assetName.endsWith('.tar.gz') || assetName.endsWith('.tgz')) {
|
|
229
|
+
await extractTarGz(archivePath, stagingDir, env);
|
|
230
|
+
executablePath = await findRuntimeExecutable(stagingDir, 'frankenphp');
|
|
231
|
+
if (!executablePath) {
|
|
232
|
+
throw new Error('Downloaded FrankenPHP archive did not contain a frankenphp executable.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (process.platform !== 'win32') {
|
|
237
|
+
await fs.chmod(executablePath, 0o755).catch(() => undefined);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await fs.rm(currentDir, { recursive: true, force: true });
|
|
241
|
+
await fs.mkdir(currentDir, { recursive: true });
|
|
242
|
+
|
|
243
|
+
const finalName = process.platform === 'win32' ? 'frankenphp.exe' : 'frankenphp';
|
|
244
|
+
const finalExecutable = path.join(currentDir, finalName);
|
|
245
|
+
await fs.copyFile(executablePath, finalExecutable);
|
|
246
|
+
if (process.platform !== 'win32') {
|
|
247
|
+
await fs.chmod(finalExecutable, 0o755).catch(() => undefined);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const manifest = {
|
|
251
|
+
id: 'frankenphp',
|
|
252
|
+
label: 'Pixcode PHP runtime',
|
|
253
|
+
provider: 'FrankenPHP',
|
|
254
|
+
version: release?.tag_name || 'custom',
|
|
255
|
+
executablePath: finalExecutable,
|
|
256
|
+
sourceUrl: asset.browser_download_url,
|
|
257
|
+
installedAt: new Date().toISOString(),
|
|
258
|
+
};
|
|
259
|
+
await fs.writeFile(manifestPath('frankenphp', env), JSON.stringify(manifest, null, 2));
|
|
260
|
+
await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
261
|
+
return manifest;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function installNpmRuntime(env = process.env) {
|
|
265
|
+
const registryUrl = env.PIXCODE_NPM_RUNTIME_REGISTRY
|
|
266
|
+
|| 'https://registry.npmjs.org/npm/latest';
|
|
267
|
+
const metadata = await fetchJson(registryUrl, env);
|
|
268
|
+
const tarballUrl = metadata?.dist?.tarball;
|
|
269
|
+
if (!tarballUrl) {
|
|
270
|
+
throw new Error('No npm runtime tarball is available from the npm registry.');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const baseDir = runtimeDir('npm', env);
|
|
274
|
+
const stagingDir = path.join(baseDir, `.staging-${Date.now()}`);
|
|
275
|
+
const currentDir = path.join(baseDir, 'current');
|
|
276
|
+
await fs.mkdir(stagingDir, { recursive: true });
|
|
277
|
+
|
|
278
|
+
const archivePath = path.join(stagingDir, 'npm-runtime.tgz');
|
|
279
|
+
await downloadFile(tarballUrl, archivePath, env);
|
|
280
|
+
await extractTarGz(archivePath, stagingDir, env);
|
|
281
|
+
|
|
282
|
+
const packageDir = path.join(stagingDir, 'package');
|
|
283
|
+
const executablePath = path.join(packageDir, 'bin', 'npm-cli.js');
|
|
284
|
+
if (!(await fileExists(executablePath))) {
|
|
285
|
+
throw new Error('Downloaded npm runtime did not contain bin/npm-cli.js.');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await fs.rm(currentDir, { recursive: true, force: true });
|
|
289
|
+
await fs.cp(packageDir, currentDir, { recursive: true, force: true });
|
|
290
|
+
|
|
291
|
+
const finalExecutable = path.join(currentDir, 'bin', 'npm-cli.js');
|
|
292
|
+
const manifest = {
|
|
293
|
+
id: 'npm',
|
|
294
|
+
label: 'Pixcode Node package runner',
|
|
295
|
+
provider: 'npm',
|
|
296
|
+
version: metadata?.version || 'latest',
|
|
297
|
+
executablePath: finalExecutable,
|
|
298
|
+
runner: 'node',
|
|
299
|
+
sourceUrl: tarballUrl,
|
|
300
|
+
installedAt: new Date().toISOString(),
|
|
301
|
+
};
|
|
302
|
+
await fs.writeFile(manifestPath('npm', env), JSON.stringify(manifest, null, 2));
|
|
303
|
+
await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
304
|
+
return manifest;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function getManagedRuntimeStatus(id, options = {}) {
|
|
308
|
+
const env = options.env || process.env;
|
|
309
|
+
const preferManaged = Boolean(options.preferManaged);
|
|
310
|
+
if (id !== 'frankenphp' && id !== 'npm') {
|
|
311
|
+
return {
|
|
312
|
+
id,
|
|
313
|
+
status: 'unsupported',
|
|
314
|
+
installable: false,
|
|
315
|
+
reason: 'Pixcode does not have a managed runtime for this stack yet.',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (id === 'npm') {
|
|
320
|
+
if (!preferManaged) {
|
|
321
|
+
const spawnEnv = buildCliSpawnEnv(env);
|
|
322
|
+
const npmExecutable = resolveNpmCommand(spawnEnv);
|
|
323
|
+
if (npmExecutable) {
|
|
324
|
+
return {
|
|
325
|
+
id,
|
|
326
|
+
label: 'npm',
|
|
327
|
+
status: 'system',
|
|
328
|
+
installable: true,
|
|
329
|
+
executablePath: npmExecutable,
|
|
330
|
+
runner: npmExecutable.endsWith('.js') ? 'node' : undefined,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const manifest = await readManifest(id, env);
|
|
336
|
+
if (manifest) {
|
|
337
|
+
return {
|
|
338
|
+
id,
|
|
339
|
+
label: manifest.label || 'Pixcode Node package runner',
|
|
340
|
+
status: 'installed',
|
|
341
|
+
installable: true,
|
|
342
|
+
executablePath: manifest.executablePath,
|
|
343
|
+
runner: manifest.runner || 'node',
|
|
344
|
+
version: manifest.version,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
id,
|
|
350
|
+
label: 'Pixcode Node package runner',
|
|
351
|
+
provider: 'npm',
|
|
352
|
+
status: 'missing',
|
|
353
|
+
installable: true,
|
|
354
|
+
reason: 'Pixcode will prepare a local Node package runner automatically before starting this project.',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!preferManaged) {
|
|
359
|
+
const spawnEnv = buildCliSpawnEnv(env);
|
|
360
|
+
const systemExecutable = findExecutableOnPath('frankenphp', spawnEnv);
|
|
361
|
+
if (systemExecutable) {
|
|
362
|
+
return {
|
|
363
|
+
id,
|
|
364
|
+
label: 'FrankenPHP',
|
|
365
|
+
status: 'system',
|
|
366
|
+
installable: true,
|
|
367
|
+
executablePath: systemExecutable,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const manifest = await readManifest(id, env);
|
|
373
|
+
if (manifest) {
|
|
374
|
+
return {
|
|
375
|
+
id,
|
|
376
|
+
label: manifest.label || 'Pixcode PHP runtime',
|
|
377
|
+
status: 'installed',
|
|
378
|
+
installable: true,
|
|
379
|
+
executablePath: manifest.executablePath,
|
|
380
|
+
version: manifest.version,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
id,
|
|
386
|
+
label: 'Pixcode PHP runtime',
|
|
387
|
+
provider: 'FrankenPHP',
|
|
388
|
+
status: 'missing',
|
|
389
|
+
installable: true,
|
|
390
|
+
reason: 'Pixcode will prepare a local PHP runtime automatically before starting this project.',
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function ensureManagedRuntime(id, options = {}) {
|
|
395
|
+
const env = options.env || process.env;
|
|
396
|
+
const status = await getManagedRuntimeStatus(id, {
|
|
397
|
+
env,
|
|
398
|
+
preferManaged: options.preferManaged,
|
|
399
|
+
});
|
|
400
|
+
if (status.executablePath) return status;
|
|
401
|
+
if (!status.installable) {
|
|
402
|
+
throw new Error(status.reason || 'This runtime cannot be prepared automatically.');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const lockKey = `${runtimesHome(env)}:${id}`;
|
|
406
|
+
if (installLocks.has(lockKey)) return installLocks.get(lockKey);
|
|
407
|
+
|
|
408
|
+
const installPromise = (async () => {
|
|
409
|
+
if (id === 'frankenphp') {
|
|
410
|
+
const manifest = await installFrankenPhp(env);
|
|
411
|
+
return {
|
|
412
|
+
id,
|
|
413
|
+
label: manifest.label,
|
|
414
|
+
status: 'installed',
|
|
415
|
+
installable: true,
|
|
416
|
+
executablePath: manifest.executablePath,
|
|
417
|
+
version: manifest.version,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (id === 'npm') {
|
|
421
|
+
const manifest = await installNpmRuntime(env);
|
|
422
|
+
return {
|
|
423
|
+
id,
|
|
424
|
+
label: manifest.label,
|
|
425
|
+
status: 'installed',
|
|
426
|
+
installable: true,
|
|
427
|
+
executablePath: manifest.executablePath,
|
|
428
|
+
runner: manifest.runner || 'node',
|
|
429
|
+
version: manifest.version,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
throw new Error(`Unsupported managed runtime: ${id}`);
|
|
433
|
+
})().finally(() => {
|
|
434
|
+
installLocks.delete(lockKey);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
installLocks.set(lockKey, installPromise);
|
|
438
|
+
return installPromise;
|
|
439
|
+
}
|