@pixelbyte-software/pixcode 1.38.1 → 1.38.3

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.
Files changed (35) hide show
  1. package/dist/assets/index-BzgMq98S.css +32 -0
  2. package/dist/assets/{index-Br191izN.js → index-Cc0uLPZw.js} +140 -140
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +76 -1
  5. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  6. package/dist-server/server/modules/orchestration/workflows/workspace-target.js +2 -0
  7. package/dist-server/server/modules/orchestration/workflows/workspace-target.js.map +1 -1
  8. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +2 -1
  9. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -1
  10. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +5 -2
  11. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  12. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +1 -1
  13. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  14. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js +1 -1
  15. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js.map +1 -1
  16. package/dist-server/server/routes/taskmaster.js +31 -52
  17. package/dist-server/server/routes/taskmaster.js.map +1 -1
  18. package/dist-server/server/services/install-jobs.js +152 -17
  19. package/dist-server/server/services/install-jobs.js.map +1 -1
  20. package/package.json +1 -1
  21. package/scripts/smoke/chat-session-provider-pools.mjs +35 -0
  22. package/scripts/smoke/desktop-native-notifications.mjs +30 -0
  23. package/scripts/smoke/desktop-tray-icon.mjs +33 -0
  24. package/scripts/smoke/mac-desktop-runtime.mjs +43 -0
  25. package/scripts/smoke/multi-project-ui.mjs +45 -0
  26. package/scripts/smoke/orchestration-permission-fallback.mjs +34 -0
  27. package/server/modules/orchestration/workflows/workflow-runner.ts +105 -1
  28. package/server/modules/orchestration/workflows/workspace-target.ts +2 -0
  29. package/server/modules/providers/list/codex/codex-auth.provider.ts +2 -1
  30. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +6 -2
  31. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +1 -1
  32. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +1 -1
  33. package/server/routes/taskmaster.js +36 -57
  34. package/server/services/install-jobs.js +159 -16
  35. package/dist/assets/index-BzL2G4Sw.css +0 -32
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ const projectsState = readFileSync('src/hooks/useProjectsState.ts', 'utf8');
5
+
6
+ function assert(condition, message) {
7
+ if (!condition) {
8
+ console.error(`chat-session-provider-pools smoke failed: ${message}`);
9
+ process.exit(1);
10
+ }
11
+ }
12
+
13
+ const getProjectSessionsBody = projectsState.match(/const getProjectSessions = \(project: Project\): ProjectSession\[\] => \{[\s\S]*?\n\};/)?.[0] ?? '';
14
+
15
+ assert(
16
+ getProjectSessionsBody.includes('project.qwenSessions') && getProjectSessionsBody.includes('project.opencodeSessions'),
17
+ 'selected-session refresh logic should include qwen/opencode pools',
18
+ );
19
+
20
+ assert(
21
+ projectsState.includes('serialize(nextProject.qwenSessions)') && projectsState.includes('serialize(nextProject.opencodeSessions)'),
22
+ 'project change detection should include qwen/opencode sessions',
23
+ );
24
+
25
+ assert(
26
+ projectsState.includes("sessionId.startsWith('codex-')") && projectsState.includes("sessionId.startsWith('gemini_')"),
27
+ 'fallback session provider inference should cover codex and gemini ids, not only qwen/opencode',
28
+ );
29
+
30
+ assert(
31
+ projectsState.includes("localStorage.setItem('selected-provider', session.__provider)"),
32
+ 'selecting an existing session should persist that session provider before navigation',
33
+ );
34
+
35
+ console.log('chat-session-provider-pools smoke passed');
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+
3
+ const mainPath = 'desktop/electron/main.cjs';
4
+ const preloadPath = 'desktop/electron/preload.cjs';
5
+ const localNotificationsPath = 'src/utils/localNotifications.ts';
6
+ const globalsPath = 'src/types/global.d.ts';
7
+
8
+ function assert(condition, message) {
9
+ if (!condition) {
10
+ console.error(`FAIL: ${message}`);
11
+ process.exit(1);
12
+ }
13
+ }
14
+
15
+ const main = fs.readFileSync(mainPath, 'utf8');
16
+ const localNotifications = fs.readFileSync(localNotificationsPath, 'utf8');
17
+ const globals = fs.readFileSync(globalsPath, 'utf8');
18
+
19
+ assert(fs.existsSync(preloadPath), 'desktop preload bridge exists');
20
+ const preload = fs.readFileSync(preloadPath, 'utf8');
21
+
22
+ assert(main.includes("preload: path.join(__dirname, 'preload.cjs')"), 'BrowserWindow loads the desktop preload');
23
+ assert(main.includes("ipcMain.handle('pixcode:desktop-notification'"), 'main process handles desktop notification IPC');
24
+ assert(preload.includes('contextBridge.exposeInMainWorld'), 'preload exposes a safe renderer bridge');
25
+ assert(preload.includes('pixcodeDesktop'), 'preload exposes window.pixcodeDesktop');
26
+ assert(preload.includes("ipcRenderer.invoke('pixcode:desktop-notification'"), 'preload invokes desktop notification IPC');
27
+ assert(localNotifications.includes('window.pixcodeDesktop?.notify'), 'local notifications try the desktop native bridge first');
28
+ assert(globals.includes('pixcodeDesktop?:'), 'global window type declares the desktop bridge');
29
+
30
+ console.log('desktop native notification bridge smoke passed');
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ const main = readFileSync('desktop/electron/main.cjs', 'utf8');
5
+
6
+ function assert(condition, message) {
7
+ if (!condition) {
8
+ console.error(`desktop-tray-icon smoke failed: ${message}`);
9
+ process.exit(1);
10
+ }
11
+ }
12
+
13
+ assert(
14
+ main.includes('function normalizeTrayIcon'),
15
+ 'tray icons should be normalized before creating the Electron Tray',
16
+ );
17
+
18
+ assert(
19
+ main.includes("process.platform === 'darwin'") && main.includes('width: 18') && main.includes('height: 18'),
20
+ 'macOS menu bar icon should be resized to 18x18 instead of using the 1024 app icon',
21
+ );
22
+
23
+ assert(
24
+ main.includes('setTemplateImage(true)'),
25
+ 'macOS menu bar icon should be marked as a template image',
26
+ );
27
+
28
+ assert(
29
+ main.includes('return normalizeTrayIcon(nativeImage.createFromPath(p))'),
30
+ 'resolveTrayIcon should return the normalized image',
31
+ );
32
+
33
+ console.log('desktop-tray-icon smoke passed');
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ const installJobs = readFileSync('server/services/install-jobs.js', 'utf8');
5
+ const taskmaster = readFileSync('server/routes/taskmaster.js', 'utf8');
6
+ const codexAuth = readFileSync('server/modules/providers/list/codex/codex-auth.provider.ts', 'utf8');
7
+ const cursorAuth = readFileSync('server/modules/providers/list/cursor/cursor-auth.provider.ts', 'utf8');
8
+ const geminiAuth = readFileSync('server/modules/providers/list/gemini/gemini-auth.provider.ts', 'utf8');
9
+ const qwenAuth = readFileSync('server/modules/providers/list/qwen/qwen-auth.provider.ts', 'utf8');
10
+
11
+ function assert(condition, message) {
12
+ if (!condition) {
13
+ console.error(`mac-desktop-runtime smoke failed: ${message}`);
14
+ process.exit(1);
15
+ }
16
+ }
17
+
18
+ assert(
19
+ installJobs.includes('function collectUserShellPath'),
20
+ 'install-jobs should hydrate PATH from the user shell for macOS GUI launches',
21
+ );
22
+
23
+ assert(
24
+ installJobs.includes("'.nvm'") && installJobs.includes("'.volta'") && installJobs.includes("'.asdf'"),
25
+ 'install-jobs should search common user Node manager bin directories',
26
+ );
27
+
28
+ assert(
29
+ installJobs.includes("findExecutableOnPath('npm'") && installJobs.includes('buildCliSpawnEnv'),
30
+ 'Task/CLI installers should resolve npm from the augmented runtime PATH before spawning',
31
+ );
32
+
33
+ assert(
34
+ taskmaster.includes('findExecutableOnPath') && !taskmaster.includes("spawn('which'"),
35
+ 'TaskMaster status should use Pixcode executable resolution, not bare which from a minimal GUI PATH',
36
+ );
37
+
38
+ assert(codexAuth.includes('CODEX_CLI_PATH'), 'Codex auth should honor CODEX_CLI_PATH');
39
+ assert(cursorAuth.includes('CURSOR_CLI_PATH'), 'Cursor auth should honor CURSOR_CLI_PATH');
40
+ assert(geminiAuth.includes('GEMINI_CLI_PATH'), 'Gemini auth should honor GEMINI_CLI_PATH');
41
+ assert(qwenAuth.includes('QWEN_CLI_PATH'), 'Qwen auth should honor QWEN_CLI_PATH');
42
+
43
+ console.log('mac-desktop-runtime smoke passed');
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ const chatComposer = readFileSync('src/components/chat/view/subcomponents/ChatComposer.tsx', 'utf8');
5
+ const workerSlotsControl = readFileSync('src/components/chat/view/subcomponents/WorkerSlotsControl.tsx', 'utf8');
6
+ const mainContent = readFileSync('src/components/main-content/view/MainContent.tsx', 'utf8');
7
+
8
+ function assert(condition, message) {
9
+ if (!condition) {
10
+ console.error(`multi-project-ui smoke failed: ${message}`);
11
+ process.exit(1);
12
+ }
13
+ }
14
+
15
+ assert(
16
+ chatComposer.includes('worker-slot-composer-rail'),
17
+ 'chat composer should render the multi-project launcher in a dedicated rail next to submit',
18
+ );
19
+
20
+ assert(
21
+ /<WorkerSlotsControl[\s\S]*align="right"/.test(chatComposer),
22
+ 'chat composer should right-align the worker slot popover near the submit button',
23
+ );
24
+
25
+ assert(
26
+ workerSlotsControl.includes('MAX_WORKER_SLOTS') && workerSlotsControl.includes('workerSlotsFull'),
27
+ 'worker slots control should keep the four-slot limit visible in the UI',
28
+ );
29
+
30
+ assert(
31
+ workerSlotsControl.includes('translate-y-0') && workerSlotsControl.includes('opacity-100'),
32
+ 'worker slots popover should animate into view instead of appearing abruptly',
33
+ );
34
+
35
+ assert(
36
+ mainContent.includes('x: 28') && mainContent.includes("transformOrigin: 'right center'"),
37
+ 'right side panels should animate from the right edge toward the left',
38
+ );
39
+
40
+ assert(
41
+ mainContent.includes('transition-[width,opacity,transform]'),
42
+ 'split panes should animate width, opacity, and transform changes',
43
+ );
44
+
45
+ console.log('multi-project-ui smoke passed');
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ const runner = readFileSync('server/modules/orchestration/workflows/workflow-runner.ts', 'utf8');
5
+ const workspaceTarget = readFileSync('server/modules/orchestration/workflows/workspace-target.ts', 'utf8');
6
+
7
+ function assert(condition, message) {
8
+ if (!condition) {
9
+ console.error(`orchestration-permission-fallback smoke failed: ${message}`);
10
+ process.exit(1);
11
+ }
12
+ }
13
+
14
+ assert(
15
+ runner.includes('isExternalDirectoryPermissionError'),
16
+ 'workflow runner should classify external_directory permission failures',
17
+ );
18
+
19
+ assert(
20
+ runner.includes('buildPermissionFallbackOutput') && runner.includes('buildFallbackFinalReport'),
21
+ 'workflow runner should synthesize user-facing outputs instead of failing the whole run',
22
+ );
23
+
24
+ assert(
25
+ runner.includes('resolveNodePermissionMode') && runner.includes('bypassPermissions'),
26
+ 'host workspace orchestration should avoid default auto-reject permission mode for selected project paths',
27
+ );
28
+
29
+ assert(
30
+ workspaceTarget.includes('Do not access parent directories') && workspaceTarget.includes('If a tool reports a permission denial'),
31
+ 'workspace prompt should tell agents to stay inside the selected workspace and report permission denials',
32
+ );
33
+
34
+ console.log('orchestration-permission-fallback smoke passed');
@@ -7,6 +7,7 @@ import type {
7
7
  WorkflowRun,
8
8
  } from '@/modules/orchestration/workflows/workflow.types.js';
