@pixelbyte-software/pixcode 1.40.5 → 1.40.8

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.
@@ -83,12 +83,21 @@ function selectFrankenPhpAsset(release) {
83
83
  return candidates[0]?.asset || null;
84
84
  }
85
85
 
86
- async function fetchJson(url, env = process.env) {
86
+ function isGitHubUrl(url) {
87
+ try {
88
+ const hostname = new URL(url).hostname.toLowerCase();
89
+ return hostname === 'github.com' || hostname === 'api.github.com' || hostname.endsWith('.github.com');
90
+ } catch {
91
+ return String(url).includes('github.com');
92
+ }
93
+ }
94
+
95
+ async function fetchJson(url, env = process.env, options = {}) {
87
96
  const headers = {
88
- Accept: 'application/vnd.github+json',
97
+ Accept: options.accept || 'application/json',
89
98
  'User-Agent': 'Pixcode Live View',
90
99
  };
91
- if (env.GITHUB_TOKEN) headers.Authorization = `Bearer ${env.GITHUB_TOKEN}`;
100
+ if (env.GITHUB_TOKEN && isGitHubUrl(url)) headers.Authorization = `Bearer ${env.GITHUB_TOKEN}`;
92
101
 
93
102
  const response = await fetch(url, { headers });
94
103
  if (!response.ok) {
@@ -99,7 +108,7 @@ async function fetchJson(url, env = process.env) {
99
108
 
100
109
  async function downloadFile(url, targetFile, env = process.env) {
101
110
  const headers = { 'User-Agent': 'Pixcode Live View' };
102
- if (env.GITHUB_TOKEN && url.includes('github.com')) headers.Authorization = `Bearer ${env.GITHUB_TOKEN}`;
111
+ if (env.GITHUB_TOKEN && isGitHubUrl(url)) headers.Authorization = `Bearer ${env.GITHUB_TOKEN}`;
103
112
 
104
113
  const response = await fetch(url, { headers });
105
114
  if (!response.ok) {
@@ -112,50 +121,65 @@ async function downloadFile(url, targetFile, env = process.env) {
112
121
  function runProcess(command, args, options = {}) {
113
122
  return new Promise((resolve, reject) => {
114
123
  let stderr = '';
124
+ let settled = false;
125
+ const { timeoutMs, ...spawnOptions } = options;
115
126
  const child = spawn(command, args, {
116
- ...options,
127
+ ...spawnOptions,
117
128
  stdio: ['ignore', 'ignore', 'pipe'],
118
129
  windowsHide: true,
119
130
  });
131
+ const finish = (callback, value) => {
132
+ if (settled) return;
133
+ settled = true;
134
+ if (timer) clearTimeout(timer);
135
+ callback(value);
136
+ };
137
+ const timer = timeoutMs
138
+ ? setTimeout(() => {
139
+ try {
140
+ child.kill();
141
+ } catch {
142
+ // Process may have exited between timeout scheduling and kill.
143
+ }
144
+ finish(reject, new Error(`${command} timed out after ${timeoutMs}ms`));
145
+ }, timeoutMs)
146
+ : null;
120
147
  child.stderr.on('data', (chunk) => {
121
148
  stderr += chunk.toString();
122
149
  });
123
- child.on('error', reject);
150
+ child.on('error', (error) => finish(reject, error));
124
151
  child.on('close', (code) => {
125
152
  if (code === 0) {
126
- resolve();
153
+ finish(resolve);
127
154
  return;
128
155
  }
129
- reject(new Error(`${command} exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
156
+ finish(reject, new Error(`${command} exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
130
157
  });
131
158
  });
132
159
  }
133
160
 
161
+ function buildPowerShellExpandArchiveArgs(archivePath, targetDir) {
162
+ return [
163
+ '-NoProfile',
164
+ '-ExecutionPolicy',
165
+ 'Bypass',
166
+ '-Command',
167
+ '& { param([string]$archive, [string]$destination) Expand-Archive -Force -LiteralPath $archive -DestinationPath $destination }',
168
+ archivePath,
169
+ targetDir,
170
+ ];
171
+ }
172
+
134
173
  async function extractZip(archivePath, targetDir, env = process.env) {
135
174
  if (process.platform === 'win32') {
136
175
  const shell = env.ComSpec || process.env.ComSpec || 'powershell.exe';
137
176
  const isCmd = shell.toLowerCase().endsWith('cmd.exe');
177
+ const expandArgs = buildPowerShellExpandArchiveArgs(archivePath, targetDir);
138
178
  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 });
179
+ await runProcess('powershell.exe', expandArgs, { env });
148
180
  return;
149
181
  }
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 });
182
+ await runProcess(shell, expandArgs, { env });
159
183
  return;
160
184
  }
161
185
 
@@ -194,10 +218,33 @@ async function findRuntimeExecutable(searchRoot, binaryName) {
194
218
  return null;
195
219
  }
196
220
 
221
+ async function copyRuntimeWithSidecars(executablePath, currentDir) {
222
+ const executableDir = path.dirname(executablePath);
223
+ await fs.rm(currentDir, { recursive: true, force: true });
224
+ await fs.mkdir(currentDir, { recursive: true });
225
+ await fs.cp(executableDir, currentDir, { recursive: true, force: true });
226
+ return path.join(currentDir, path.basename(executablePath));
227
+ }
228
+
229
+ async function validateManagedRuntimeExecutable(id, executablePath, env = process.env) {
230
+ if (id !== 'frankenphp') return true;
231
+ try {
232
+ await runProcess(executablePath, ['version'], {
233
+ env,
234
+ timeoutMs: 5000,
235
+ });
236
+ return true;
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
241
+
197
242
  async function installFrankenPhp(env = process.env) {
198
243
  const releaseApiUrl = env.PIXCODE_FRANKENPHP_RELEASE_API
199
244
  || 'https://api.github.com/repos/php/frankenphp/releases/latest';
200
- const release = env.PIXCODE_FRANKENPHP_URL ? null : await fetchJson(releaseApiUrl, env);
245
+ const release = env.PIXCODE_FRANKENPHP_URL
246
+ ? null
247
+ : await fetchJson(releaseApiUrl, env, { accept: 'application/vnd.github+json' });
201
248
  const asset = env.PIXCODE_FRANKENPHP_URL
202
249
  ? {
203
250
  name: path.basename(new URL(env.PIXCODE_FRANKENPHP_URL).pathname),
@@ -237,12 +284,7 @@ async function installFrankenPhp(env = process.env) {
237
284
  await fs.chmod(executablePath, 0o755).catch(() => undefined);
238
285
  }
239
286
 
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);
287
+ const finalExecutable = await copyRuntimeWithSidecars(executablePath, currentDir);
246
288
  if (process.platform !== 'win32') {
247
289
  await fs.chmod(finalExecutable, 0o755).catch(() => undefined);
248
290
  }
@@ -264,7 +306,7 @@ async function installFrankenPhp(env = process.env) {
264
306
  async function installNpmRuntime(env = process.env) {
265
307
  const registryUrl = env.PIXCODE_NPM_RUNTIME_REGISTRY
266
308
  || 'https://registry.npmjs.org/npm/latest';
267
- const metadata = await fetchJson(registryUrl, env);
309
+ const metadata = await fetchJson(registryUrl, env, { accept: 'application/json' });
268
310
  const tarballUrl = metadata?.dist?.tarball;
269
311
  if (!tarballUrl) {
270
312
  throw new Error('No npm runtime tarball is available from the npm registry.');
@@ -371,6 +413,18 @@ export async function getManagedRuntimeStatus(id, options = {}) {
371
413
 
372
414
  const manifest = await readManifest(id, env);
373
415
  if (manifest) {
416
+ const valid = await validateManagedRuntimeExecutable(id, manifest.executablePath, env);
417
+ if (!valid) {
418
+ return {
419
+ id,
420
+ label: 'Pixcode PHP runtime',
421
+ provider: 'FrankenPHP',
422
+ status: 'missing',
423
+ installable: true,
424
+ reason: 'The existing Pixcode PHP runtime is incomplete or cannot start. Pixcode will reinstall it automatically.',
425
+ };
426
+ }
427
+
374
428
  return {
375
429
  id,
376
430
  label: manifest.label || 'Pixcode PHP runtime',
@@ -86,6 +86,20 @@ function mergeCatalogs(primary, secondary) {
86
86
  return Array.from(seen.values());
87
87
  }
88
88
 
89
+ function mergeProviderCatalogs(provider, primary, staticCatalog) {
90
+ const normalizedPrimary = normalizeList(primary);
91
+
92
+ // OpenCode Zen free models rotate often. When models.dev succeeds, treat
93
+ // that live catalog as authoritative; otherwise stale static freebies can
94
+ // leak back into the UI and fail later with ProviderModelNotFoundError.
95
+ if (provider === 'opencode') {
96
+ const liveModels = normalizedPrimary.filter((item) => item.source === 'api');
97
+ if (liveModels.length > 0) return liveModels;
98
+ }
99
+
100
+ return mergeCatalogs(normalizedPrimary, staticCatalog);
101
+ }
102
+
89
103
  // ---------------- Per-provider live discovery ----------------
90
104
 
91
105
  async function discoverAnthropic(apiKey, baseUrl) {
@@ -287,8 +301,9 @@ export async function getProviderModels(provider, opts = {}) {
287
301
  : false;
288
302
 
289
303
  if (!forceRefresh && cacheFresh && Array.isArray(cached?.models)) {
304
+ const merged = mergeProviderCatalogs(provider, cached.models, staticCatalog);
290
305
  return {
291
- models: mergeCatalogs(normalizeList(cached.models), staticCatalog),
306
+ models: merged,
292
307
  fetchedAt: cached.fetchedAt,
293
308
  error: cached.error,
294
309
  fromCache: true,
@@ -305,7 +320,7 @@ export async function getProviderModels(provider, opts = {}) {
305
320
  } catch (err) {
306
321
  error = err?.message || String(err);
307
322
  }
308
- const merged = mergeCatalogs(normalizeList(liveModels), staticCatalog);
323
+ const merged = mergeProviderCatalogs(provider, liveModels, staticCatalog);
309
324
  const entry = { models: merged, error };
310
325
  await saveCacheEntry(provider, entry).catch(() => { /* non-fatal */ });
311
326
  return { models: merged, fetchedAt: new Date().toISOString(), error, fromCache: false };
@@ -367,7 +382,7 @@ export async function getProviderModels(provider, opts = {}) {
367
382
  }
368
383
  }
369
384
 
370
- const merged = mergeCatalogs(normalizeList(liveModels), staticCatalog);
385
+ const merged = mergeProviderCatalogs(provider, liveModels, staticCatalog);
371
386
  const entry = { models: merged, error };
372
387
  await saveCacheEntry(provider, entry).catch(() => { /* non-fatal */ });
373
388
  return { models: merged, fetchedAt: new Date().toISOString(), error, fromCache: false };
@@ -110,11 +110,10 @@ export const QWEN_MODELS = {
110
110
  export const OPENCODE_MODELS = {
111
111
  OPTIONS: [
112
112
  // OpenCode Zen — free tier (no charge, may rate-limit). The "limited
113
- // time" Zen freebies rotate, so this is the safest small set.
113
+ // time" Zen freebies rotate, so keep this fallback conservative and let
114
+ // the live models.dev catalog populate the full current list.
114
115
  { value: "opencode/big-pickle", label: "OpenCode Zen · Big Pickle (Free)", free: true },
115
116
  { value: "opencode/minimax-m2.5-free", label: "OpenCode Zen · MiniMax M2.5 (Free)", free: true },
116
- { value: "opencode/hy3-preview-free", label: "OpenCode Zen · Hy3 Preview (Free)", free: true },
117
- { value: "opencode/ling-2.6-flash-free", label: "OpenCode Zen · Ling 2.6 Flash (Free)", free: true },
118
117
  { value: "opencode/nemotron-3-super-free", label: "OpenCode Zen · Nemotron 3 Super (Free)", free: true },
119
118
  { value: "opencode/gpt-5-nano", label: "OpenCode Zen · GPT-5 Nano (Free)", free: true },
120
119