@pixelbyte-software/pixcode 1.41.3 → 1.41.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.
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const root = process.cwd();
8
+
9
+ function read(relativePath) {
10
+ return fs.readFileSync(path.join(root, relativePath), 'utf8');
11
+ }
12
+
13
+ const managerSource = read('server/services/runtime-manager.js');
14
+ assert.match(managerSource, /RUNTIME_DEFINITIONS/, 'Runtime manager should define a central runtime registry.');
15
+ assert.match(managerSource, /node/, 'Runtime manager should include Node.js.');
16
+ assert.match(managerSource, /php/, 'Runtime manager should include PHP.');
17
+ assert.match(managerSource, /python/, 'Runtime manager should include Python.');
18
+ assert.match(managerSource, /go/, 'Runtime manager should include Go.');
19
+ assert.match(managerSource, /java/, 'Runtime manager should include Java.');
20
+ assert.match(managerSource, /rust/, 'Runtime manager should include Rust.');
21
+ assert.match(managerSource, /discoverRuntime/, 'Runtime manager should expose runtime discovery.');
22
+ assert.match(managerSource, /resolveLiveViewRuntime/, 'Runtime manager should expose Live View runtime resolution.');
23
+
24
+ const liveViewSource = read('server/services/live-view.js');
25
+ assert.match(liveViewSource, /resolveLiveViewRuntime/, 'Live View should route runtime checks through the runtime manager.');
26
+ assert.match(liveViewSource, /runtime:\s*session\.runtime/, 'Live View public session payload should expose runtime diagnostics.');
27
+ assert.ok(
28
+ liveViewSource.includes('target.runtime'),
29
+ 'Live View start should keep the runtime manager result on the session.',
30
+ );
31
+
32
+ const {
33
+ discoverRuntime,
34
+ resolveLiveViewRuntime,
35
+ runtimeManager,
36
+ } = await import('../../server/services/runtime-manager.js');
37
+
38
+ assert.equal(typeof runtimeManager.discover, 'function', 'Runtime manager should expose a discover method.');
39
+
40
+ const nodeRuntime = await discoverRuntime('node');
41
+ assert.equal(nodeRuntime.id, 'node');
42
+ assert.equal(nodeRuntime.status, 'available', 'The current Node runtime should be detected as available.');
43
+ assert.ok(nodeRuntime.path, 'Node runtime should include an executable path.');
44
+ assert.match(nodeRuntime.version || '', /\d+\.\d+\.\d+/, 'Node runtime should include a version.');
45
+
46
+ const missingPython = await discoverRuntime('python', {
47
+ strictPath: true,
48
+ env: {
49
+ ...process.env,
50
+ PATH: '',
51
+ Path: '',
52
+ },
53
+ });
54
+ assert.equal(missingPython.status, 'missing', 'Missing Python should produce a missing runtime status.');
55
+ assert.match(missingPython.diagnostic.message, /Python/i, 'Missing Python diagnostics should name the runtime.');
56
+ assert.match(missingPython.diagnostic.action, /python/i, 'Missing Python diagnostics should include an actionable install command.');
57
+
58
+ const nodeLiveRuntime = await resolveLiveViewRuntime({
59
+ id: 'npm-dev-vite',
60
+ label: 'Vite dev server',
61
+ framework: 'Vite',
62
+ command: 'npm',
63
+ args: ['run', 'dev'],
64
+ displayCommand: 'npm run dev',
65
+ packageManager: 'npm',
66
+ }, {
67
+ env: {
68
+ ...process.env,
69
+ PATH: '',
70
+ Path: '',
71
+ },
72
+ preferManaged: true,
73
+ });
74
+ assert.equal(nodeLiveRuntime.runtime.id, 'node', 'JavaScript Live View commands should resolve to the Node runtime.');
75
+ assert.equal(nodeLiveRuntime.managedRuntime?.id, 'npm', 'JavaScript Live View commands should keep the managed npm hook.');
76
+ assert.equal(nodeLiveRuntime.available, true, 'Managed npm should keep JavaScript projects runnable when npm is missing.');
77
+ assert.match(nodeLiveRuntime.reason, /Node package runner/i, 'Managed npm diagnostics should explain the package runner.');
78
+
79
+ const phpLiveRuntime = await resolveLiveViewRuntime({
80
+ id: 'php-built-in',
81
+ label: 'PHP built-in server',
82
+ framework: 'PHP',
83
+ command: 'php',
84
+ args: ['-S', '127.0.0.1:$PORT', '-t', '.'],
85
+ displayCommand: 'php -S 127.0.0.1:$PORT -t .',
86
+ }, {
87
+ env: {
88
+ ...process.env,
89
+ PATH: '',
90
+ Path: '',
91
+ },
92
+ preferManaged: true,
93
+ });
94
+ assert.equal(phpLiveRuntime.runtime.id, 'php', 'PHP Live View commands should resolve to the PHP runtime.');
95
+ assert.equal(phpLiveRuntime.managedRuntime?.id, 'frankenphp', 'PHP Live View commands should keep the managed FrankenPHP hook.');
96
+ assert.equal(phpLiveRuntime.available, true, 'Managed PHP should keep PHP projects runnable when php is missing.');
97
+ assert.match(phpLiveRuntime.reason, /PHP runtime/i, 'Managed PHP diagnostics should explain the Pixcode runtime.');
98
+
99
+ console.log('runtime manager smoke passed');
@@ -31,6 +31,29 @@ function buildUrls(req, session) {
31
31
  };