9
9
  import {
10
+ type ResolvedWorkspaceTarget,
10
11
  resolveWorkflowWorkspace,
11
12
  workspaceContextPrompt,
12
13
  workspaceTargetMetadata,
@@ -362,6 +363,92 @@ function compactOutputForContext(text: string): string {
362
363
  ].join('');
363
364
  }
364
365
 
366
+ function isExternalDirectoryPermissionError(value: unknown): boolean {
367
+ const text = String(value ?? '').toLocaleLowerCase('en');
368
+ return (
369
+ text.includes('external_directory') ||
370
+ /permission requested:.*auto-rejecting/u.test(text) ||
371
+ /auto-rejecting.*permission/u.test(text) ||
372
+ /outside (the )?(workspace|working directory)/u.test(text) ||
373
+ /permission.*external/u.test(text)
374
+ );
375
+ }
376
+
377
+ function isFinalReportNode(node: WorkflowNode): boolean {
378
+ return node.id === 'final_report' || node.stage === 'final_report' || node.stage === 'report';
379
+ }
380
+
381
+ function workspaceNeedsHostPermissionBypass(target: ResolvedWorkspaceTarget): boolean {
382
+ return (target.kind === 'selected_project' || target.kind === 'custom') && target.projectPath !== target.appRoot;
383
+ }
384
+
385
+ function resolveNodePermissionMode(node: WorkflowNode, target: ResolvedWorkspaceTarget): string | undefined {
386
+ if (node.permissionMode && node.permissionMode !== 'default') {
387
+ return node.permissionMode;
388
+ }
389
+
390
+ if (workspaceNeedsHostPermissionBypass(target)) {
391
+ return 'bypassPermissions';
392
+ }
393
+
394
+ return node.permissionMode;
395
+ }
396
+
397
+ function buildPermissionFallbackOutput(
398
+ node: WorkflowNode,
399
+ reason: string,
400
+ target: ResolvedWorkspaceTarget,
401
+ ): string {
402
+ return [
403
+ 'Bu adım çalışma alanı izin sınırına takıldı.',
404
+ '',
405
+ `Ajan: ${node.agentLabel || node.id}`,
406
+ `Hedef çalışma alanı: ${target.projectPath}`,
407
+ `Hata: ${reason}`,
408
+ '',
409
+ 'Pixcode bu adımı workflow dışına taşırmadan devam ettirdi. Ajan aynı dış dizin yoluna tekrar tekrar erişmek yerine mevcut bağlamla ilerlemeli.',
410
+ ].join('\n');
411
+ }
412
+
413
+ function buildFallbackFinalReport(
414
+ outputs: Map<string, string>,
415
+ reason: string,
416
+ target: ResolvedWorkspaceTarget,
417
+ ): string {
418
+ const completedOutputs = [...outputs.entries()]
419
+ .map(([nodeId, output]) => [`## ${nodeId}`, output || '(çıktı yok)'].join('\n'))
420
+ .join('\n\n');
421
+
422
+ return [
423
+ 'Final rapor aracı çalışma alanı izin sınırına takıldı, bu yüzden Pixcode tamamlanan ajan çıktılarından güvenli bir özet üretti.',
424
+ '',
425
+ `Hedef çalışma alanı: ${target.projectPath}`,
426
+ `İzin hatası: ${reason}`,
427
+ '',
428
+ completedOutputs || 'Bu turda final rapora aktarılabilecek tamamlanmış ajan çıktısı yok.',
429
+ ].join('\n');
430
+ }
431
+
432
+ function completeNodeWithPermissionFallback(
433
+ nodeRun: WorkflowNodeRun,
434
+ node: WorkflowNode,
435
+ outputs: Map<string, string>,
436
+ completed: Set<string>,
437
+ reason: string,
438
+ target: ResolvedWorkspaceTarget,
439
+ ): void {
440
+ const outputText = isFinalReportNode(node)
441
+ ? buildFallbackFinalReport(outputs, reason, target)
442
+ : buildPermissionFallbackOutput(node, reason, target);
443
+
444
+ nodeRun.status = 'completed';
445
+ nodeRun.error = reason;
446
+ nodeRun.outputText = outputText;
447
+ nodeRun.finishedAt = nodeRun.finishedAt ?? Date.now();
448
+ outputs.set(node.id, compactOutputForContext(outputText));
449
+ completed.add(node.id);
450
+ }
451
+
365
452
  function expandAgentTeamWorkflow(workflow: Workflow, metadata?: Record<string, unknown>): Workflow {
366
453
  const agents = readAgentAssignments(metadata);
367
454
  if (agents.length === 0) {
@@ -1316,6 +1403,7 @@ class WorkflowRunner {
1316
1403
 
1317
1404
  nodeRun.status = 'running';
1318
1405
  nodeRun.startedAt = Date.now();
1406
+ nodeRun.permissionMode = resolveNodePermissionMode(node, resolveWorkflowWorkspace(run.metadata));
1319
1407
  workflowStore.setRun(run);
1320
1408
 
1321
1409
  const inputContext = node.inputs.map((input) => outputs.get(input)).filter(Boolean).join('\n\n');
@@ -1328,6 +1416,7 @@ class WorkflowRunner {
1328
1416
  const isolation = readIsolation(settings.isolation) ?? node.isolation ?? 'host';
1329
1417
  const keepAfterCompletion = readBoolean(settings.keepWorkspace) ?? true;
1330
1418
  const baseRef = readString(settings.baseRef) ?? 'HEAD';
1419
+ const effectivePermissionMode = resolveNodePermissionMode(node, workspaceTarget);
1331
1420
  let body: { id?: string; error?: { message?: string } };
1332
1421
  try {
1333
1422
  const submit = await fetch(`${localA2ABaseUrl()}/tasks`, {
@@ -1348,7 +1437,7 @@ class WorkflowRunner {
1348
1437
  agentLabel: node.agentLabel,
1349
1438
  assignment: node.assignment,
1350
1439
  model: node.model,
1351
- permissionMode: node.permissionMode,
1440
+ permissionMode: effectivePermissionMode,
1352
1441
  toolsSettings: node.toolsSettings,
1353
1442
  projectPath,
1354
1443
  workspaceTarget: workspaceTargetMetadata(workspaceTarget),
@@ -1370,6 +1459,11 @@ class WorkflowRunner {
1370
1459
  nodeRun.status = 'failed';
1371
1460
  nodeRun.error = error instanceof Error ? error.message : String(error);
1372
1461
  workflowStore.setRun(run);
1462
+ if (isExternalDirectoryPermissionError(nodeRun.error)) {
1463
+ completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
1464
+ workflowStore.setRun(run);
1465
+ return;
1466
+ }
1373
1467
  if (await this.runFallbackAfterFailure(node, workflow, run, outputs, started, completed, nodeRun.error)) {
1374
1468
  return;
1375
1469
  }
@@ -1417,6 +1511,11 @@ class WorkflowRunner {
1417
1511
  outputs.set(node.id, compactOutputForContext(nodeRun.outputText));
1418
1512
  }
1419
1513
  workflowStore.setRun(run);
1514
+ if (isExternalDirectoryPermissionError(nodeRun.error)) {
1515
+ completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
1516
+ workflowStore.setRun(run);
1517
+ return;
1518
+ }
1420
1519
  if (await this.runFallbackAfterFailure(node, workflow, run, outputs, started, completed, nodeRun.error)) {
1421
1520
  return;
1422
1521
  }
@@ -1452,6 +1551,11 @@ class WorkflowRunner {
1452
1551
  nodeRun.status = 'failed';
1453
1552
  nodeRun.error = result.error ?? `A2A task ended with ${result.state}`;
1454
1553
  workflowStore.setRun(run);
1554
+ if (isExternalDirectoryPermissionError(`${nodeRun.error}\n${nodeRun.outputText ?? ''}`)) {
1555
+ completeNodeWithPermissionFallback(nodeRun, node, outputs, completed, nodeRun.error, workspaceTarget);
1556
+ workflowStore.setRun(run);
1557
+ return;
1558
+ }
1455
1559
  if (await this.runFallbackAfterFailure(node, workflow, run, outputs, started, completed, nodeRun.error)) {
1456
1560
  return;
1457
1561
  }
@@ -114,7 +114,9 @@ export function workspaceContextPrompt(target: ResolvedWorkspaceTarget): string
114
114
  'Rules:',
115
115
  '- Treat the working directory above as the authoritative project/application for this task.',
116
116
  '- Do not guess another repository path unless the user explicitly asks for it.',
117
+ '- Do not access parent directories, sibling projects, or broad external globs outside the working directory unless the user explicitly asks.',
117
118
  '- If the user asks to analyze or modify Pixcode/orchestration itself, the target workspace must be the Pixcode app root.',
118
119
  '- Before running package-manager commands, inspect the package files in the working directory.',
120
+ '- If a tool reports a permission denial, summarize the limitation and continue from the provided context instead of retrying the same external path.',
119
121
  ].filter(Boolean).join('\n');
120
122
  }
@@ -24,8 +24,9 @@ export class CodexProviderAuth implements IProviderAuth {
24
24
  * Checks whether Codex is available to the server runtime.
25
25
  */
26
26
  private checkInstalled(): boolean {
27
+ const cliPath = process.env.CODEX_CLI_PATH || 'codex';
27
28
  try {
28
- const result = spawn.sync('codex', ['--version'], { stdio: 'ignore', timeout: 5000 });
29
+ const result = spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
29
30
  return !result.error && result.status === 0;
30
31
  } catch {
31
32
  return false;
@@ -11,12 +11,16 @@ type CursorLoginStatus = {
11
11
  };
12
12
 
13
13
  export class CursorProviderAuth implements IProviderAuth {
14
+ private cliPath(): string {
15
+ return process.env.CURSOR_CLI_PATH || 'cursor-agent';
16
+ }
17
+
14
18
  /**
15
19
  * Checks whether the cursor-agent CLI is available on this host.
16
20
  */
17
21
  private checkInstalled(): boolean {
18
22
  try {
19
- const result = spawn.sync('cursor-agent', ['--version'], { stdio: 'ignore', timeout: 5000 });
23
+ const result = spawn.sync(this.cliPath(), ['--version'], { stdio: 'ignore', timeout: 5000 });
20
24
  return !result.error && result.status === 0;
21
25
  } catch {
22
26
  return false;
@@ -74,7 +78,7 @@ export class CursorProviderAuth implements IProviderAuth {
74
78
  }, 5000);
75
79
 
76
80
  try {
77
- childProcess = spawn('cursor-agent', ['status']);
81
+ childProcess = spawn(this.cliPath(), ['status']);
78
82
  } catch {
79
83
  clearTimeout(timeout);
80
84
  processCompleted = true;
@@ -24,7 +24,7 @@ export class GeminiProviderAuth implements IProviderAuth {
24
24
  * Checks whether the Gemini CLI is available on this host.
25
25
  */
26
26
  private checkInstalled(): boolean {
27
- const cliPath = process.env.GEMINI_PATH || 'gemini';
27
+ const cliPath = process.env.GEMINI_CLI_PATH || process.env.GEMINI_PATH || 'gemini';
28
28
  try {
29
29
  const result = spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
30
30
  return !result.error && result.status === 0;
@@ -32,7 +32,7 @@ type QwenCredentialsStatus = {
32
32
  */
33
33
  export class QwenProviderAuth implements IProviderAuth {
34
34
  private checkInstalled(): boolean {
35
- const cliPath = process.env.QWEN_PATH || 'qwen';
35
+ const cliPath = process.env.QWEN_CLI_PATH || process.env.QWEN_PATH || 'qwen';
36
36
  try {
37
37
  const result = spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
38
38
  return !result.error && result.status === 0;
@@ -13,13 +13,16 @@ import path from 'path';
13
13
  import { spawn } from 'child_process';
14
14
 
15
15
  import express from 'express';
16
+ import crossSpawn from 'cross-spawn';
16
17
 
17
18
  import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
18
19
 
19
20
  import { extractProjectDirectory } from '../projects.js';
20
21
  import {
21
22
  cancelInstallJob,
23
+ buildCliSpawnEnv,
22
24
  createInstallJob,
25
+ findExecutableOnPath,
23
26
  getInstallJob,
24
27
  snapshotDonePayload
25
28
  } from '../services/install-jobs.js';
@@ -34,70 +37,46 @@ const router = express.Router();
34
37
  */
35
38
  async function checkTaskMasterInstallation() {
36
39
  return new Promise((resolve) => {
37
- // Check if task-master command is available
38
- const child = spawn('which', ['task-master'], {
40
+ const env = buildCliSpawnEnv();
41
+ const taskMasterPath = findExecutableOnPath('task-master', env);
42
+
43
+ if (!taskMasterPath) {
44
+ resolve({
45
+ isInstalled: false,
46
+ installPath: null,
47
+ version: null,
48
+ reason: 'TaskMaster CLI not found in PATH'
49
+ });
50
+ return;
51
+ }
52
+
53
+ const versionChild = crossSpawn(taskMasterPath, ['--version'], {
39
54
  stdio: ['ignore', 'pipe', 'pipe'],
40
- shell: true
55
+ env,
56
+ windowsHide: true,
41
57
  });
42
-
43
- let output = '';
44
- let errorOutput = '';
45
-
46
- child.stdout.on('data', (data) => {
47
- output += data.toString();
48
- });
49
-
50
- child.stderr.on('data', (data) => {
51
- errorOutput += data.toString();
52
- });
53
-
54
- child.on('close', (code) => {
55
- if (code === 0 && output.trim()) {
56
- // TaskMaster is installed, get version
57
- const versionChild = spawn('task-master', ['--version'], {
58
- stdio: ['ignore', 'pipe', 'pipe'],
59
- shell: true
60
- });
61
-
62
- let versionOutput = '';
63
58
 
64
- versionChild.stdout.on('data', (data) => {
65
- versionOutput += data.toString();
66
- });
59
+ let versionOutput = '';
67
60
 
68
- versionChild.on('close', (versionCode) => {
69
- resolve({
70
- isInstalled: true,
71
- installPath: output.trim(),
72
- version: versionCode === 0 ? versionOutput.trim() : 'unknown',
73
- reason: null
74
- });
75
- });
61
+ versionChild.stdout?.on('data', (data) => {
62
+ versionOutput += data.toString();
63
+ });
76
64
 
77
- versionChild.on('error', () => {
78
- resolve({
79
- isInstalled: true,
80
- installPath: output.trim(),
81
- version: 'unknown',
82
- reason: null
83
- });
84
- });
85
- } else {
86
- resolve({
87
- isInstalled: false,
88
- installPath: null,
89
- version: null,
90
- reason: 'TaskMaster CLI not found in PATH'
91
- });
92
- }
65
+ versionChild.on('close', (versionCode) => {
66
+ resolve({
67
+ isInstalled: true,
68
+ installPath: taskMasterPath,
69
+ version: versionCode === 0 ? versionOutput.trim() : 'unknown',
70
+ reason: null
71
+ });
93
72
  });
94
-
95
- child.on('error', (error) => {
73
+
74
+ versionChild.on('error', () => {
96
75
  resolve({
97
- isInstalled: false,
98
- installPath: null,
99
- version: null,
100
- reason: `Error checking installation: ${error.message}`
76
+ isInstalled: true,
77
+ installPath: taskMasterPath,
78
+ version: 'unknown',
79
+ reason: null
101
80
  });
102
81
  });
103
82
  });