@pixelbyte-software/pixcode 1.53.1 → 1.53.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.
- package/dist/assets/{index-BO0lz8ZD.js → index-CZZiJpxs.js} +196 -172
- package/dist/assets/index-FhOsv2Yy.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +201 -9
- package/dist-server/server/index.js.map +1 -1
- package/package.json +1 -1
- package/server/index.js +225 -9
- package/dist/assets/index-DnjqWG8B.css +0 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelbyte-software/pixcode",
|
|
3
|
-
"version": "1.53.
|
|
3
|
+
"version": "1.53.3",
|
|
4
4
|
"description": "Self-hosted AI coding agent control room for Claude Code, Cursor CLI, OpenAI Codex, Gemini CLI, Qwen Code, and OpenCode with chat, files, shell, Git, orchestration, API keys, Telegram, MCP, plugins, themes, and desktop/server deployment.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist-server/server/index.js",
|
package/server/index.js
CHANGED
|
@@ -273,6 +273,59 @@ function getActiveUpdateJob() {
|
|
|
273
273
|
return null;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function countByProvider(items) {
|
|
277
|
+
return items.reduce((counts, item) => {
|
|
278
|
+
const provider = item?.provider || 'unknown';
|
|
279
|
+
counts[provider] = (counts[provider] || 0) + 1;
|
|
280
|
+
return counts;
|
|
281
|
+
}, {});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getActiveProviderWorkSummary() {
|
|
285
|
+
const sessions = [
|
|
286
|
+
...getActiveClaudeSDKSessions().map((id) => ({ id, provider: 'claude' })),
|
|
287
|
+
...getActiveCursorSessions().map((session) => ({ ...session, provider: 'cursor' })),
|
|
288
|
+
...getActiveCodexSessions().map((session) => ({ ...session, provider: 'codex' })),
|
|
289
|
+
...getActiveGeminiSessions().map((session) => ({ ...session, provider: 'gemini' })),
|
|
290
|
+
...getActiveQwenSessions().map((session) => ({ ...session, provider: 'qwen' })),
|
|
291
|
+
...getActiveOpencodeSessions().map((session) => ({ ...session, provider: 'opencode' })),
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
total: sessions.length,
|
|
296
|
+
byProvider: countByProvider(sessions),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function getActivePtyWorkSummary() {
|
|
301
|
+
const activePtys = Array.from(ptySessionsMap.values())
|
|
302
|
+
.filter((session) => session?.pty && session.lifecycleState === 'running')
|
|
303
|
+
.map((session) => ({
|
|
304
|
+
provider: session.isPlainShell ? 'plain-shell' : (session.provider || 'unknown'),
|
|
305
|
+
connected: Boolean(session.ws && session.ws.readyState === WebSocket.OPEN),
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
total: activePtys.length,
|
|
310
|
+
connected: activePtys.filter((session) => session.connected).length,
|
|
311
|
+
detached: activePtys.filter((session) => !session.connected).length,
|
|
312
|
+
byProvider: countByProvider(activePtys),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function getActiveWorkSummary() {
|
|
317
|
+
const pty = getActivePtyWorkSummary();
|
|
318
|
+
const agents = getActiveProviderWorkSummary();
|
|
319
|
+
const total = pty.total + agents.total;
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
hasActiveWork: total > 0,
|
|
323
|
+
total,
|
|
324
|
+
pty,
|
|
325
|
+
agents,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
276
329
|
async function readLatestPixcodePackageMetadata() {
|
|
277
330
|
const registryRes = await fetch('https://registry.npmjs.org/@pixelbyte-software/pixcode');
|
|
278
331
|
if (!registryRes.ok) throw new Error(`Registry returned HTTP ${registryRes.status}`);
|
|
@@ -375,8 +428,10 @@ async function runCommandUpdateJob(job, updateCommand, updateCwd) {
|
|
|
375
428
|
cwd: updateCwd,
|
|
376
429
|
env: process.env,
|
|
377
430
|
shell: true,
|
|
431
|
+
detached: true,
|
|
378
432
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
379
433
|
});
|
|
434
|
+
try { child.unref(); } catch { /* noop */ }
|
|
380
435
|
child.stdout?.on('data', (data) => appendUpdateJobLog(job, 'stdout', data.toString()));
|
|
381
436
|
child.stderr?.on('data', (data) => appendUpdateJobLog(job, 'stderr', data.toString()));
|
|
382
437
|
child.on('error', reject);
|
|
@@ -695,7 +750,108 @@ async function setupProjectsWatcher() {
|
|
|
695
750
|
// `project_files_updated` events to subscribed clients only, letting the
|
|
696
751
|
// explorer refresh automatically without HTTP polling.
|
|
697
752
|
const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
|
|
698
|
-
const
|
|
753
|
+
const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 512 * 1024;
|
|
754
|
+
const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 800;
|
|
755
|
+
const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath, fileSnapshots }
|
|
756
|
+
|
|
757
|
+
function runWorkspaceCommand(command, args, cwd) {
|
|
758
|
+
return new Promise((resolve, reject) => {
|
|
759
|
+
const child = spawn(command, args, {
|
|
760
|
+
cwd,
|
|
761
|
+
shell: false,
|
|
762
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
763
|
+
});
|
|
764
|
+
let stdout = '';
|
|
765
|
+
let stderr = '';
|
|
766
|
+
|
|
767
|
+
child.stdout?.on('data', (chunk) => {
|
|
768
|
+
stdout += chunk.toString();
|
|
769
|
+
});
|
|
770
|
+
child.stderr?.on('data', (chunk) => {
|
|
771
|
+
stderr += chunk.toString();
|
|
772
|
+
});
|
|
773
|
+
child.on('error', reject);
|
|
774
|
+
child.on('close', (code) => {
|
|
775
|
+
if (code === 0) {
|
|
776
|
+
resolve({ stdout, stderr });
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
reject(new Error(stderr || `${command} exited with code ${code}`));
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function parseGitPorcelainZ(output) {
|
|
786
|
+
const entries = [];
|
|
787
|
+
const parts = String(output || '').split('\0').filter(Boolean);
|
|
788
|
+
|
|
789
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
790
|
+
const entry = parts[index];
|
|
791
|
+
if (entry.length < 4) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const status = entry.slice(0, 2);
|
|
796
|
+
const filePath = entry.slice(3).replace(/\\/g, '/');
|
|
797
|
+
if (filePath) {
|
|
798
|
+
entries.push(filePath);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (status[0] === 'R' || status[0] === 'C') {
|
|
802
|
+
index += 1;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return entries;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function normalizeWorkspaceRelativePath(rootPath, filePath) {
|
|
810
|
+
const relativePath = path.relative(rootPath, filePath).replace(/\\/g, '/');
|
|
811
|
+
if (!relativePath || relativePath.startsWith('../') || relativePath === '..' || path.isAbsolute(relativePath)) {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return relativePath;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async function readWorkspaceTextSnapshot(filePath) {
|
|
819
|
+
try {
|
|
820
|
+
const stats = await fsPromises.stat(filePath);
|
|
821
|
+
if (!stats.isFile() || stats.size > WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const buffer = await fsPromises.readFile(filePath);
|
|
826
|
+
if (buffer.includes(0)) {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return buffer.toString('utf8');
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (error?.code === 'ENOENT') {
|
|
833
|
+
return '';
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
|
|
841
|
+
try {
|
|
842
|
+
const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
|
|
843
|
+
const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
|
|
844
|
+
|
|
845
|
+
await Promise.all(changedPaths.map(async (relativePath) => {
|
|
846
|
+
const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
|
|
847
|
+
if (typeof content === 'string') {
|
|
848
|
+
fileSnapshots.set(relativePath, content);
|
|
849
|
+
}
|
|
850
|
+
}));
|
|
851
|
+
} catch {
|
|
852
|
+
// Non-git workspaces still get event-time snapshots after the first change.
|
|
853
|
+
}
|
|
854
|
+
}
|
|
699
855
|
|
|
700
856
|
async function subscribeToWorkspace(ws, projectName) {
|
|
701
857
|
if (!projectName || typeof projectName !== 'string') return;
|
|
@@ -722,11 +878,48 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
722
878
|
debounceTimer: null,
|
|
723
879
|
pendingEvent: null,
|
|
724
880
|
rootPath,
|
|
881
|
+
fileSnapshots: new Map(),
|
|
725
882
|
};
|
|
726
883
|
workspaceWatchers.set(projectName, entry);
|
|
884
|
+
await initializeWorkspaceDirtySnapshots(rootPath, entry.fileSnapshots);
|
|
885
|
+
|
|
886
|
+
const broadcastFileUpdate = async (eventType, filePath) => {
|
|
887
|
+
const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
|
|
888
|
+
const previousContent = relativePath ? entry.fileSnapshots.get(relativePath) : undefined;
|
|
889
|
+
let currentContent;
|
|
890
|
+
|
|
891
|
+
if (relativePath && !eventType.endsWith('Dir')) {
|
|
892
|
+
if (eventType === 'unlink') {
|
|
893
|
+
currentContent = '';
|
|
894
|
+
entry.fileSnapshots.delete(relativePath);
|
|
895
|
+
} else {
|
|
896
|
+
const snapshot = await readWorkspaceTextSnapshot(filePath);
|
|
897
|
+
if (typeof snapshot === 'string') {
|
|
898
|
+
currentContent = snapshot;
|
|
899
|
+
entry.fileSnapshots.set(relativePath, snapshot);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const nextEvent = {
|
|
905
|
+
eventType,
|
|
906
|
+
filePath,
|
|
907
|
+
changedFile: relativePath,
|
|
908
|
+
oldContent: typeof previousContent === 'string'
|
|
909
|
+
? previousContent
|
|
910
|
+
: eventType === 'add'
|
|
911
|
+
? ''
|
|
912
|
+
: undefined,
|
|
913
|
+
currentContent,
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
entry.pendingEvent = entry.pendingEvent?.changedFile === relativePath
|
|
917
|
+
? {
|
|
918
|
+
...nextEvent,
|
|
919
|
+
oldContent: entry.pendingEvent.oldContent ?? nextEvent.oldContent,
|
|
920
|
+
}
|
|
921
|
+
: nextEvent;
|
|
727
922
|
|
|
728
|
-
const broadcastFileUpdate = (eventType, filePath) => {
|
|
729
|
-
entry.pendingEvent = { eventType, filePath };
|
|
730
923
|
if (entry.debounceTimer) {
|
|
731
924
|
clearTimeout(entry.debounceTimer);
|
|
732
925
|
}
|
|
@@ -734,11 +927,23 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
734
927
|
entry.debounceTimer = null;
|
|
735
928
|
const pending = entry.pendingEvent || {};
|
|
736
929
|
entry.pendingEvent = null;
|
|
930
|
+
const hasSnapshotDiff = (
|
|
931
|
+
typeof pending.oldContent === 'string'
|
|
932
|
+
&& typeof pending.currentContent === 'string'
|
|
933
|
+
&& pending.oldContent !== pending.currentContent
|
|
934
|
+
);
|
|
737
935
|
const message = JSON.stringify({
|
|
738
936
|
type: 'project_files_updated',
|
|
739
937
|
projectName,
|
|
740
938
|
changeType: pending.eventType || 'change',
|
|
741
|
-
changedFile: pending.filePath ? path.relative(rootPath, pending.filePath) : null,
|
|
939
|
+
changedFile: pending.changedFile ?? (pending.filePath ? path.relative(rootPath, pending.filePath) : null),
|
|
940
|
+
...(hasSnapshotDiff
|
|
941
|
+
? {
|
|
942
|
+
oldContent: pending.oldContent,
|
|
943
|
+
currentContent: pending.currentContent,
|
|
944
|
+
diffSource: 'workspace-snapshot',
|
|
945
|
+
}
|
|
946
|
+
: {}),
|
|
742
947
|
timestamp: new Date().toISOString(),
|
|
743
948
|
});
|
|
744
949
|
entry.subscribers.forEach((client) => {
|
|
@@ -764,11 +969,11 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
764
969
|
});
|
|
765
970
|
|
|
766
971
|
watcher
|
|
767
|
-
.on('add', (filePath) => broadcastFileUpdate('add', filePath))
|
|
768
|
-
.on('change', (filePath) => broadcastFileUpdate('change', filePath))
|
|
769
|
-
.on('unlink', (filePath) => broadcastFileUpdate('unlink', filePath))
|
|
770
|
-
.on('addDir', (dirPath) => broadcastFileUpdate('addDir', dirPath))
|
|
771
|
-
.on('unlinkDir', (dirPath) => broadcastFileUpdate('unlinkDir', dirPath))
|
|
972
|
+
.on('add', (filePath) => void broadcastFileUpdate('add', filePath))
|
|
973
|
+
.on('change', (filePath) => void broadcastFileUpdate('change', filePath))
|
|
974
|
+
.on('unlink', (filePath) => void broadcastFileUpdate('unlink', filePath))
|
|
975
|
+
.on('addDir', (dirPath) => void broadcastFileUpdate('addDir', dirPath))
|
|
976
|
+
.on('unlinkDir', (dirPath) => void broadcastFileUpdate('unlinkDir', dirPath))
|
|
772
977
|
.on('error', (error) => {
|
|
773
978
|
console.error(`[ERROR] Workspace watcher error for ${projectName}:`, error);
|
|
774
979
|
});
|
|
@@ -1485,6 +1690,7 @@ app.get('/api/system/update-state', authenticateToken, (req, res) => {
|
|
|
1485
1690
|
success: true,
|
|
1486
1691
|
state: readSystemUpdateState(),
|
|
1487
1692
|
activeJob: snapshotUpdateJob(getActiveUpdateJob()),
|
|
1693
|
+
activeWork: getActiveWorkSummary(),
|
|
1488
1694
|
currentVersion: SERVER_VERSION,
|
|
1489
1695
|
installMode,
|
|
1490
1696
|
capabilities: {
|
|
@@ -1885,6 +2091,16 @@ app.post('/api/system/update', authenticateToken, requireAdmin, requireApiScope(
|
|
|
1885
2091
|
// (systemd/pm2/daemon manager) can bring the server back on the new code.
|
|
1886
2092
|
// Foreground installs without a wrapper will simply stop; the UI reports this.
|
|
1887
2093
|
app.post('/api/system/restart', authenticateToken, requireAdmin, requireApiScope('system:restart'), (req, res) => {
|
|
2094
|
+
const forceRestart = req.body?.force === true || req.query.force === 'true';
|
|
2095
|
+
const activeWork = getActiveWorkSummary();
|
|
2096
|
+
if (!forceRestart && activeWork.hasActiveWork) {
|
|
2097
|
+
return res.status(409).json({
|
|
2098
|
+
success: false,
|
|
2099
|
+
error: 'Active terminal or agent sessions are running. Confirm restart to interrupt them.',
|
|
2100
|
+
activeWork,
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1888
2104
|
res.json({
|
|
1889
2105
|
success: true,
|
|
1890
2106
|
version: SERVER_VERSION,
|