@pixelbyte-software/pixcode 1.53.18 → 1.53.19
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-D1Nd_kpU.js → index-CvIHrHIG.js} +171 -163
- package/dist/index.html +1 -1
- package/dist-server/server/index.js +246 -99
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/projects.js +96 -78
- package/dist-server/server/projects.js.map +1 -1
- package/dist-server/server/routes/git.js +43 -29
- package/dist-server/server/routes/git.js.map +1 -1
- package/package.json +2 -1
- package/scripts/smoke/backend-resource-bounds.mjs +91 -0
- package/server/index.js +262 -99
- package/server/projects.js +100 -76
- package/server/routes/git.js +42 -30
package/server/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import os from 'os';
|
|
|
8
8
|
import http from 'http';
|
|
9
9
|
import net from 'node:net';
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
|
+
import readline from 'node:readline';
|
|
11
12
|
import { createRequire } from 'node:module';
|
|
12
13
|
import { spawn } from 'child_process';
|
|
13
14
|
|
|
@@ -41,6 +42,7 @@ const DAEMON_COMMAND_CONTEXT = {
|
|
|
41
42
|
cliEntry: path.join(APP_ROOT, 'server', 'cli.js'),
|
|
42
43
|
nodeExecPath: process.execPath,
|
|
43
44
|
};
|
|
45
|
+
const JSONL_STREAM_LINE_MAX_CHARS = 1024 * 1024;
|
|
44
46
|
|
|
45
47
|
function resolveMonacoAssetsPath() {
|
|
46
48
|
const appParent = path.dirname(APP_ROOT);
|
|
@@ -625,9 +627,25 @@ const WATCHER_IGNORED_PATTERNS = [
|
|
|
625
627
|
'**/node_modules/**',
|
|
626
628
|
'**/.git/**',
|
|
627
629
|
'**/dist/**',
|
|
630
|
+
'**/dist-server/**',
|
|
628
631
|
'**/build/**',
|
|
632
|
+
'**/out/**',
|
|
633
|
+
'**/target/**',
|
|
634
|
+
'**/vendor/**',
|
|
635
|
+
'**/prebuilts/**',
|
|
636
|
+
'**/.repo/**',
|
|
637
|
+
'**/.gradle/**',
|
|
638
|
+
'**/.next/**',
|
|
639
|
+
'**/.nuxt/**',
|
|
640
|
+
'**/.svelte-kit/**',
|
|
641
|
+
'**/.turbo/**',
|
|
642
|
+
'**/.cache/**',
|
|
643
|
+
'**/.venv/**',
|
|
644
|
+
'**/venv/**',
|
|
645
|
+
'**/coverage/**',
|
|
629
646
|
'**/*.tmp',
|
|
630
647
|
'**/*.swp',
|
|
648
|
+
'**/*.log',
|
|
631
649
|
'**/.DS_Store'
|
|
632
650
|
];
|
|
633
651
|
// Debounce chokidar events before rescanning all provider project trees.
|
|
@@ -638,8 +656,10 @@ const WATCHER_IGNORED_PATTERNS = [
|
|
|
638
656
|
// a full chat reply into ~1 scan while still feeling responsive when
|
|
639
657
|
// the user flips to the projects list.
|
|
640
658
|
const WATCHER_DEBOUNCE_MS = Number.parseInt(process.env.PIXCODE_PROJECT_WATCH_DEBOUNCE_MS || '', 10) || 10000;
|
|
659
|
+
const PROVIDER_WATCHER_DEPTH = Number.parseInt(process.env.PIXCODE_PROVIDER_WATCH_DEPTH || '', 10) || 8;
|
|
641
660
|
let projectsWatchers = [];
|
|
642
661
|
let projectsWatcherDebounceTimer = null;
|
|
662
|
+
let pendingProjectsWatcherRefresh = null;
|
|
643
663
|
const connectedClients = new Set();
|
|
644
664
|
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
645
665
|
const STARTUP_INPUT_READY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -678,7 +698,20 @@ async function setupProjectsWatcher() {
|
|
|
678
698
|
);
|
|
679
699
|
projectsWatchers = [];
|
|
680
700
|
|
|
701
|
+
const sendToOpenClient = (client, payload) => {
|
|
702
|
+
if (client.readyState !== WebSocket.OPEN) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
if (client.bufferedAmount > SHELL_WS_BACKPRESSURE_LIMIT) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
client.send(JSON.stringify(payload));
|
|
709
|
+
return true;
|
|
710
|
+
};
|
|
711
|
+
|
|
681
712
|
const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
|
|
713
|
+
pendingProjectsWatcherRefresh = { eventType, filePath, provider, rootPath };
|
|
714
|
+
|
|
682
715
|
if (projectsWatcherDebounceTimer) {
|
|
683
716
|
clearTimeout(projectsWatcherDebounceTimer);
|
|
684
717
|
}
|
|
@@ -686,35 +719,37 @@ async function setupProjectsWatcher() {
|
|
|
686
719
|
projectsWatcherDebounceTimer = setTimeout(async () => {
|
|
687
720
|
// Prevent reentrant calls
|
|
688
721
|
if (isGetProjectsRunning) {
|
|
722
|
+
projectsWatcherDebounceTimer = setTimeout(() => {
|
|
723
|
+
const pending = pendingProjectsWatcherRefresh;
|
|
724
|
+
if (pending) {
|
|
725
|
+
debouncedUpdate(pending.eventType, pending.filePath, pending.provider, pending.rootPath);
|
|
726
|
+
}
|
|
727
|
+
}, WATCHER_DEBOUNCE_MS);
|
|
689
728
|
return;
|
|
690
729
|
}
|
|
691
730
|
|
|
692
731
|
try {
|
|
693
732
|
isGetProjectsRunning = true;
|
|
733
|
+
const pending = pendingProjectsWatcherRefresh || { eventType, filePath, provider, rootPath };
|
|
734
|
+
pendingProjectsWatcherRefresh = null;
|
|
694
735
|
|
|
695
|
-
if (eventType === 'addDir' || eventType === 'unlinkDir') {
|
|
736
|
+
if (pending.eventType === 'addDir' || pending.eventType === 'unlinkDir') {
|
|
696
737
|
clearProjectDirectoryCache();
|
|
697
738
|
}
|
|
698
739
|
|
|
699
|
-
// Get updated projects list
|
|
700
|
-
const updatedProjects = await getProjects(broadcastProgress);
|
|
701
|
-
|
|
702
740
|
const updatePayload = {
|
|
703
741
|
type: 'projects_updated',
|
|
704
742
|
timestamp: new Date().toISOString(),
|
|
705
|
-
changeType: eventType,
|
|
706
|
-
changedFile: path.relative(rootPath, filePath),
|
|
707
|
-
watchProvider: provider
|
|
743
|
+
changeType: pending.eventType,
|
|
744
|
+
changedFile: path.relative(pending.rootPath, pending.filePath),
|
|
745
|
+
watchProvider: pending.provider,
|
|
746
|
+
invalidated: true,
|
|
708
747
|
};
|
|
709
748
|
|
|
710
|
-
// Notify
|
|
749
|
+
// Notify clients that provider metadata changed. Avoid broadcasting the full
|
|
750
|
+
// project/session tree from watcher events; clients can pull when needed.
|
|
711
751
|
connectedClients.forEach(client => {
|
|
712
|
-
|
|
713
|
-
client.send(JSON.stringify({
|
|
714
|
-
...updatePayload,
|
|
715
|
-
projects: filterProjectsForUser(updatedProjects, client.user),
|
|
716
|
-
}));
|
|
717
|
-
}
|
|
752
|
+
sendToOpenClient(client, updatePayload);
|
|
718
753
|
});
|
|
719
754
|
|
|
720
755
|
} catch (error) {
|
|
@@ -737,7 +772,7 @@ async function setupProjectsWatcher() {
|
|
|
737
772
|
persistent: true,
|
|
738
773
|
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
739
774
|
followSymlinks: false,
|
|
740
|
-
depth:
|
|
775
|
+
depth: PROVIDER_WATCHER_DEPTH,
|
|
741
776
|
awaitWriteFinish: {
|
|
742
777
|
// Raised from (100, 50) to (500, 250). The old settings
|
|
743
778
|
// had chokidar polling every 50ms per in-flight file; an
|
|
@@ -782,6 +817,8 @@ async function setupProjectsWatcher() {
|
|
|
782
817
|
// `project_files_updated` events to subscribed clients only, letting the
|
|
783
818
|
// explorer refresh automatically without HTTP polling.
|
|
784
819
|
const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
|
|
820
|
+
const WORKSPACE_WATCHER_DEPTH = Number.parseInt(process.env.PIXCODE_WORKSPACE_WATCH_DEPTH || '', 10) || 5;
|
|
821
|
+
const WORKSPACE_WS_BACKPRESSURE_LIMIT = 2 * 1024 * 1024;
|
|
785
822
|
const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 128 * 1024;
|
|
786
823
|
const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 80;
|
|
787
824
|
const WORKSPACE_DIFF_SNAPSHOT_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
@@ -1014,7 +1051,7 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
1014
1051
|
timestamp: new Date().toISOString(),
|
|
1015
1052
|
});
|
|
1016
1053
|
entry.subscribers.forEach((client) => {
|
|
1017
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
1054
|
+
if (client.readyState === WebSocket.OPEN && client.bufferedAmount <= WORKSPACE_WS_BACKPRESSURE_LIMIT) {
|
|
1018
1055
|
client.send(message);
|
|
1019
1056
|
}
|
|
1020
1057
|
});
|
|
@@ -1028,7 +1065,7 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
1028
1065
|
persistent: true,
|
|
1029
1066
|
ignoreInitial: true,
|
|
1030
1067
|
followSymlinks: false,
|
|
1031
|
-
depth:
|
|
1068
|
+
depth: WORKSPACE_WATCHER_DEPTH,
|
|
1032
1069
|
awaitWriteFinish: {
|
|
1033
1070
|
stabilityThreshold: 500,
|
|
1034
1071
|
pollInterval: 250
|
|
@@ -1085,6 +1122,36 @@ const PTY_SESSION_BUFFER_MAX_BYTES = Number.parseInt(process.env.PIXCODE_PTY_BUF
|
|
|
1085
1122
|
const SHELL_INPUT_CHUNK_CHARS = 4096;
|
|
1086
1123
|
const SHELL_PENDING_INPUT_MAX_BYTES = Number.parseInt(process.env.PIXCODE_SHELL_PENDING_INPUT_MAX_BYTES || '', 10) || (16 * 1024 * 1024);
|
|
1087
1124
|
const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
|
|
1125
|
+
const FILE_TREE_MAX_ITEMS = Number.parseInt(process.env.PIXCODE_FILE_TREE_MAX_ITEMS || '', 10) || 5000;
|
|
1126
|
+
const FILE_TREE_MAX_DIRECTORIES = Number.parseInt(process.env.PIXCODE_FILE_TREE_MAX_DIRECTORIES || '', 10) || 1200;
|
|
1127
|
+
const FILE_TREE_SCAN_MAX_MS = Number.parseInt(process.env.PIXCODE_FILE_TREE_SCAN_MAX_MS || '', 10) || 4000;
|
|
1128
|
+
const FILE_TREE_MAX_ENTRIES_PER_DIRECTORY = Number.parseInt(process.env.PIXCODE_FILE_TREE_MAX_ENTRIES_PER_DIRECTORY || '', 10) || 1500;
|
|
1129
|
+
const FILE_TREE_EXCLUDED_ENTRY_NAMES = new Set([
|
|
1130
|
+
'.cache',
|
|
1131
|
+
'.git',
|
|
1132
|
+
'.gradle',
|
|
1133
|
+
'.hg',
|
|
1134
|
+
'.next',
|
|
1135
|
+
'.nuxt',
|
|
1136
|
+
'.pnpm-store',
|
|
1137
|
+
'.repo',
|
|
1138
|
+
'.svn',
|
|
1139
|
+
'.svelte-kit',
|
|
1140
|
+
'.turbo',
|
|
1141
|
+
'.venv',
|
|
1142
|
+
'build',
|
|
1143
|
+
'coverage',
|
|
1144
|
+
'DerivedData',
|
|
1145
|
+
'dist',
|
|
1146
|
+
'dist-server',
|
|
1147
|
+
'node_modules',
|
|
1148
|
+
'out',
|
|
1149
|
+
'Pods',
|
|
1150
|
+
'prebuilts',
|
|
1151
|
+
'target',
|
|
1152
|
+
'vendor',
|
|
1153
|
+
'venv',
|
|
1154
|
+
]);
|
|
1088
1155
|
import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
|
|
1089
1156
|
|
|
1090
1157
|
function terminatePtySession(sessionKey, session, reason) {
|
|
@@ -4018,11 +4085,22 @@ function handleShellConnection(ws, request) {
|
|
|
4018
4085
|
if (existingSession.buffer && existingSession.buffer.length > 0) {
|
|
4019
4086
|
console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
|
|
4020
4087
|
existingSession.buffer.forEach(bufferedData => {
|
|
4088
|
+
if (ws.bufferedAmount > SHELL_WS_BACKPRESSURE_LIMIT) {
|
|
4089
|
+
existingSession.droppedOutputBytes = (existingSession.droppedOutputBytes || 0) + Buffer.byteLength(String(bufferedData || ''), 'utf8');
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4021
4092
|
ws.send(JSON.stringify({
|
|
4022
4093
|
type: 'output',
|
|
4023
4094
|
data: bufferedData
|
|
4024
4095
|
}));
|
|
4025
4096
|
});
|
|
4097
|
+
if ((existingSession.droppedOutputBytes || 0) > 0 && ws.bufferedAmount <= SHELL_WS_BACKPRESSURE_LIMIT) {
|
|
4098
|
+
ws.send(JSON.stringify({
|
|
4099
|
+
type: 'output',
|
|
4100
|
+
data: `\r\n\x1b[33m[Pixcode] ${existingSession.droppedOutputBytes} bytes of buffered terminal output were skipped because the browser connection was backpressured.\x1b[0m\r\n`,
|
|
4101
|
+
}));
|
|
4102
|
+
existingSession.droppedOutputBytes = 0;
|
|
4103
|
+
}
|
|
4026
4104
|
}
|
|
4027
4105
|
|
|
4028
4106
|
existingSession.ws = ws;
|
|
@@ -4531,22 +4609,46 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
4531
4609
|
if (provider === 'codex') {
|
|
4532
4610
|
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
4533
4611
|
|
|
4534
|
-
// Find the session file by searching for the session ID
|
|
4612
|
+
// Find the session file by searching for the session ID without
|
|
4613
|
+
// materializing every directory entry in memory.
|
|
4535
4614
|
const findSessionFile = async (dir) => {
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4615
|
+
const pendingDirs = [dir];
|
|
4616
|
+
let visitedDirs = 0;
|
|
4617
|
+
let visitedFiles = 0;
|
|
4618
|
+
const maxDirs = 2500;
|
|
4619
|
+
const maxFiles = 10000;
|
|
4620
|
+
|
|
4621
|
+
while (pendingDirs.length > 0 && visitedDirs < maxDirs && visitedFiles < maxFiles) {
|
|
4622
|
+
const currentDir = pendingDirs.pop();
|
|
4623
|
+
visitedDirs += 1;
|
|
4624
|
+
|
|
4625
|
+
try {
|
|
4626
|
+
const dirHandle = await fsPromises.opendir(currentDir);
|
|
4627
|
+
try {
|
|
4628
|
+
for await (const entry of dirHandle) {
|
|
4629
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
4630
|
+
if (entry.isDirectory()) {
|
|
4631
|
+
if (visitedDirs + pendingDirs.length < maxDirs) {
|
|
4632
|
+
pendingDirs.push(fullPath);
|
|
4633
|
+
}
|
|
4634
|
+
} else {
|
|
4635
|
+
visitedFiles += 1;
|
|
4636
|
+
if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
|
|
4637
|
+
return fullPath;
|
|
4638
|
+
}
|
|
4639
|
+
if (visitedFiles >= maxFiles) {
|
|
4640
|
+
break;
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
} finally {
|
|
4645
|
+
await dirHandle.close().catch(() => undefined);
|
|
4545
4646
|
}
|
|
4647
|
+
} catch (error) {
|
|
4648
|
+
// Skip directories we can't read
|
|
4546
4649
|
}
|
|
4547
|
-
} catch (error) {
|
|
4548
|
-
// Skip directories we can't read
|
|
4549
4650
|
}
|
|
4651
|
+
|
|
4550
4652
|
return null;
|
|
4551
4653
|
};
|
|
4552
4654
|
|
|
@@ -4556,24 +4658,31 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
4556
4658
|
return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
|
|
4557
4659
|
}
|
|
4558
4660
|
|
|
4559
|
-
//
|
|
4560
|
-
|
|
4661
|
+
// Stream and parse the Codex JSONL file. Keeping only the latest
|
|
4662
|
+
// token_count event avoids loading very large session logs.
|
|
4561
4663
|
try {
|
|
4562
|
-
|
|
4664
|
+
await fsPromises.access(sessionFilePath);
|
|
4563
4665
|
} catch (error) {
|
|
4564
4666
|
if (error.code === 'ENOENT') {
|
|
4565
4667
|
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
4566
4668
|
}
|
|
4567
4669
|
throw error;
|
|
4568
4670
|
}
|
|
4569
|
-
const
|
|
4671
|
+
const fileStream = fs.createReadStream(sessionFilePath, { encoding: 'utf8' });
|
|
4672
|
+
const rl = readline.createInterface({
|
|
4673
|
+
input: fileStream,
|
|
4674
|
+
crlfDelay: Infinity,
|
|
4675
|
+
});
|
|
4570
4676
|
let totalTokens = 0;
|
|
4571
4677
|
let contextWindow = 200000; // Default for Codex/OpenAI
|
|
4572
4678
|
|
|
4573
|
-
|
|
4574
|
-
|
|
4679
|
+
for await (const line of rl) {
|
|
4680
|
+
if (!line.trim() || line.length > JSONL_STREAM_LINE_MAX_CHARS) {
|
|
4681
|
+
continue;
|
|
4682
|
+
}
|
|
4683
|
+
|
|
4575
4684
|
try {
|
|
4576
|
-
const entry = JSON.parse(
|
|
4685
|
+
const entry = JSON.parse(line);
|
|
4577
4686
|
|
|
4578
4687
|
// Codex stores token info in event_msg with type: "token_count"
|
|
4579
4688
|
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
@@ -4584,7 +4693,6 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
4584
4693
|
if (tokenInfo.model_context_window) {
|
|
4585
4694
|
contextWindow = tokenInfo.model_context_window;
|
|
4586
4695
|
}
|
|
4587
|
-
break; // Stop after finding the latest token count
|
|
4588
4696
|
}
|
|
4589
4697
|
} catch (parseError) {
|
|
4590
4698
|
// Skip lines that can't be parsed
|
|
@@ -4622,17 +4730,21 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
4622
4730
|
return res.status(400).json({ error: 'Invalid path' });
|
|
4623
4731
|
}
|
|
4624
4732
|
|
|
4625
|
-
//
|
|
4626
|
-
|
|
4733
|
+
// Stream and parse the JSONL file. Keeping only the latest assistant
|
|
4734
|
+
// usage object avoids loading very large session logs.
|
|
4627
4735
|
try {
|
|
4628
|
-
|
|
4736
|
+
await fsPromises.access(jsonlPath);
|
|
4629
4737
|
} catch (error) {
|
|
4630
4738
|
if (error.code === 'ENOENT') {
|
|
4631
4739
|
return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
|
|
4632
4740
|
}
|
|
4633
4741
|
throw error; // Re-throw other errors to be caught by outer try-catch
|
|
4634
4742
|
}
|
|
4635
|
-
const
|
|
4743
|
+
const fileStream = fs.createReadStream(jsonlPath, { encoding: 'utf8' });
|
|
4744
|
+
const rl = readline.createInterface({
|
|
4745
|
+
input: fileStream,
|
|
4746
|
+
crlfDelay: Infinity,
|
|
4747
|
+
});
|
|
4636
4748
|
|
|
4637
4749
|
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
4638
4750
|
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
|
@@ -4640,10 +4752,13 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
4640
4752
|
let cacheCreationTokens = 0;
|
|
4641
4753
|
let cacheReadTokens = 0;
|
|
4642
4754
|
|
|
4643
|
-
|
|
4644
|
-
|
|
4755
|
+
for await (const line of rl) {
|
|
4756
|
+
if (!line.trim() || line.length > JSONL_STREAM_LINE_MAX_CHARS) {
|
|
4757
|
+
continue;
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4645
4760
|
try {
|
|
4646
|
-
const entry = JSON.parse(
|
|
4761
|
+
const entry = JSON.parse(line);
|
|
4647
4762
|
|
|
4648
4763
|
// Only count assistant messages which have usage data
|
|
4649
4764
|
if (entry.type === 'assistant' && entry.message?.usage) {
|
|
@@ -4653,8 +4768,6 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
4653
4768
|
inputTokens = usage.input_tokens || 0;
|
|
4654
4769
|
cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
4655
4770
|
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
4656
|
-
|
|
4657
|
-
break; // Stop after finding the latest assistant message
|
|
4658
4771
|
}
|
|
4659
4772
|
} catch (parseError) {
|
|
4660
4773
|
// Skip lines that can't be parsed
|
|
@@ -4772,74 +4885,124 @@ function permToRwx(perm) {
|
|
|
4772
4885
|
return r + w + x;
|
|
4773
4886
|
}
|
|
4774
4887
|
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4888
|
+
function createFileTreeScanContext() {
|
|
4889
|
+
return {
|
|
4890
|
+
startedAt: Date.now(),
|
|
4891
|
+
itemCount: 0,
|
|
4892
|
+
directoryCount: 0,
|
|
4893
|
+
limitReached: false,
|
|
4894
|
+
};
|
|
4895
|
+
}
|
|
4778
4896
|
|
|
4779
|
-
|
|
4780
|
-
|
|
4897
|
+
function hasFileTreeBudget(context) {
|
|
4898
|
+
if (!context || context.limitReached) {
|
|
4899
|
+
return false;
|
|
4900
|
+
}
|
|
4781
4901
|
|
|
4782
|
-
|
|
4783
|
-
|
|
4902
|
+
if (context.itemCount >= FILE_TREE_MAX_ITEMS || context.directoryCount >= FILE_TREE_MAX_DIRECTORIES) {
|
|
4903
|
+
context.limitReached = true;
|
|
4904
|
+
return false;
|
|
4905
|
+
}
|
|
4784
4906
|
|
|
4907
|
+
if (Date.now() - context.startedAt > FILE_TREE_SCAN_MAX_MS) {
|
|
4908
|
+
context.limitReached = true;
|
|
4909
|
+
return false;
|
|
4910
|
+
}
|
|
4785
4911
|
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
entry.name === 'dist' ||
|
|
4789
|
-
entry.name === 'build' ||
|
|
4790
|
-
entry.name === '.git' ||
|
|
4791
|
-
entry.name === '.svn' ||
|
|
4792
|
-
entry.name === '.hg') continue;
|
|
4912
|
+
return true;
|
|
4913
|
+
}
|
|
4793
4914
|
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
type: entry.isDirectory() ? 'directory' : 'file'
|
|
4799
|
-
};
|
|
4915
|
+
function shouldSkipFileTreeEntry(entryName, showHidden) {
|
|
4916
|
+
if (!showHidden && entryName.startsWith('.')) {
|
|
4917
|
+
return true;
|
|
4918
|
+
}
|
|
4800
4919
|
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
item.modified = stats.mtime.toISOString();
|
|
4806
|
-
|
|
4807
|
-
// Convert permissions to rwx format
|
|
4808
|
-
const mode = stats.mode;
|
|
4809
|
-
const ownerPerm = (mode >> 6) & 7;
|
|
4810
|
-
const groupPerm = (mode >> 3) & 7;
|
|
4811
|
-
const otherPerm = mode & 7;
|
|
4812
|
-
item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
|
|
4813
|
-
item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
|
|
4814
|
-
} catch (statError) {
|
|
4815
|
-
// If stat fails, provide default values
|
|
4816
|
-
item.size = 0;
|
|
4817
|
-
item.modified = null;
|
|
4818
|
-
item.permissions = '000';
|
|
4819
|
-
item.permissionsRwx = '---------';
|
|
4820
|
-
}
|
|
4920
|
+
return FILE_TREE_EXCLUDED_ENTRY_NAMES.has(entryName)
|
|
4921
|
+
|| entryName.endsWith('.log')
|
|
4922
|
+
|| entryName === '.DS_Store';
|
|
4923
|
+
}
|
|
4821
4924
|
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4925
|
+
async function readDirectoryEntriesBounded(dirPath, context) {
|
|
4926
|
+
const entries = [];
|
|
4927
|
+
|
|
4928
|
+
try {
|
|
4929
|
+
const dir = await fsPromises.opendir(dirPath);
|
|
4930
|
+
try {
|
|
4931
|
+
for await (const entry of dir) {
|
|
4932
|
+
if (!hasFileTreeBudget(context) || entries.length >= FILE_TREE_MAX_ENTRIES_PER_DIRECTORY) {
|
|
4933
|
+
context.limitReached = true;
|
|
4934
|
+
break;
|
|
4831
4935
|
}
|
|
4832
|
-
}
|
|
4833
4936
|
|
|
4834
|
-
|
|
4937
|
+
entries.push(entry);
|
|
4938
|
+
}
|
|
4939
|
+
} finally {
|
|
4940
|
+
await dir.close().catch(() => undefined);
|
|
4835
4941
|
}
|
|
4836
4942
|
} catch (error) {
|
|
4837
|
-
|
|
4838
|
-
if (error.code !== 'EACCES' && error.code !== 'EPERM') {
|
|
4943
|
+
if (error.code !== 'EACCES' && error.code !== 'EPERM' && error.code !== 'ENOENT') {
|
|
4839
4944
|
console.error('Error reading directory:', error);
|
|
4840
4945
|
}
|
|
4841
4946
|
}
|
|
4842
4947
|
|
|
4948
|
+
return entries;
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = true, scanContext = null) {
|
|
4952
|
+
const context = scanContext || createFileTreeScanContext();
|
|
4953
|
+
const items = [];
|
|
4954
|
+
|
|
4955
|
+
if (!hasFileTreeBudget(context)) {
|
|
4956
|
+
return items;
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
context.directoryCount += 1;
|
|
4960
|
+
const entries = await readDirectoryEntriesBounded(dirPath, context);
|
|
4961
|
+
|
|
4962
|
+
for (const entry of entries) {
|
|
4963
|
+
if (!hasFileTreeBudget(context) || shouldSkipFileTreeEntry(entry.name, showHidden)) {
|
|
4964
|
+
continue;
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
const itemPath = path.join(dirPath, entry.name);
|
|
4968
|
+
const item = {
|
|
4969
|
+
name: entry.name,
|
|
4970
|
+
path: itemPath,
|
|
4971
|
+
type: entry.isDirectory() ? 'directory' : 'file'
|
|
4972
|
+
};
|
|
4973
|
+
|
|
4974
|
+
try {
|
|
4975
|
+
const stats = await fsPromises.stat(itemPath);
|
|
4976
|
+
item.size = stats.size;
|
|
4977
|
+
item.modified = stats.mtime.toISOString();
|
|
4978
|
+
|
|
4979
|
+
const mode = stats.mode;
|
|
4980
|
+
const ownerPerm = (mode >> 6) & 7;
|
|
4981
|
+
const groupPerm = (mode >> 3) & 7;
|
|
4982
|
+
const otherPerm = mode & 7;
|
|
4983
|
+
item.permissions = ((mode >> 6) & 7).toString() + ((mode >> 3) & 7).toString() + (mode & 7).toString();
|
|
4984
|
+
item.permissionsRwx = permToRwx(ownerPerm) + permToRwx(groupPerm) + permToRwx(otherPerm);
|
|
4985
|
+
} catch {
|
|
4986
|
+
item.size = 0;
|
|
4987
|
+
item.modified = null;
|
|
4988
|
+
item.permissions = '000';
|
|
4989
|
+
item.permissionsRwx = '---------';
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
context.itemCount += 1;
|
|
4993
|
+
|
|
4994
|
+
if (entry.isDirectory() && currentDepth < maxDepth && hasFileTreeBudget(context)) {
|
|
4995
|
+
try {
|
|
4996
|
+
await fsPromises.access(item.path, fs.constants.R_OK);
|
|
4997
|
+
item.children = await getFileTree(item.path, maxDepth, currentDepth + 1, showHidden, context);
|
|
4998
|
+
} catch {
|
|
4999
|
+
item.children = [];
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
items.push(item);
|
|
5004
|
+
}
|
|
5005
|
+
|
|
4843
5006
|
return items.sort((a, b) => {
|
|
4844
5007
|
if (a.type !== b.type) {
|
|
4845
5008
|
return a.type === 'directory' ? -1 : 1;
|