32
32
  }
33
33
 
34
+ function buildEnvironmentTunnel(tunnel, urls) {
35
+ const active = Boolean(tunnel?.running && urls?.external);
36
+ return {
37
+ status: active ? 'active' : 'local-only',
38
+ url: active ? urls.external : null,
39
+ localUrl: urls?.local || null,
40
+ preferredUrl: urls?.preferred || urls?.local || null,
41
+ };
42
+ }
43
+
44
+ function attachEnvironmentRuntimeState(environment, urls, tunnel) {
45
+ if (!environment) return environment;
46
+ return {
47
+ ...environment,
48
+ urls,
49
+ tunnel: buildEnvironmentTunnel(tunnel, urls),
50
+ diagnostics: {
51
+ ...environment.diagnostics,
52
+ publicTunnelReady: Boolean(tunnel?.running && urls?.external),
53
+ },
54
+ };
55
+ }
56
+
34
57
  function escapeHtml(value) {
35
58
  return String(value ?? '')
36
59
  .replaceAll('&', '&')
@@ -202,10 +225,13 @@ router.get('/:projectName/status', async (req, res) => {
202
225
  const { projectName } = req.params;
203
226
  const projectPath = await resolveProjectPath(projectName);
204
227
  const state = await getLiveViewState(projectName, projectPath);
228
+ const urls = buildUrls(req, state.session);
229
+ const tunnel = getTunnelState();
205
230
  res.json({
206
231
  ...state,
207
- urls: buildUrls(req, state.session),
208
- tunnel: getTunnelState(),
232
+ urls,
233
+ tunnel,
234
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
209
235
  });
210
236
  } catch (error) {
211
237
  res.status(error.statusCode || 500).json({ error: error.message || 'Failed to read Live View state' });
@@ -217,11 +243,15 @@ router.post('/:projectName/start', async (req, res) => {
217
243
  const { projectName } = req.params;
218
244
  const projectPath = await resolveProjectPath(projectName);
219
245
  const session = await startLiveView(projectName, projectPath, req.body || {});
246
+ const state = await getLiveViewState(projectName, projectPath);
247
+ const urls = buildUrls(req, session);
248
+ const tunnel = getTunnelState();
220
249
  res.json({
221
250
  success: true,
222
251
  session,
223
- urls: buildUrls(req, session),
224
- tunnel: getTunnelState(),
252
+ urls,
253
+ tunnel,
254
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
225
255
  });
226
256
  } catch (error) {
227
257
  const status = error.code === 'LIVE_VIEW_NOT_AVAILABLE' ? 422 : 500;
@@ -234,11 +264,15 @@ router.post('/:projectName/restart', async (req, res) => {
234
264
  const { projectName } = req.params;
235
265
  const projectPath = await resolveProjectPath(projectName);
236
266
  const session = await restartLiveView(projectName, projectPath, req.body || {});
267
+ const state = await getLiveViewState(projectName, projectPath);
268
+ const urls = buildUrls(req, session);
269
+ const tunnel = getTunnelState();
237
270
  res.json({
238
271
  success: true,
239
272
  session,
240
- urls: buildUrls(req, session),
241
- tunnel: getTunnelState(),
273
+ urls,
274
+ tunnel,
275
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
242
276
  });
243
277
  } catch (error) {
244
278
  res.status(500).json({ error: error.message || 'Failed to restart Live View' });
@@ -248,11 +282,14 @@ router.post('/:projectName/restart', async (req, res) => {
248
282
  router.post('/:projectName/stop', async (req, res) => {
249
283
  try {
250
284
  const session = await stopLiveView(req.params.projectName);
285
+ const urls = buildUrls(req, session);
286
+ const tunnel = getTunnelState();
251
287
  res.json({
252
288
  success: true,
253
289
  session,
254
- urls: buildUrls(req, session),
255
- tunnel: getTunnelState(),
290
+ urls,
291
+ tunnel,
292
+ environment: attachEnvironmentRuntimeState(null, urls, tunnel),
256
293
  });
257
294
  } catch (error) {
258
295
  res.status(500).json({ error: error.message || 'Failed to stop Live View' });
@@ -5,13 +5,13 @@ import net from 'node:net';
5
5
  import path from 'node:path';
6
6
 
7
7
  import { buildCliSpawnEnv } from './install-jobs.js';
8
- import { ensureManagedRuntime, getManagedRuntimeStatus } from './managed-runtimes.js';
8
+ import { ensureManagedRuntime } from './managed-runtimes.js';
9
+ import { resolveLiveViewRuntime } from './runtime-manager.js';
9
10
 
10
11
  const sessionsByProject = new Map();
11
12
  const sessionsByShareId = new Map();
12
13
  const READY_TIMEOUT_MS = 12000;
13
14
  const LOG_LIMIT = 200;
14
- const RUNTIME_CHECK_TIMEOUT_MS = 1800;
15
15
 
16
16
  const localUrlRegex = /https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[[^\]]+\])(?::(\d+))?[^\s"'<>]*/i;
17
17
 
@@ -82,88 +82,6 @@ function buildDisplayCommand(command, args) {
82
82
  return [command, ...args].join(' ');
83
83
  }
84
84
 
85
- function quoteForPosixShell(value) {
86
- return `'${String(value).replaceAll("'", "'\\''")}'`;
87
- }
88
-
89
- function quoteForWindowsShell(value) {
90
- return `"${String(value).replaceAll('"', '""')}"`;
91
- }
92
-
93
- function isPathLikeCommand(command) {
94
- return path.isAbsolute(command) || command.includes('/') || command.includes('\\');
95
- }
96
-
97
- function runtimeMissingReason(command, framework) {
98
- const base = `${command} is not available on this machine.`;
99
- if (framework === 'PHP' || command === 'php') {
100
- return 'Pixcode can prepare a local PHP runtime automatically before starting this project.';
101
- }
102
- if (command === 'npm' || command === 'pnpm' || command === 'yarn' || command === 'bun') {
103
- return `${base} Pixcode can prepare a local Node package runner automatically before starting this project.`;
104
- }
105
- if (command === 'python' || command === 'python3') {
106
- return `${base} Pixcode does not have a managed Python runtime for this stack yet.`;
107
- }
108
- return `${base} Pixcode does not have a managed ${framework || command} runtime for this stack yet.`;
109
- }
110
-
111
- async function checkCommandAvailability(command, env = process.env) {
112
- if (!command || command.includes('\n') || command.includes('\r')) return true;
113
-
114
- if (isPathLikeCommand(command)) {
115
- try {
116
- await fs.access(command);
117
- return true;
118
- } catch {
119
- return false;
120
- }
121
- }
122
-
123
- const checker = process.platform === 'win32'
124
- ? {
125
- command: process.env.ComSpec || 'cmd.exe',
126
- args: ['/d', '/s', '/c', `where ${quoteForWindowsShell(command)}`],
127
- }
128
- : {
129
- command: '/bin/sh',
130
- args: ['-lc', `command -v ${quoteForPosixShell(command)}`],
131
- };
132
-
133
- return new Promise((resolve) => {
134
- let settled = false;
135
- let child = null;
136
- const finish = (available) => {
137
- if (settled) return;
138
- settled = true;
139
- clearTimeout(timer);
140
- resolve(available);
141
- };
142
-
143
- const timer = setTimeout(() => {
144
- try {
145
- child?.kill();
146
- } catch {
147
- // Ignore a raced process exit.
148
- }
149
- finish(true);
150
- }, RUNTIME_CHECK_TIMEOUT_MS);
151
-
152
- child = spawn(checker.command, checker.args, {
153
- env,
154
- stdio: 'ignore',
155
- windowsHide: true,
156
- });
157
-
158
- child.on('error', (error) => {
159
- finish(error?.code === 'ENOENT' ? false : true);
160
- });
161
- child.on('exit', (code) => {
162
- finish(code === 0);
163
- });
164
- });
165
- }
166
-
167
85
  function buildPackageCommand(packageManager, scriptName, id, label, framework, extraArgs = []) {
168
86
  const args = packageRunArgs(packageManager, scriptName, extraArgs);
169
87
  return {
@@ -376,6 +294,91 @@ function buildManagedPhpCommand(runtimeStatus) {
376
294
  };
377
295
  }
378
296
 
297
+ function publicCommand(command) {
298
+ if (!command) return null;
299
+ return {
300
+ id: command.id,
301
+ label: command.label,
302
+ displayCommand: command.displayCommand,
303
+ custom: command.custom === true || command.id === 'custom',
304
+ };
305
+ }
306
+
307
+ function liveViewEnvironmentMode(target, session) {
308
+ const kind = session?.kind || target?.kind || 'none';
309
+ if (kind === 'static') return 'static';
310
+ if (kind === 'process') return 'local-process';
311
+ return 'unavailable';
312
+ }
313
+
314
+ function liveViewEnvironmentStatus(target, session) {
315
+ if (session?.status) return session.status;
316
+ if (target?.available) return 'ready';
317
+ return 'unavailable';
318
+ }
319
+
320
+ function liveViewEnvironmentCommand(target, session) {
321
+ return publicCommand(session?.command) || publicCommand(target?.command);
322
+ }
323
+
324
+ function liveViewEnvironmentLogs(session) {
325
+ return Array.isArray(session?.log) ? session.log.slice(-40) : [];
326
+ }
327
+
328
+ function liveViewEnvironmentRuntime(target, session) {
329
+ return session?.runtime || target?.runtime || null;
330
+ }
331
+
332
+ function liveViewEnvironmentManagedRuntime(target, session) {
333
+ return session?.managedRuntime || target?.managedRuntime || target?.command?.managedRuntime || null;
334
+ }
335
+
336
+ export function buildLiveViewEnvironment({ target = null, session = null } = {}) {
337
+ const mode = liveViewEnvironmentMode(target, session);
338
+ const status = liveViewEnvironmentStatus(target, session);
339
+ const command = liveViewEnvironmentCommand(target, session);
340
+ const framework = session?.framework || target?.framework || null;
341
+ const label = session?.label || target?.label || framework || 'Live View';
342
+ const runtime = liveViewEnvironmentRuntime(target, session);
343
+ const managedRuntime = liveViewEnvironmentManagedRuntime(target, session);
344
+ const logs = liveViewEnvironmentLogs(session);
345
+ const reason = session?.error || target?.reason || null;
346
+
347
+ return {
348
+ id: mode === 'unavailable' ? 'live-view-unavailable' : `live-view-${mode}`,
349
+ mode,
350
+ status,
351
+ framework,
352
+ label,
353
+ command,
354
+ runtime,
355
+ managedRuntime,
356
+ port: session?.port ?? null,
357
+ upstreamUrl: session?.upstreamUrl ?? null,
358
+ sharePath: session?.sharePath ?? null,
359
+ logs,
360
+ diagnostics: {
361
+ runnerKind: session?.kind || target?.kind || 'none',
362
+ targetAvailable: Boolean(target?.available || session),
363
+ reason,
364
+ error: session?.error || null,
365
+ exitCode: session?.exitCode ?? null,
366
+ exitSignal: session?.exitSignal ?? null,
367
+ spawnErrorCode: session?.spawnErrorCode ?? null,
368
+ startedAt: session?.startedAt || null,
369
+ stoppedAt: session?.stoppedAt || null,
370
+ readyTimeoutMs: READY_TIMEOUT_MS,
371
+ staticServing: mode === 'static',
372
+ customCommand: command?.custom === true,
373
+ publicTunnelReady: false,
374
+ },
375
+ tunnel: {
376
+ status: 'local-only',
377
+ url: null,
378
+ },
379
+ };
380
+ }
381
+
379
382
  function detectPackageCommand(packageJson, packageManager) {
380
383
  const scripts = packageJson.scripts || {};
381
384
  const devScript = String(scripts.dev || '');
@@ -596,11 +599,13 @@ export async function detectLiveViewTarget(projectPath, options = {}) {
596
599
 
597
600
  const processCommand = await detectProcessCommand(projectPath);
598
601
  if (processCommand) {
602
+ const runtimeResolution = await resolveLiveViewRuntime(processCommand, {
603
+ env: options.env || process.env,
604
+ preferManaged: true,
605
+ });
606
+
599
607
  if (isPackageManagerCommand(processCommand.command)) {
600
- const managedRuntime = await getManagedRuntimeStatus('npm', {
601
- env: options.env || process.env,
602
- preferManaged: true,
603
- });
608
+ const managedRuntime = runtimeResolution.managedRuntime;
604
609
  const command = buildManagedPackageCommand(processCommand, managedRuntime);
605
610
  return {
606
611
  available: true,
@@ -609,17 +614,13 @@ export async function detectLiveViewTarget(projectPath, options = {}) {
609
614
  framework: processCommand.framework,
610
615
  command,
611
616
  managedRuntime,
612
- reason: managedRuntime.status === 'missing'
613
- ? 'Pixcode will prepare a local Node package runner automatically before starting this project.'
614
- : 'Pixcode will run this project with its managed Node package runner.',
617
+ runtime: runtimeResolution.runtime,
618
+ reason: runtimeResolution.reason,
615
619
  };
616
620
  }
617
621
 
618
622
  if (processCommand.framework === 'PHP' || processCommand.command === 'php') {
619
- const managedRuntime = await getManagedRuntimeStatus('frankenphp', {
620
- env: options.env || process.env,
621
- preferManaged: true,
622
- });
623
+ const managedRuntime = runtimeResolution.managedRuntime;
623
624
  const command = buildManagedPhpCommand(managedRuntime);
624
625
  return {
625
626
  available: true,
@@ -628,14 +629,12 @@ export async function detectLiveViewTarget(projectPath, options = {}) {
628
629
  framework: command.framework,
629
630
  command,
630
631
  managedRuntime,
631
- reason: managedRuntime.status === 'missing'
632
- ? 'Pixcode will prepare a local PHP runtime automatically before starting this project.'
633
- : 'Pixcode will run this project with its managed PHP runtime.',
632
+ runtime: runtimeResolution.runtime,
633
+ reason: runtimeResolution.reason,
634
634
  };
635
635
  }
636
636
 
637
- const runtimeAvailable = await checkCommandAvailability(processCommand.command, options.env || process.env);
638
- if (!runtimeAvailable) {
637
+ if (!runtimeResolution.available) {
639
638
  return {
640
639
  available: false,
641
640
  kind: 'process',
@@ -643,7 +642,8 @@ export async function detectLiveViewTarget(projectPath, options = {}) {
643
642
  framework: processCommand.framework,
644
643
  command: processCommand,
645
644
  missingRuntime: processCommand.command,
646
- reason: runtimeMissingReason(processCommand.command, processCommand.framework),
645
+ runtime: runtimeResolution.runtime,
646
+ reason: runtimeResolution.reason,
647
647
  };
648
648
  }
649
649
 
@@ -653,6 +653,7 @@ export async function detectLiveViewTarget(projectPath, options = {}) {
653
653
  label: processCommand.label,
654
654
  framework: processCommand.framework,
655
655
  command: processCommand,
656
+ runtime: runtimeResolution.runtime,
656
657
  };
657
658
  }
658
659
 
@@ -732,11 +733,8 @@ function publicSession(session) {
732
733
  kind: session.kind,
733
734
  framework: session.framework,
734
735
  label: session.label,
735
- command: session.command ? {
736
- id: session.command.id,
737
- label: session.command.label,
738
- displayCommand: session.command.displayCommand,
739
- } : null,
736
+ command: publicCommand(session.command),
737
+ runtime: session.runtime || null,
740
738
  managedRuntime: session.managedRuntime || null,
741
739
  port: session.port,
742
740
  upstreamUrl: session.upstreamUrl,
@@ -753,9 +751,11 @@ function publicSession(session) {
753
751
  export async function getLiveViewState(projectName, projectPath) {
754
752
  const target = await detectLiveViewTarget(projectPath);
755
753
  const session = sessionsByProject.get(projectName) ?? null;
754
+ const publicLiveViewSession = publicSession(session);
756
755
  return {
757
756
  target,
758
- session: publicSession(session),
757
+ session: publicLiveViewSession,
758
+ environment: buildLiveViewEnvironment({ target, session: publicLiveViewSession }),
759
759
  };
760
760
  }
761
761
 
@@ -780,6 +780,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
780
780
  command: customCommand,
781
781
  args: [],
782
782
  displayCommand: customCommand,
783
+ custom: true,
783
784
  shell: true,
784
785
  },
785
786
  }
@@ -803,6 +804,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
803
804
  label: target.label,
804
805
  staticRoot: target.staticRoot,
805
806
  command: null,
807
+ runtime: null,
806
808
  port: null,
807
809
  upstreamUrl: null,
808
810
  startedAt: new Date().toISOString(),
@@ -842,6 +844,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
842
844
  framework: target.framework,
843
845
  label: target.label,
844
846
  command,
847
+ runtime: target.runtime || null,
845
848
  managedRuntime: runtimeStatus,
846
849
  port,
847
850
  host: '127.0.0.1',