@pixelbyte-software/pixcode 1.40.6 → 1.40.9
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/dist/assets/{index-ZA4sMw0s.js → index-BIWB3xUE.js} +149 -149
- package/dist/index.html +1 -1
- package/dist/landing.html +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +133 -4
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +9 -1
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
- package/dist-server/server/services/live-view.js +159 -3
- package/dist-server/server/services/live-view.js.map +1 -1
- package/dist-server/server/services/managed-runtimes.js +58 -9
- package/dist-server/server/services/managed-runtimes.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +2 -1
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/provider-models.js +16 -3
- package/dist-server/server/services/provider-models.js.map +1 -1
- package/dist-server/shared/modelConstants.js +2 -3
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/live-view-integration.mjs +156 -2
- package/scripts/smoke/notification-center.mjs +24 -0
- package/scripts/smoke/orchestration-model-sync.mjs +30 -0
- package/scripts/smoke/provider-models-opencode-live.mjs +66 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +174 -4
- package/server/modules/orchestration/workflows/workflow.routes.ts +10 -1
- package/server/services/live-view.js +166 -3
- package/server/services/managed-runtimes.js +56 -10
- package/server/services/notification-orchestrator.js +2 -1
- package/server/services/provider-models.js +18 -3
- package/shared/modelConstants.js +2 -3
|
@@ -13,6 +13,19 @@ import {
|
|
|
13
13
|
workspaceTargetMetadata,
|
|
14
14
|
} from '@/modules/orchestration/workflows/workspace-target.js';
|
|
15
15
|
import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
|
|
16
|
+
// @ts-ignore — plain-JS service
|
|
17
|
+
import { getProviderModels } from '@/services/provider-models.js';
|
|
18
|
+
// @ts-ignore — plain-JS service
|
|
19
|
+
import { notifyRunFailed, notifyRunStopped } from '@/services/notification-orchestrator.js';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
CLAUDE_MODELS,
|
|
23
|
+
CODEX_MODELS,
|
|
24
|
+
CURSOR_MODELS,
|
|
25
|
+
GEMINI_MODELS,
|
|
26
|
+
OPENCODE_MODELS,
|
|
27
|
+
QWEN_MODELS,
|
|
28
|
+
} from '../../../../shared/modelConstants.js';
|
|
16
29
|
|
|
17
30
|
const TERMINAL = new Set(['completed', 'failed', 'canceled']);
|
|
18
31
|
const SKIPPED = 'skipped';
|
|
@@ -106,6 +119,51 @@ type AgentAssignment = {
|
|
|
106
119
|
|
|
107
120
|
type KnownAgentRole = typeof KNOWN_AGENT_ROLES[number];
|
|
108
121
|
type AgentRole = string;
|
|
122
|
+
type ProviderId = 'claude' | 'cursor' | 'codex' | 'gemini' | 'qwen' | 'opencode';
|
|
123
|
+
type ProviderModel = {
|
|
124
|
+
value: string;
|
|
125
|
+
label?: string;
|
|
126
|
+
source?: 'static' | 'api';
|
|
127
|
+
free?: boolean;
|
|
128
|
+
};
|
|
129
|
+
type RunStoppedNotifier = (payload: {
|
|
130
|
+
userId: string | number;
|
|
131
|
+
provider: string;
|
|
132
|
+
sessionId?: string | null;
|
|
133
|
+
stopReason?: string;
|
|
134
|
+
sessionName?: string | null;
|
|
135
|
+
}) => void;
|
|
136
|
+
type RunFailedNotifier = (payload: {
|
|
137
|
+
userId: string | number;
|
|
138
|
+
provider: string;
|
|
139
|
+
sessionId?: string | null;
|
|
140
|
+
error: unknown;
|
|
141
|
+
sessionName?: string | null;
|
|
142
|
+
}) => void;
|
|
143
|
+
|
|
144
|
+
const sendRunStoppedNotification = notifyRunStopped as RunStoppedNotifier;
|
|
145
|
+
const sendRunFailedNotification = notifyRunFailed as RunFailedNotifier;
|
|
146
|
+
|
|
147
|
+
const adapterProviderMap: Record<string, ProviderId | undefined> = {
|
|
148
|
+
'claude-code': 'claude',
|
|
149
|
+
cursor: 'cursor',
|
|
150
|
+
codex: 'codex',
|
|
151
|
+
gemini: 'gemini',
|
|
152
|
+
qwen: 'qwen',
|
|
153
|
+
opencode: 'opencode',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const modelCatalogsByProvider: Record<ProviderId, {
|
|
157
|
+
staticList: ProviderModel[];
|
|
158
|
+
defaultModel?: string;
|
|
159
|
+
}> = {
|
|
160
|
+
claude: { staticList: CLAUDE_MODELS.OPTIONS, defaultModel: CLAUDE_MODELS.DEFAULT },
|
|
161
|
+
cursor: { staticList: CURSOR_MODELS.OPTIONS, defaultModel: CURSOR_MODELS.DEFAULT },
|
|
162
|
+
codex: { staticList: CODEX_MODELS.OPTIONS, defaultModel: CODEX_MODELS.DEFAULT },
|
|
163
|
+
gemini: { staticList: GEMINI_MODELS.OPTIONS, defaultModel: GEMINI_MODELS.DEFAULT },
|
|
164
|
+
qwen: { staticList: QWEN_MODELS.OPTIONS, defaultModel: QWEN_MODELS.DEFAULT },
|
|
165
|
+
opencode: { staticList: OPENCODE_MODELS.OPTIONS, defaultModel: OPENCODE_MODELS.DEFAULT },
|
|
166
|
+
};
|
|
109
167
|
|
|
110
168
|
function readAgentRole(value: unknown): AgentRole | undefined {
|
|
111
169
|
return typeof value === 'string' && value.trim() && value.trim() !== 'auto'
|
|
@@ -129,10 +187,94 @@ function readString(value: unknown): string | undefined {
|
|
|
129
187
|
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
130
188
|
}
|
|
131
189
|
|
|
190
|
+
function readNotificationUserId(metadata?: Record<string, unknown>): string | number | null {
|
|
191
|
+
const value = metadata?.userId;
|
|
192
|
+
return typeof value === 'string' || typeof value === 'number' ? value : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function workflowNotificationTitle(run: WorkflowRun): string {
|
|
196
|
+
return readString(run.metadata?.workflowName) ?? run.workflowId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function notifyWorkflowRunFinished(run: WorkflowRun): void {
|
|
200
|
+
const userId = readNotificationUserId(run.metadata);
|
|
201
|
+
if (!userId) return;
|
|
202
|
+
|
|
203
|
+
if (run.status === 'completed') {
|
|
204
|
+
sendRunStoppedNotification({
|
|
205
|
+
userId,
|
|
206
|
+
provider: 'system',
|
|
207
|
+
sessionId: run.id,
|
|
208
|
+
sessionName: workflowNotificationTitle(run),
|
|
209
|
+
stopReason: 'Orchestration completed',
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (run.status === 'canceled') {
|
|
215
|
+
sendRunStoppedNotification({
|
|
216
|
+
userId,
|
|
217
|
+
provider: 'system',
|
|
218
|
+
sessionId: run.id,
|
|
219
|
+
sessionName: workflowNotificationTitle(run),
|
|
220
|
+
stopReason: 'Orchestration canceled',
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (run.status === 'failed') {
|
|
226
|
+
sendRunFailedNotification({
|
|
227
|
+
userId,
|
|
228
|
+
provider: 'system',
|
|
229
|
+
sessionId: run.id,
|
|
230
|
+
sessionName: workflowNotificationTitle(run),
|
|
231
|
+
error: readString(run.metadata?.error) ?? 'Orchestration failed',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
132
236
|
function readBoolean(value: unknown): boolean | undefined {
|
|
133
237
|
return typeof value === 'boolean' ? value : undefined;
|
|
134
238
|
}
|
|
135
239
|
|
|
240
|
+
function modelValueSet(models: ProviderModel[]): Set<string> {
|
|
241
|
+
return new Set(models.map((model) => model.value).filter(Boolean));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function preferredFallbackModel(models: ProviderModel[], defaultModel?: string): string | undefined {
|
|
245
|
+
const values = modelValueSet(models);
|
|
246
|
+
if (defaultModel && values.has(defaultModel)) return defaultModel;
|
|
247
|
+
return models.find((model) => model.source === 'api' && model.free)?.value
|
|
248
|
+
?? models.find((model) => model.source === 'api')?.value
|
|
249
|
+
?? models.find((model) => model.free)?.value
|
|
250
|
+
?? models[0]?.value
|
|
251
|
+
?? defaultModel;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function resolveWorkflowModel(adapterId: string, requestedModel?: string): Promise<string | undefined> {
|
|
255
|
+
const provider = adapterProviderMap[adapterId];
|
|
256
|
+
if (!provider) return requestedModel;
|
|
257
|
+
|
|
258
|
+
const catalog = modelCatalogsByProvider[provider];
|
|
259
|
+
if (!requestedModel) return catalog.defaultModel;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const result = await getProviderModels(provider, {
|
|
263
|
+
staticList: catalog.staticList,
|
|
264
|
+
});
|
|
265
|
+
const models = Array.isArray(result?.models) ? result.models as ProviderModel[] : [];
|
|
266
|
+
if (modelValueSet(models).has(requestedModel)) {
|
|
267
|
+
return requestedModel;
|
|
268
|
+
}
|
|
269
|
+
return preferredFallbackModel(models, catalog.defaultModel) ?? requestedModel;
|
|
270
|
+
} catch {
|
|
271
|
+
const staticValues = modelValueSet(catalog.staticList);
|
|
272
|
+
return staticValues.has(requestedModel)
|
|
273
|
+
? requestedModel
|
|
274
|
+
: preferredFallbackModel(catalog.staticList, catalog.defaultModel) ?? requestedModel;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
136
278
|
function readIsolation(value: unknown): 'host' | 'worktree' | 'docker' | undefined {
|
|
137
279
|
return value === 'host' || value === 'worktree' || value === 'docker' ? value : undefined;
|
|
138
280
|
}
|
|
@@ -1372,6 +1514,7 @@ class WorkflowRunner {
|
|
|
1372
1514
|
} finally {
|
|
1373
1515
|
run.finishedAt = run.finishedAt ?? Date.now();
|
|
1374
1516
|
workflowStore.setRun(run);
|
|
1517
|
+
notifyWorkflowRunFinished(run);
|
|
1375
1518
|
this.cancelingRuns.delete(run.id);
|
|
1376
1519
|
}
|
|
1377
1520
|
}
|
|
@@ -1408,15 +1551,42 @@ class WorkflowRunner {
|
|
|
1408
1551
|
|
|
1409
1552
|
const inputContext = node.inputs.map((input) => outputs.get(input)).filter(Boolean).join('\n\n');
|
|
1410
1553
|
const workspaceTarget = resolveWorkflowWorkspace(run.metadata);
|
|
1411
|
-
const prompt = [
|
|
1412
|
-
|
|
1413
|
-
.
|
|
1554
|
+
const prompt = [
|
|
1555
|
+
'Original user request (primary task; answer this directly even if the workspace is empty):',
|
|
1556
|
+
run.input?.trim() || '(No original user request was provided.)',
|
|
1557
|
+
inputContext
|
|
1558
|
+
? `Upstream workflow context from prior agents:\n${inputContext}`
|
|
1559
|
+
: '',
|
|
1560
|
+
`Current workflow step instructions:\n${node.prompt}`,
|
|
1561
|
+
workspaceContextPrompt(workspaceTarget),
|
|
1562
|
+
].filter(Boolean).join('\n\n');
|
|
1414
1563
|
const settings = getMetadataRecord(run.metadata, 'settings');
|
|
1415
1564
|
const projectPath = workspaceTarget.projectPath;
|
|
1416
1565
|
const isolation = readIsolation(settings.isolation) ?? node.isolation ?? 'host';
|
|
1417
1566
|
const keepAfterCompletion = readBoolean(settings.keepWorkspace) ?? true;
|
|
1418
1567
|
const baseRef = readString(settings.baseRef) ?? 'HEAD';
|
|
1419
1568
|
const effectivePermissionMode = resolveNodePermissionMode(node, workspaceTarget);
|
|
1569
|
+
const effectiveModel = await resolveWorkflowModel(node.adapterId, node.model);
|
|
1570
|
+
if (effectiveModel !== node.model) {
|
|
1571
|
+
nodeRun.model = effectiveModel;
|
|
1572
|
+
const modelFallbackEvents = Array.isArray(run.metadata?.modelFallbackEvents)
|
|
1573
|
+
? run.metadata.modelFallbackEvents
|
|
1574
|
+
: [];
|
|
1575
|
+
run.metadata = {
|
|
1576
|
+
...run.metadata,
|
|
1577
|
+
modelFallbackEvents: [
|
|
1578
|
+
...modelFallbackEvents,
|
|
1579
|
+
{
|
|
1580
|
+
nodeId: node.id,
|
|
1581
|
+
adapterId: node.adapterId,
|
|
1582
|
+
requestedModel: node.model,
|
|
1583
|
+
effectiveModel,
|
|
1584
|
+
changedAt: Date.now(),
|
|
1585
|
+
},
|
|
1586
|
+
],
|
|
1587
|
+
};
|
|
1588
|
+
workflowStore.setRun(run);
|
|
1589
|
+
}
|
|
1420
1590
|
let body: { id?: string; error?: { message?: string } };
|
|
1421
1591
|
try {
|
|
1422
1592
|
const submit = await fetch(`${localA2ABaseUrl()}/tasks`, {
|
|
@@ -1436,7 +1606,7 @@ class WorkflowRunner {
|
|
|
1436
1606
|
agentInstanceId: node.agentInstanceId,
|
|
1437
1607
|
agentLabel: node.agentLabel,
|
|
1438
1608
|
assignment: node.assignment,
|
|
1439
|
-
model:
|
|
1609
|
+
model: effectiveModel,
|
|
1440
1610
|
permissionMode: effectivePermissionMode,
|
|
1441
1611
|
toolsSettings: node.toolsSettings,
|
|
1442
1612
|
projectPath,
|
|
@@ -39,6 +39,11 @@ function readMetadata(body: unknown): Record<string, unknown> | undefined {
|
|
|
39
39
|
return metadata && typeof metadata === 'object' ? metadata as Record<string, unknown> : undefined;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function readRequestUserId(req: express.Request): string | number | null {
|
|
43
|
+
const user = (req as express.Request & { user?: { id?: string | number; userId?: string | number } }).user;
|
|
44
|
+
return user?.id ?? user?.userId ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
42
47
|
function sendRunSnapshot(res: express.Response, runId: string): boolean {
|
|
43
48
|
const run = workflowStore.getRun(runId);
|
|
44
49
|
if (!run) {
|
|
@@ -179,7 +184,11 @@ export function createWorkflowRouter(): Router {
|
|
|
179
184
|
const run = workflowRunner.start(
|
|
180
185
|
workflow,
|
|
181
186
|
typeof req.body?.input === 'string' ? req.body.input : '',
|
|
182
|
-
|
|
187
|
+
{
|
|
188
|
+
...readMetadata(req.body),
|
|
189
|
+
userId: readRequestUserId(req),
|
|
190
|
+
workflowName: workflow.name,
|
|
191
|
+
},
|
|
183
192
|
);
|
|
184
193
|
res.status(202).json(run);
|
|
185
194
|
} catch (error) {
|
|
@@ -208,6 +208,155 @@ function buildManagedPackageCommand(command, runtimeStatus) {
|
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
function packageBinCandidates(binName) {
|
|
212
|
+
return process.platform === 'win32'
|
|
213
|
+
? [binName, `${binName}.cmd`, `${binName}.ps1`, `${binName}.exe`]
|
|
214
|
+
: [binName];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function expectedPackageBin(command) {
|
|
218
|
+
if (command.framework === 'Vite' || command.id === 'npm-dev-vite') return 'vite';
|
|
219
|
+
if (command.framework === 'Next.js' || command.id === 'npm-dev-next') return 'next';
|
|
220
|
+
if (command.framework === 'Nuxt' || command.id === 'npm-dev-nuxt') return 'nuxt';
|
|
221
|
+
if (command.framework === 'Astro' || command.id === 'npm-dev-astro') return 'astro';
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function packageDependenciesReady(projectPath, command) {
|
|
226
|
+
if (!command?.scriptName && !command?.packageManager) return true;
|
|
227
|
+
if (!(await dirExists(path.join(projectPath, 'node_modules')))) return false;
|
|
228
|
+
|
|
229
|
+
const binName = expectedPackageBin(command);
|
|
230
|
+
if (!binName) return true;
|
|
231
|
+
|
|
232
|
+
const binDir = path.join(projectPath, 'node_modules', '.bin');
|
|
233
|
+
for (const candidate of packageBinCandidates(binName)) {
|
|
234
|
+
if (await fileExists(path.join(binDir, candidate))) return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function packageInstallInvocation(command) {
|
|
240
|
+
if (!command || command.packageManager !== 'npm') return null;
|
|
241
|
+
const installArgs = ['install', '--no-audit', '--no-fund', '--include=dev'];
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
command.managedRuntime?.id === 'npm'
|
|
245
|
+
&& command.args?.[0]
|
|
246
|
+
&& String(command.args[0]).endsWith('npm-cli.js')
|
|
247
|
+
) {
|
|
248
|
+
return {
|
|
249
|
+
command: command.command,
|
|
250
|
+
args: [command.args[0], ...installArgs],
|
|
251
|
+
displayCommand: 'npm install --no-audit --no-fund --include=dev',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (command.command === 'npm' || path.basename(command.command || '').startsWith('npm')) {
|
|
256
|
+
return {
|
|
257
|
+
command: command.command,
|
|
258
|
+
args: installArgs,
|
|
259
|
+
displayCommand: 'npm install --no-audit --no-fund --include=dev',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function runPrepProcess(command, args, options = {}) {
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
let settled = false;
|
|
269
|
+
const child = spawn(command, args, {
|
|
270
|
+
cwd: options.cwd,
|
|
271
|
+
env: options.env,
|
|
272
|
+
shell: shouldUseShell({ command }),
|
|
273
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
274
|
+
windowsHide: true,
|
|
275
|
+
});
|
|
276
|
+
const timeoutMs = Number.parseInt(process.env.PIXCODE_LIVE_VIEW_INSTALL_TIMEOUT_MS || '', 10) || 300000;
|
|
277
|
+
const finish = (callback, value) => {
|
|
278
|
+
if (settled) return;
|
|
279
|
+
settled = true;
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
callback(value);
|
|
282
|
+
};
|
|
283
|
+
const timer = setTimeout(() => {
|
|
284
|
+
try {
|
|
285
|
+
child.kill();
|
|
286
|
+
} catch {
|
|
287
|
+
// Process may already be gone.
|
|
288
|
+
}
|
|
289
|
+
finish(reject, new Error(`Dependency install timed out after ${Math.round(timeoutMs / 1000)}s.`));
|
|
290
|
+
}, timeoutMs);
|
|
291
|
+
|
|
292
|
+
child.stdout.on('data', (chunk) => {
|
|
293
|
+
chunk.toString().split(/\r?\n/).forEach((line) => options.onLog?.(line));
|
|
294
|
+
});
|
|
295
|
+
child.stderr.on('data', (chunk) => {
|
|
296
|
+
chunk.toString().split(/\r?\n/).forEach((line) => options.onLog?.(line));
|
|
297
|
+
});
|
|
298
|
+
child.on('error', (error) => finish(reject, error));
|
|
299
|
+
child.on('exit', (code, signal) => {
|
|
300
|
+
if (code === 0) {
|
|
301
|
+
finish(resolve);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
finish(reject, new Error(`Dependency install exited with ${signal || `code ${code}`}.`));
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function preparePackageDependencies(projectPath, command, env = process.env, onLog = () => {}) {
|
|
310
|
+
if (!command || command.packageManager !== 'npm') return false;
|
|
311
|
+
if (await packageDependenciesReady(projectPath, command)) return false;
|
|
312
|
+
|
|
313
|
+
const install = packageInstallInvocation(command);
|
|
314
|
+
if (!install) return false;
|
|
315
|
+
|
|
316
|
+
onLog(`Installing project dependencies: ${install.displayCommand}`);
|
|
317
|
+
await runPrepProcess(install.command, install.args, {
|
|
318
|
+
cwd: projectPath,
|
|
319
|
+
env: {
|
|
320
|
+
...env,
|
|
321
|
+
NODE_ENV: 'development',
|
|
322
|
+
NPM_CONFIG_PRODUCTION: 'false',
|
|
323
|
+
npm_config_production: 'false',
|
|
324
|
+
},
|
|
325
|
+
onLog,
|
|
326
|
+
});
|
|
327
|
+
if (!(await packageDependenciesReady(projectPath, command))) {
|
|
328
|
+
const binName = expectedPackageBin(command);
|
|
329
|
+
throw new Error(binName
|
|
330
|
+
? `${binName} was not installed. Check package.json dependencies or install output.`
|
|
331
|
+
: 'Project dependencies were not installed.');
|
|
332
|
+
}
|
|
333
|
+
onLog('Project dependencies are ready.');
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function prependPathEntries(env, entries) {
|
|
338
|
+
const cleanEntries = entries.filter(Boolean);
|
|
339
|
+
if (!cleanEntries.length) return env;
|
|
340
|
+
|
|
341
|
+
const existingPath = env.Path || env.PATH || '';
|
|
342
|
+
const nextPath = [...cleanEntries, existingPath].filter(Boolean).join(path.delimiter);
|
|
343
|
+
return process.platform === 'win32'
|
|
344
|
+
? { ...env, PATH: nextPath, Path: nextPath }
|
|
345
|
+
: { ...env, PATH: nextPath };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function applyManagedRuntimeEnv(env, runtimeStatus) {
|
|
349
|
+
if (runtimeStatus?.id !== 'frankenphp' || !runtimeStatus.executablePath) {
|
|
350
|
+
return env;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const runtimeDir = path.dirname(runtimeStatus.executablePath);
|
|
354
|
+
return prependPathEntries(env, [
|
|
355
|
+
runtimeDir,
|
|
356
|
+
path.join(runtimeDir, 'ext'),
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
|
|
211
360
|
function buildManagedPhpCommand(runtimeStatus) {
|
|
212
361
|
const executable = runtimeStatus?.executablePath || 'frankenphp';
|
|
213
362
|
return {
|
|
@@ -711,6 +860,7 @@ export async function startLiveView(projectName, projectPath, options = {}) {
|
|
|
711
860
|
...buildCliSpawnEnv(process.env),
|
|
712
861
|
...(command.env || {}),
|
|
713
862
|
...(process.versions.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
|
863
|
+
NODE_ENV: 'development',
|
|
714
864
|
PORT: String(port),
|
|
715
865
|
HOST: '127.0.0.1',
|
|
716
866
|
VITE_HOST: '127.0.0.1',
|
|
@@ -718,16 +868,29 @@ export async function startLiveView(projectName, projectPath, options = {}) {
|
|
|
718
868
|
BROWSER: 'none',
|
|
719
869
|
NEXT_TELEMETRY_DISABLED: '1',
|
|
720
870
|
};
|
|
871
|
+
const spawnEnv = applyManagedRuntimeEnv(env, runtimeStatus);
|
|
872
|
+
|
|
873
|
+
sessionsByProject.set(projectName, session);
|
|
874
|
+
sessionsByShareId.set(shareId, session);
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
await preparePackageDependencies(projectPath, command, spawnEnv, (line) => appendLog(session, line));
|
|
878
|
+
} catch (error) {
|
|
879
|
+
session.status = 'error';
|
|
880
|
+
session.stoppedAt = new Date().toISOString();
|
|
881
|
+
session.error = error instanceof Error ? error.message : String(error);
|
|
882
|
+
appendLog(session, session.error);
|
|
883
|
+
return publicSession(session);
|
|
884
|
+
}
|
|
885
|
+
|
|
721
886
|
const child = spawn(command.command, command.args, {
|
|
722
887
|
cwd: projectPath,
|
|
723
|
-
env,
|
|
888
|
+
env: spawnEnv,
|
|
724
889
|
shell: shouldUseShell(command),
|
|
725
890
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
726
891
|
});
|
|
727
892
|
|
|
728
893
|
session.child = child;
|
|
729
|
-
sessionsByProject.set(projectName, session);
|
|
730
|
-
sessionsByShareId.set(shareId, session);
|
|
731
894
|
|
|
732
895
|
child.stdout.on('data', (chunk) => {
|
|
733
896
|
chunk.toString().split(/\r?\n/).forEach((line) => appendLog(session, line));
|
|
@@ -121,21 +121,39 @@ async function downloadFile(url, targetFile, env = process.env) {
|
|
|
121
121
|
function runProcess(command, args, options = {}) {
|
|
122
122
|
return new Promise((resolve, reject) => {
|
|
123
123
|
let stderr = '';
|
|
124
|
+
let settled = false;
|
|
125
|
+
const { timeoutMs, ...spawnOptions } = options;
|
|
124
126
|
const child = spawn(command, args, {
|
|
125
|
-
...
|
|
127
|
+
...spawnOptions,
|
|
126
128
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
127
129
|
windowsHide: true,
|
|
128
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;
|
|
129
147
|
child.stderr.on('data', (chunk) => {
|
|
130
148
|
stderr += chunk.toString();
|
|
131
149
|
});
|
|
132
|
-
child.on('error', reject);
|
|
150
|
+
child.on('error', (error) => finish(reject, error));
|
|
133
151
|
child.on('close', (code) => {
|
|
134
152
|
if (code === 0) {
|
|
135
|
-
resolve
|
|
153
|
+
finish(resolve);
|
|
136
154
|
return;
|
|
137
155
|
}
|
|
138
|
-
reject
|
|
156
|
+
finish(reject, new Error(`${command} exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
|
|
139
157
|
});
|
|
140
158
|
});
|
|
141
159
|
}
|
|
@@ -200,6 +218,27 @@ async function findRuntimeExecutable(searchRoot, binaryName) {
|
|
|
200
218
|
return null;
|
|
201
219
|
}
|
|
202
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
|
+
|
|
203
242
|
async function installFrankenPhp(env = process.env) {
|
|
204
243
|
const releaseApiUrl = env.PIXCODE_FRANKENPHP_RELEASE_API
|
|
205
244
|
|| 'https://api.github.com/repos/php/frankenphp/releases/latest';
|
|
@@ -245,12 +284,7 @@ async function installFrankenPhp(env = process.env) {
|
|
|
245
284
|
await fs.chmod(executablePath, 0o755).catch(() => undefined);
|
|
246
285
|
}
|
|
247
286
|
|
|
248
|
-
|
|
249
|
-
await fs.mkdir(currentDir, { recursive: true });
|
|
250
|
-
|
|
251
|
-
const finalName = process.platform === 'win32' ? 'frankenphp.exe' : 'frankenphp';
|
|
252
|
-
const finalExecutable = path.join(currentDir, finalName);
|
|
253
|
-
await fs.copyFile(executablePath, finalExecutable);
|
|
287
|
+
const finalExecutable = await copyRuntimeWithSidecars(executablePath, currentDir);
|
|
254
288
|
if (process.platform !== 'win32') {
|
|
255
289
|
await fs.chmod(finalExecutable, 0o755).catch(() => undefined);
|
|
256
290
|
}
|
|
@@ -379,6 +413,18 @@ export async function getManagedRuntimeStatus(id, options = {}) {
|
|
|
379
413
|
|
|
380
414
|
const manifest = await readManifest(id, env);
|
|
381
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
|
+
|
|
382
428
|
return {
|
|
383
429
|
id,
|
|
384
430
|
label: manifest.label || 'Pixcode PHP runtime',
|
|
@@ -172,8 +172,9 @@ function buildPushBody(event) {
|
|
|
172
172
|
|
|
173
173
|
function buildNotificationPayload(event) {
|
|
174
174
|
const pushBody = buildPushBody(event);
|
|
175
|
+
const baseId = event.dedupeKey || `${event.provider || 'system'}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
|
|
175
176
|
return {
|
|
176
|
-
id:
|
|
177
|
+
id: `${baseId}:${event.createdAt}`,
|
|
177
178
|
title: pushBody.title,
|
|
178
179
|
body: pushBody.body,
|
|
179
180
|
kind: event.kind || 'info',
|
|
@@ -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:
|
|
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 =
|
|
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 =
|
|
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 };
|
package/shared/modelConstants.js
CHANGED
|
@@ -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
|
|
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
|
|