@pixelbyte-software/pixcode 1.38.2 → 1.38.4
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-B-OgjpDF.css +32 -0
- package/dist/assets/{index-BJiAbLzU.js → index-Dk77bpMj.js} +174 -174
- package/dist/index.html +2 -2
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +76 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workspace-target.js +2 -0
- package/dist-server/server/modules/orchestration/workflows/workspace-target.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +2 -1
- package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +5 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js +1 -1
- package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js.map +1 -1
- package/dist-server/server/routes/git.js +136 -1
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/network.js +5 -1
- package/dist-server/server/routes/network.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +31 -52
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/services/external-access.js +20 -5
- package/dist-server/server/services/external-access.js.map +1 -1
- package/dist-server/server/services/install-jobs.js +152 -17
- package/dist-server/server/services/install-jobs.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/chat-session-provider-pools.mjs +35 -0
- package/scripts/smoke/command-center-non-git.mjs +46 -0
- package/scripts/smoke/desktop-tray-icon.mjs +33 -0
- package/scripts/smoke/mac-desktop-runtime.mjs +43 -0
- package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -0
- package/scripts/smoke/multi-project-ui.mjs +45 -0
- package/scripts/smoke/notification-inapp-preference.mjs +23 -0
- package/scripts/smoke/orchestration-permission-fallback.mjs +34 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +105 -1
- package/server/modules/orchestration/workflows/workspace-target.ts +2 -0
- package/server/modules/providers/list/codex/codex-auth.provider.ts +2 -1
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +6 -2
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +1 -1
- package/server/modules/providers/list/qwen/qwen-auth.provider.ts +1 -1
- package/server/routes/git.js +155 -1
- package/server/routes/network.js +5 -1
- package/server/routes/taskmaster.js +36 -57
- package/server/services/external-access.js +21 -5
- package/server/services/install-jobs.js +159 -16
- package/dist/assets/index-BzL2G4Sw.css +0 -32
package/server/routes/git.js
CHANGED
|
@@ -10,6 +10,150 @@ import { spawnCursor } from '../cursor-cli.js';
|
|
|
10
10
|
|
|
11
11
|
const router = express.Router();
|
|
12
12
|
const COMMIT_DIFF_CHARACTER_LIMIT = 500_000;
|
|
13
|
+
const FILESYSTEM_SCAN_MAX_FILES = 5_000;
|
|
14
|
+
const FILESYSTEM_SCAN_MAX_DEPTH = 10;
|
|
15
|
+
const filesystemChangeSnapshots = new Map();
|
|
16
|
+
const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
|
|
17
|
+
'.git',
|
|
18
|
+
'.hg',
|
|
19
|
+
'.svn',
|
|
20
|
+
'node_modules',
|
|
21
|
+
'dist',
|
|
22
|
+
'dist-server',
|
|
23
|
+
'build',
|
|
24
|
+
'.next',
|
|
25
|
+
'.nuxt',
|
|
26
|
+
'.svelte-kit',
|
|
27
|
+
'coverage',
|
|
28
|
+
'.turbo',
|
|
29
|
+
'.cache',
|
|
30
|
+
'.pixcode-dev',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function isNotGitRepositoryMessage(message = '') {
|
|
34
|
+
return message.includes('Not a git repository')
|
|
35
|
+
|| message.includes('not a git repository')
|
|
36
|
+
|| message.includes('Project directory is not a git repository');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function shouldSkipFilesystemEntry(entryName) {
|
|
40
|
+
return FILESYSTEM_SCAN_EXCLUDED_DIRS.has(entryName)
|
|
41
|
+
|| entryName.endsWith('.log')
|
|
42
|
+
|| entryName === '.DS_Store';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toProjectRelativePath(projectPath, filePath) {
|
|
46
|
+
return path.relative(projectPath, filePath).replace(/\\/g, '/');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function collectFilesystemSnapshot(projectPath) {
|
|
50
|
+
const snapshot = new Map();
|
|
51
|
+
let limitReached = false;
|
|
52
|
+
|
|
53
|
+
async function walk(directoryPath, depth) {
|
|
54
|
+
if (limitReached || depth > FILESYSTEM_SCAN_MAX_DEPTH) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let entries = [];
|
|
59
|
+
try {
|
|
60
|
+
entries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (limitReached || shouldSkipFilesystemEntry(entry.name)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const absolutePath = path.join(directoryPath, entry.name);
|
|
71
|
+
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
await walk(absolutePath, depth + 1);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!entry.isFile()) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const stat = await fs.stat(absolutePath);
|
|
83
|
+
snapshot.set(toProjectRelativePath(projectPath, absolutePath), {
|
|
84
|
+
mtimeMs: Math.round(stat.mtimeMs),
|
|
85
|
+
size: stat.size,
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (snapshot.size >= FILESYSTEM_SCAN_MAX_FILES) {
|
|
92
|
+
limitReached = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await walk(projectPath, 0);
|
|
99
|
+
return { snapshot, limitReached };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function diffFilesystemSnapshots(previousSnapshot, nextSnapshot) {
|
|
103
|
+
if (!previousSnapshot) {
|
|
104
|
+
return { modified: [], added: [], deleted: [] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const modified = [];
|
|
108
|
+
const added = [];
|
|
109
|
+
const deleted = [];
|
|
110
|
+
|
|
111
|
+
for (const [filePath, nextMeta] of nextSnapshot.entries()) {
|
|
112
|
+
const previousMeta = previousSnapshot.get(filePath);
|
|
113
|
+
if (!previousMeta) {
|
|
114
|
+
added.push(filePath);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (previousMeta.mtimeMs !== nextMeta.mtimeMs || previousMeta.size !== nextMeta.size) {
|
|
119
|
+
modified.push(filePath);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const filePath of previousSnapshot.keys()) {
|
|
124
|
+
if (!nextSnapshot.has(filePath)) {
|
|
125
|
+
deleted.push(filePath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
modified: modified.sort(),
|
|
131
|
+
added: added.sort(),
|
|
132
|
+
deleted: deleted.sort(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function buildFilesystemStatus(projectPath) {
|
|
137
|
+
const normalizedProjectPath = path.resolve(projectPath);
|
|
138
|
+
const previousSnapshot = filesystemChangeSnapshots.get(normalizedProjectPath) ?? null;
|
|
139
|
+
const { snapshot, limitReached } = await collectFilesystemSnapshot(normalizedProjectPath);
|
|
140
|
+
filesystemChangeSnapshots.set(normalizedProjectPath, snapshot);
|
|
141
|
+
const { modified, added, deleted } = diffFilesystemSnapshots(previousSnapshot, snapshot);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
isGitRepository: false,
|
|
145
|
+
trackingMode: 'filesystem',
|
|
146
|
+
branch: null,
|
|
147
|
+
hasCommits: false,
|
|
148
|
+
modified,
|
|
149
|
+
added,
|
|
150
|
+
deleted,
|
|
151
|
+
untracked: [],
|
|
152
|
+
snapshotReady: Boolean(previousSnapshot),
|
|
153
|
+
fileCount: snapshot.size,
|
|
154
|
+
scanLimitReached: limitReached,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
13
157
|
|
|
14
158
|
function spawnAsync(command, args, options = {}) {
|
|
15
159
|
return new Promise((resolve, reject) => {
|
|
@@ -297,8 +441,9 @@ router.get('/status', async (req, res) => {
|
|
|
297
441
|
return res.status(400).json({ error: 'Project name is required' });
|
|
298
442
|
}
|
|
299
443
|
|
|
444
|
+
let projectPath;
|
|
300
445
|
try {
|
|
301
|
-
|
|
446
|
+
projectPath = await getActualProjectPath(project);
|
|
302
447
|
|
|
303
448
|
// Validate git repository
|
|
304
449
|
await validateGitRepository(projectPath);
|
|
@@ -340,6 +485,15 @@ router.get('/status', async (req, res) => {
|
|
|
340
485
|
untracked
|
|
341
486
|
});
|
|
342
487
|
} catch (error) {
|
|
488
|
+
if (projectPath && isNotGitRepositoryMessage(error.message)) {
|
|
489
|
+
try {
|
|
490
|
+
res.json(await buildFilesystemStatus(projectPath));
|
|
491
|
+
return;
|
|
492
|
+
} catch (fallbackError) {
|
|
493
|
+
console.error('Filesystem status fallback error:', fallbackError);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
343
497
|
console.error('Git status error:', error);
|
|
344
498
|
res.json({
|
|
345
499
|
error: error.message.includes('not a git repository') || error.message.includes('Project directory is not a git repository')
|
package/server/routes/network.js
CHANGED
|
@@ -104,7 +104,11 @@ router.post('/tunnel', async (req, res) => {
|
|
|
104
104
|
// required external binary is missing" — it tells the UI to show the
|
|
105
105
|
// "install cloudflared/ngrok" hint rather than a generic server error.
|
|
106
106
|
const status = error?.code === 'ENOENT_TUNNEL' ? 424 : 502;
|
|
107
|
-
res.status(status).json({
|
|
107
|
+
res.status(status).json({
|
|
108
|
+
error: error?.message || 'Tunnel start failed',
|
|
109
|
+
installHint: error?.installHint,
|
|
110
|
+
tunnel: getTunnelState(),
|
|
111
|
+
});
|
|
108
112
|
}
|
|
109
113
|
});
|
|
110
114
|
|
|
@@ -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
|
-
|
|
38
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
versionOutput += data.toString();
|
|
66
|
-
});
|
|
59
|
+
let versionOutput = '';
|
|
67
60
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
73
|
+
|
|
74
|
+
versionChild.on('error', () => {
|
|
96
75
|
resolve({
|
|
97
|
-
isInstalled:
|
|
98
|
-
installPath:
|
|
99
|
-
version:
|
|
100
|
-
reason:
|
|
76
|
+
isInstalled: true,
|
|
77
|
+
installPath: taskMasterPath,
|
|
78
|
+
version: 'unknown',
|
|
79
|
+
reason: null
|
|
101
80
|
});
|
|
102
81
|
});
|
|
103
82
|
});
|
|
@@ -37,6 +37,7 @@ let tunnelState = {
|
|
|
37
37
|
binary: null, // 'cloudflared' | 'ngrok'
|
|
38
38
|
url: null,
|
|
39
39
|
error: null,
|
|
40
|
+
installHint: null,
|
|
40
41
|
log: [],
|
|
41
42
|
};
|
|
42
43
|
|
|
@@ -66,6 +67,18 @@ const detectBinary = async () => {
|
|
|
66
67
|
return null;
|
|
67
68
|
};
|
|
68
69
|
|
|
70
|
+
const createTunnelInstallHint = () => ({
|
|
71
|
+
title: 'Tunnel binary required',
|
|
72
|
+
message: 'Install cloudflared or ngrok to create a public mobile URL. Local LAN QR codes still work on the same Wi-Fi/network.',
|
|
73
|
+
commands: [
|
|
74
|
+
'macOS: brew install cloudflared',
|
|
75
|
+
'Windows: winget install Cloudflare.cloudflared',
|
|
76
|
+
'Linux: install cloudflared from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/',
|
|
77
|
+
'Alternative: install and authenticate ngrok from https://ngrok.com/download',
|
|
78
|
+
],
|
|
79
|
+
docsUrl: 'https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/',
|
|
80
|
+
});
|
|
81
|
+
|
|
69
82
|
const cloudflareUrlRegex = /https?:\/\/[a-z0-9.-]+trycloudflare\.com/i;
|
|
70
83
|
const ngrokUrlRegex = /https?:\/\/[a-z0-9.-]+\.ngrok(-free)?\.(app|io)/i;
|
|
71
84
|
|
|
@@ -90,16 +103,18 @@ export const startTunnel = async ({ port }) => {
|
|
|
90
103
|
|
|
91
104
|
const binary = await detectBinary();
|
|
92
105
|
if (!binary) {
|
|
93
|
-
|
|
106
|
+
const installHint = createTunnelInstallHint();
|
|
107
|
+
tunnelState = { running: false, binary: null, url: null, error: 'No tunnel binary found', installHint, log: [] };
|
|
94
108
|
const err = new Error('No tunnel binary found (tried cloudflared, ngrok)');
|
|
95
109
|
err.code = 'ENOENT_TUNNEL';
|
|
110
|
+
err.installHint = installHint;
|
|
96
111
|
throw err;
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
const args = buildTunnelArgs(binary, port);
|
|
100
115
|
const child = spawn(binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
101
116
|
tunnelProc = child;
|
|
102
|
-
tunnelState = { running: true, binary, url: null, error: null, log: [] };
|
|
117
|
+
tunnelState = { running: true, binary, url: null, error: null, installHint: null, log: [] };
|
|
103
118
|
|
|
104
119
|
const handleChunk = (chunk) => {
|
|
105
120
|
const text = chunk.toString();
|
|
@@ -119,6 +134,7 @@ export const startTunnel = async ({ port }) => {
|
|
|
119
134
|
binary,
|
|
120
135
|
url: null,
|
|
121
136
|
error: code === 0 ? null : `Tunnel exited with code ${code}`,
|
|
137
|
+
installHint: null,
|
|
122
138
|
log: tunnelState.log,
|
|
123
139
|
};
|
|
124
140
|
});
|
|
@@ -138,7 +154,7 @@ export const startTunnel = async ({ port }) => {
|
|
|
138
154
|
// If we never captured a URL, kill the child so we don't leak it.
|
|
139
155
|
try { child.kill(); } catch { /* ignore */ }
|
|
140
156
|
tunnelProc = null;
|
|
141
|
-
tunnelState = { ...tunnelState, running: false, error: 'Tunnel did not report a public URL' };
|
|
157
|
+
tunnelState = { ...tunnelState, running: false, error: 'Tunnel did not report a public URL', installHint: null };
|
|
142
158
|
throw new Error(tunnelState.error);
|
|
143
159
|
}
|
|
144
160
|
|
|
@@ -147,7 +163,7 @@ export const startTunnel = async ({ port }) => {
|
|
|
147
163
|
|
|
148
164
|
export const stopTunnel = async () => {
|
|
149
165
|
if (!tunnelProc) {
|
|
150
|
-
tunnelState = { running: false, binary: null, url: null, error: null, log: [] };
|
|
166
|
+
tunnelState = { running: false, binary: null, url: null, error: null, installHint: null, log: [] };
|
|
151
167
|
return tunnelState;
|
|
152
168
|
}
|
|
153
169
|
try {
|
|
@@ -156,7 +172,7 @@ export const stopTunnel = async () => {
|
|
|
156
172
|
// already dead
|
|
157
173
|
}
|
|
158
174
|
tunnelProc = null;
|
|
159
|
-
tunnelState = { running: false, binary: null, url: null, error: null, log: [] };
|
|
175
|
+
tunnelState = { running: false, binary: null, url: null, error: null, installHint: null, log: [] };
|
|
160
176
|
return tunnelState;
|
|
161
177
|
};
|
|
162
178
|
|
|
@@ -48,6 +48,11 @@ import spawn from 'cross-spawn';
|
|
|
48
48
|
const jobs = new Map();
|
|
49
49
|
const FINISHED_TTL_MS = 10 * 60 * 1000;
|
|
50
50
|
const HARD_TIMEOUT_MS = 10 * 60 * 1000;
|
|
51
|
+
const USER_SHELL_PATH_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
52
|
+
const userShellPathCache = {
|
|
53
|
+
value: null,
|
|
54
|
+
readAt: 0,
|
|
55
|
+
};
|
|
51
56
|
|
|
52
57
|
export const CLI_HOME = path.join(os.homedir(), '.pixcode', 'cli-bin');
|
|
53
58
|
export const CLI_BIN_DIR = path.join(CLI_HOME, 'node_modules', '.bin');
|
|
@@ -94,13 +99,9 @@ function ensureCliHome() {
|
|
|
94
99
|
*/
|
|
95
100
|
export function primeCliBinPath(env = process.env) {
|
|
96
101
|
ensureCliHome();
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
const next = current ? `${CLI_BIN_DIR}${sep}${current}` : CLI_BIN_DIR;
|
|
101
|
-
env.PATH = next;
|
|
102
|
-
if ('Path' in env) env.Path = next;
|
|
103
|
-
}
|
|
102
|
+
const augmentedEnv = buildCliSpawnEnv(env);
|
|
103
|
+
env.PATH = augmentedEnv.PATH;
|
|
104
|
+
if ('Path' in env || augmentedEnv.Path) env.Path = augmentedEnv.Path || augmentedEnv.PATH;
|
|
104
105
|
// Once PATH is ready, resolve any well-known provider binaries to absolute
|
|
105
106
|
// paths and export them as *_CLI_PATH env vars. This side-steps a Windows
|
|
106
107
|
// gotcha: `child_process.spawn('claude', …)` does NOT auto-resolve .cmd /
|
|
@@ -141,6 +142,135 @@ export function resolveProviderExecutables(env = process.env) {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
function pathSeparator() {
|
|
146
|
+
return process.platform === 'win32' ? ';' : ':';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function splitPathList(value) {
|
|
150
|
+
return String(value || '').split(pathSeparator()).map((entry) => entry.trim()).filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function collectNvmNodeBins(home) {
|
|
154
|
+
const versionsDir = path.join(home, '.nvm', 'versions', 'node');
|
|
155
|
+
try {
|
|
156
|
+
return fs.readdirSync(versionsDir)
|
|
157
|
+
.map((version) => path.join(versionsDir, version, 'bin'))
|
|
158
|
+
.filter((candidate) => {
|
|
159
|
+
try {
|
|
160
|
+
return fs.statSync(candidate).isDirectory();
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.sort()
|
|
166
|
+
.reverse();
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function collectKnownUserBinDirs(env = process.env) {
|
|
173
|
+
const home = os.homedir();
|
|
174
|
+
if (process.platform === 'win32') {
|
|
175
|
+
return [
|
|
176
|
+
path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'npm'),
|
|
177
|
+
path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Programs', 'nodejs'),
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [
|
|
182
|
+
CLI_BIN_DIR,
|
|
183
|
+
path.dirname(process.execPath),
|
|
184
|
+
...collectNvmNodeBins(home),
|
|
185
|
+
path.join(home, '.volta', 'bin'),
|
|
186
|
+
path.join(home, '.asdf', 'shims'),
|
|
187
|
+
path.join(home, '.bun', 'bin'),
|
|
188
|
+
path.join(home, '.local', 'bin'),
|
|
189
|
+
path.join(home, '.npm-global', 'bin'),
|
|
190
|
+
'/opt/homebrew/bin',
|
|
191
|
+
'/opt/homebrew/sbin',
|
|
192
|
+
'/usr/local/bin',
|
|
193
|
+
'/usr/local/sbin',
|
|
194
|
+
'/usr/bin',
|
|
195
|
+
'/bin',
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function collectUserShellPath(env = process.env) {
|
|
200
|
+
if (process.platform === 'win32') return [];
|
|
201
|
+
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
if (userShellPathCache.value && now - userShellPathCache.readAt < USER_SHELL_PATH_CACHE_TTL_MS) {
|
|
204
|
+
return userShellPathCache.value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const shells = [env.SHELL, '/bin/zsh', '/bin/bash']
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.filter((candidate, index, list) => list.indexOf(candidate) === index)
|
|
210
|
+
.filter((candidate) => {
|
|
211
|
+
try {
|
|
212
|
+
return fs.existsSync(candidate);
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const marker = '__PIXCODE_LOGIN_PATH__=';
|
|
219
|
+
for (const shell of shells) {
|
|
220
|
+
try {
|
|
221
|
+
const output = execFileSync(shell, ['-lc', `printf '\\n${marker}%s\\n' "$PATH"`], {
|
|
222
|
+
encoding: 'utf8',
|
|
223
|
+
env,
|
|
224
|
+
timeout: 2500,
|
|
225
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
226
|
+
});
|
|
227
|
+
const line = output.split(/\r?\n/).reverse().find((part) => part.startsWith(marker));
|
|
228
|
+
const shellPath = line?.slice(marker.length);
|
|
229
|
+
if (shellPath) {
|
|
230
|
+
const entries = splitPathList(shellPath);
|
|
231
|
+
userShellPathCache.value = entries;
|
|
232
|
+
userShellPathCache.readAt = now;
|
|
233
|
+
return entries;
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
// GUI-launched macOS apps often have a tiny PATH. If the user's
|
|
237
|
+
// shell startup files are noisy or slow, fall back to known bins.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
userShellPathCache.value = [];
|
|
242
|
+
userShellPathCache.readAt = now;
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function mergePathEntries(env, preferredEntries) {
|
|
247
|
+
const existing = splitPathList(env.PATH || env.Path || '');
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
const merged = [];
|
|
250
|
+
|
|
251
|
+
for (const entry of [...preferredEntries, ...existing]) {
|
|
252
|
+
if (!entry) continue;
|
|
253
|
+
const key = path.resolve(entry);
|
|
254
|
+
if (seen.has(key)) continue;
|
|
255
|
+
seen.add(key);
|
|
256
|
+
merged.push(entry);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const nextPath = merged.join(pathSeparator());
|
|
260
|
+
env.PATH = nextPath;
|
|
261
|
+
if ('Path' in env) env.Path = nextPath;
|
|
262
|
+
return env;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function buildCliSpawnEnv(baseEnv = process.env) {
|
|
266
|
+
const env = { ...baseEnv };
|
|
267
|
+
return mergePathEntries(env, [
|
|
268
|
+
CLI_BIN_DIR,
|
|
269
|
+
...collectUserShellPath(baseEnv),
|
|
270
|
+
...collectKnownUserBinDirs(baseEnv),
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
|
|
144
274
|
/**
|
|
145
275
|
* Cross-platform lookup for the Claude Code CLI executable. The
|
|
146
276
|
* @anthropic-ai/claude-agent-sdk SDK spawns its target with plain
|
|
@@ -301,10 +431,8 @@ export function findExecutableOnPath(name, env = process.env) {
|
|
|
301
431
|
paths.push(path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Programs', `${name}-code`));
|
|
302
432
|
paths.push(path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'AnthropicClaude'));
|
|
303
433
|
} else {
|
|
304
|
-
paths.push(
|
|
305
|
-
paths.push(
|
|
306
|
-
paths.push('/usr/local/bin');
|
|
307
|
-
paths.push('/opt/homebrew/bin');
|
|
434
|
+
paths.push(...collectUserShellPath(env));
|
|
435
|
+
paths.push(...collectKnownUserBinDirs(env));
|
|
308
436
|
}
|
|
309
437
|
|
|
310
438
|
const exts = isWindows
|
|
@@ -331,7 +459,7 @@ export function findExecutableOnPath(name, env = process.env) {
|
|
|
331
459
|
* more reliable than trusting PATH — when Pixcode runs as a daemon, PATH
|
|
332
460
|
* is often minimal and doesn't include the user's node install.
|
|
333
461
|
*/
|
|
334
|
-
function resolveNpmCommand() {
|
|
462
|
+
function resolveNpmCommand(env = process.env) {
|
|
335
463
|
const nodeDir = path.dirname(process.execPath);
|
|
336
464
|
const isWindows = process.platform === 'win32';
|
|
337
465
|
const candidates = isWindows
|
|
@@ -348,8 +476,11 @@ function resolveNpmCommand() {
|
|
|
348
476
|
return siblingNpm; // we'll invoke `node <npm-cli.js>`
|
|
349
477
|
}
|
|
350
478
|
}
|
|
351
|
-
|
|
352
|
-
|
|
479
|
+
|
|
480
|
+
const resolvedFromPath = findExecutableOnPath('npm', env);
|
|
481
|
+
if (resolvedFromPath) return resolvedFromPath;
|
|
482
|
+
|
|
483
|
+
return null;
|
|
353
484
|
}
|
|
354
485
|
|
|
355
486
|
function packageFromCommand(installCmd) {
|
|
@@ -406,7 +537,19 @@ export function createInstallJob({ provider, installCmd, packageName }) {
|
|
|
406
537
|
appendLog('meta', `Installing ${pkg} into ${CLI_HOME}\n`);
|
|
407
538
|
appendLog('meta', `(sandboxed — no sudo / admin required)\n`);
|
|
408
539
|
|
|
409
|
-
const
|
|
540
|
+
const installEnv = buildCliSpawnEnv(process.env);
|
|
541
|
+
const npmCmd = resolveNpmCommand(installEnv);
|
|
542
|
+
if (!npmCmd) {
|
|
543
|
+
job.status = 'error';
|
|
544
|
+
job.error = 'npm was not found. Install Node.js/npm or add it to your macOS login shell PATH, then click Refresh.';
|
|
545
|
+
job.finishedAt = new Date().toISOString();
|
|
546
|
+
appendLog('stderr', job.error + '\n');
|
|
547
|
+
emitter.emit('done', buildDonePayload(job));
|
|
548
|
+
scheduleCleanup(job);
|
|
549
|
+
jobs.set(id, job);
|
|
550
|
+
return job;
|
|
551
|
+
}
|
|
552
|
+
|
|
410
553
|
const useNodeRunner = npmCmd.endsWith('.js');
|
|
411
554
|
|
|
412
555
|
const cmd = useNodeRunner ? process.execPath : npmCmd;
|
|
@@ -420,7 +563,7 @@ export function createInstallJob({ provider, installCmd, packageName }) {
|
|
|
420
563
|
try {
|
|
421
564
|
child = spawn(cmd, args, {
|
|
422
565
|
cwd: CLI_HOME,
|
|
423
|
-
env: { ...
|
|
566
|
+
env: { ...installEnv, npm_config_yes: 'true' },
|
|
424
567
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
425
568
|
windowsHide: true,
|
|
426
569
|
// cross-spawn handles .cmd/.bat resolution itself — no shell
|