@pixelbyte-software/pixcode 1.53.28 → 1.54.1
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-BBDYzJ7B.js → index-CLO8YURv.js} +148 -148
- package/dist/index.html +1 -1
- package/dist-server/server/claude-sdk.js +6 -0
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cli.js +67 -0
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/index.js +278 -54
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/account-lockout.js +101 -0
- package/dist-server/server/middleware/account-lockout.js.map +1 -0
- package/dist-server/server/middleware/auth.js +71 -9
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/middleware/rate-limiter.js +62 -0
- package/dist-server/server/middleware/rate-limiter.js.map +1 -0
- package/dist-server/server/routes/agent.js +3 -4
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/auth.js +75 -13
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/codex.js +1 -1
- package/dist-server/server/routes/codex.js.map +1 -1
- package/dist-server/server/routes/diagnostics.js +6 -3
- package/dist-server/server/routes/diagnostics.js.map +1 -1
- package/dist-server/server/routes/gemini.js +1 -1
- package/dist-server/server/routes/gemini.js.map +1 -1
- package/dist-server/server/routes/git.js +9 -9
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/live-view.js +2 -2
- package/dist-server/server/routes/live-view.js.map +1 -1
- package/dist-server/server/routes/plugins.js +12 -12
- package/dist-server/server/routes/plugins.js.map +1 -1
- package/dist-server/server/routes/projects.js +2 -2
- package/dist-server/server/routes/projects.js.map +1 -1
- package/dist-server/server/routes/qwen.js +1 -1
- package/dist-server/server/routes/qwen.js.map +1 -1
- package/dist-server/server/routes/remote.js +1 -1
- package/dist-server/server/routes/remote.js.map +1 -1
- package/dist-server/server/routes/webhooks.js +1 -1
- package/dist-server/server/routes/webhooks.js.map +1 -1
- package/dist-server/server/services/startup-update.js +31 -6
- package/dist-server/server/services/startup-update.js.map +1 -1
- package/dist-server/server/services/telegram/bot.js +6 -4
- package/dist-server/server/services/telegram/bot.js.map +1 -1
- package/dist-server/server/setup-wizard.js +246 -0
- package/dist-server/server/setup-wizard.js.map +1 -0
- package/dist-server/server/utils/plugin-loader.js +3 -0
- package/dist-server/server/utils/plugin-loader.js.map +1 -1
- package/dist-server/server/utils/port-access.js +25 -8
- package/dist-server/server/utils/port-access.js.map +1 -1
- package/dist-server/server/utils/security-log.js +89 -0
- package/dist-server/server/utils/security-log.js.map +1 -0
- package/package.json +1 -1
- package/server/claude-sdk.js +7 -0
- package/server/cli.js +67 -0
- package/server/index.js +287 -55
- package/server/middleware/account-lockout.js +112 -0
- package/server/middleware/auth.js +72 -9
- package/server/middleware/rate-limiter.js +82 -0
- package/server/routes/agent.js +3 -4
- package/server/routes/auth.js +86 -13
- package/server/routes/codex.js +1 -1
- package/server/routes/diagnostics.js +6 -3
- package/server/routes/gemini.js +1 -1
- package/server/routes/git.js +9 -9
- package/server/routes/live-view.js +2 -2
- package/server/routes/plugins.js +12 -12
- package/server/routes/projects.js +2 -2
- package/server/routes/qwen.js +1 -1
- package/server/routes/remote.js +1 -1
- package/server/routes/webhooks.js +1 -1
- package/server/services/startup-update.js +31 -6
- package/server/services/telegram/bot.js +6 -4
- package/server/setup-wizard.js +263 -0
- package/server/utils/plugin-loader.js +4 -0
- package/server/utils/port-access.js +26 -8
- package/server/utils/security-log.js +84 -0
package/server/routes/git.js
CHANGED
|
@@ -887,7 +887,7 @@ router.post('/initial-commit', async (req, res) => {
|
|
|
887
887
|
});
|
|
888
888
|
}
|
|
889
889
|
|
|
890
|
-
res.status(500).json({ error: error
|
|
890
|
+
res.status(500).json({ error: "Internal server error" });
|
|
891
891
|
}
|
|
892
892
|
});
|
|
893
893
|
|
|
@@ -918,7 +918,7 @@ router.post('/commit', async (req, res) => {
|
|
|
918
918
|
res.json({ success: true, output: stdout });
|
|
919
919
|
} catch (error) {
|
|
920
920
|
console.error('Git commit error:', error);
|
|
921
|
-
res.status(500).json({ error: error
|
|
921
|
+
res.status(500).json({ error: "Internal server error" });
|
|
922
922
|
}
|
|
923
923
|
});
|
|
924
924
|
|
|
@@ -965,7 +965,7 @@ router.post('/revert-local-commit', async (req, res) => {
|
|
|
965
965
|
});
|
|
966
966
|
} catch (error) {
|
|
967
967
|
console.error('Git revert local commit error:', error);
|
|
968
|
-
res.status(500).json({ error: error
|
|
968
|
+
res.status(500).json({ error: "Internal server error" });
|
|
969
969
|
}
|
|
970
970
|
});
|
|
971
971
|
|
|
@@ -1031,7 +1031,7 @@ router.post('/checkout', async (req, res) => {
|
|
|
1031
1031
|
res.json({ success: true, output: stdout });
|
|
1032
1032
|
} catch (error) {
|
|
1033
1033
|
console.error('Git checkout error:', error);
|
|
1034
|
-
res.status(500).json({ error: error
|
|
1034
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1035
1035
|
}
|
|
1036
1036
|
});
|
|
1037
1037
|
|
|
@@ -1053,7 +1053,7 @@ router.post('/create-branch', async (req, res) => {
|
|
|
1053
1053
|
res.json({ success: true, output: stdout });
|
|
1054
1054
|
} catch (error) {
|
|
1055
1055
|
console.error('Git create branch error:', error);
|
|
1056
|
-
res.status(500).json({ error: error
|
|
1056
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1057
1057
|
}
|
|
1058
1058
|
});
|
|
1059
1059
|
|
|
@@ -1079,7 +1079,7 @@ router.post('/delete-branch', async (req, res) => {
|
|
|
1079
1079
|
res.json({ success: true, output: stdout });
|
|
1080
1080
|
} catch (error) {
|
|
1081
1081
|
console.error('Git delete branch error:', error);
|
|
1082
|
-
res.status(500).json({ error: error
|
|
1082
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1083
1083
|
}
|
|
1084
1084
|
});
|
|
1085
1085
|
|
|
@@ -1234,7 +1234,7 @@ router.post('/generate-commit-message', async (req, res) => {
|
|
|
1234
1234
|
res.json({ message });
|
|
1235
1235
|
} catch (error) {
|
|
1236
1236
|
console.error('Generate commit message error:', error);
|
|
1237
|
-
res.status(500).json({ error: error
|
|
1237
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1238
1238
|
}
|
|
1239
1239
|
});
|
|
1240
1240
|
|
|
@@ -1764,7 +1764,7 @@ router.post('/discard', async (req, res) => {
|
|
|
1764
1764
|
res.json({ success: true, message: `Changes discarded for ${repositoryRelativeFilePath}` });
|
|
1765
1765
|
} catch (error) {
|
|
1766
1766
|
console.error('Git discard error:', error);
|
|
1767
|
-
res.status(500).json({ error: error
|
|
1767
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1768
1768
|
}
|
|
1769
1769
|
});
|
|
1770
1770
|
|
|
@@ -1815,7 +1815,7 @@ router.post('/delete-untracked', async (req, res) => {
|
|
|
1815
1815
|
}
|
|
1816
1816
|
} catch (error) {
|
|
1817
1817
|
console.error('Git delete untracked error:', error);
|
|
1818
|
-
res.status(500).json({ error: error
|
|
1818
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1819
1819
|
}
|
|
1820
1820
|
});
|
|
1821
1821
|
|
|
@@ -298,7 +298,7 @@ router.post('/:projectName/restart', requireLiveViewProjectAccess('useShell'), a
|
|
|
298
298
|
environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
|
|
299
299
|
});
|
|
300
300
|
} catch (error) {
|
|
301
|
-
res.status(500).json({ error:
|
|
301
|
+
res.status(500).json({ error: 'Failed to restart Live View' });
|
|
302
302
|
}
|
|
303
303
|
});
|
|
304
304
|
|
|
@@ -315,7 +315,7 @@ router.post('/:projectName/stop', requireLiveViewProjectAccess('useShell'), asyn
|
|
|
315
315
|
environment: attachEnvironmentRuntimeState(null, urls, tunnel),
|
|
316
316
|
});
|
|
317
317
|
} catch (error) {
|
|
318
|
-
res.status(500).json({ error:
|
|
318
|
+
res.status(500).json({ error: 'Failed to stop Live View' });
|
|
319
319
|
}
|
|
320
320
|
});
|
|
321
321
|
|
package/server/routes/plugins.js
CHANGED
|
@@ -317,7 +317,7 @@ router.get('/marketplace', (req, res) => {
|
|
|
317
317
|
categories: ['popular', 'skills', 'plugin', 'workflow', 'new'],
|
|
318
318
|
});
|
|
319
319
|
} catch (err) {
|
|
320
|
-
res.status(500).json({ error: 'Failed to read marketplace'
|
|
320
|
+
res.status(500).json({ error: 'Failed to read marketplace' });
|
|
321
321
|
}
|
|
322
322
|
});
|
|
323
323
|
|
|
@@ -358,7 +358,7 @@ router.get('/marketplace/search', async (req, res) => {
|
|
|
358
358
|
res.json({ entries });
|
|
359
359
|
} catch (err) {
|
|
360
360
|
const status = err?.name === 'AbortError' ? 504 : 502;
|
|
361
|
-
res.status(status).json({ error: 'GitHub search failed'
|
|
361
|
+
res.status(status).json({ error: 'GitHub search failed' });
|
|
362
362
|
} finally {
|
|
363
363
|
clearTimeout(timeout);
|
|
364
364
|
}
|
|
@@ -378,7 +378,7 @@ router.post('/marketplace/sources', (req, res) => {
|
|
|
378
378
|
writeMarketplaceSources(sources.slice(0, 200));
|
|
379
379
|
res.json({ success: true, source, installedSources: readMarketplaceSources() });
|
|
380
380
|
} catch (err) {
|
|
381
|
-
res.status(400).json({ error: 'Failed to add source'
|
|
381
|
+
res.status(400).json({ error: 'Failed to add source' });
|
|
382
382
|
}
|
|
383
383
|
});
|
|
384
384
|
|
|
@@ -391,7 +391,7 @@ router.delete('/marketplace/sources/:id', (req, res) => {
|
|
|
391
391
|
writeMarketplaceSources(next);
|
|
392
392
|
res.json({ success: true, installedSources: next });
|
|
393
393
|
} catch (err) {
|
|
394
|
-
res.status(400).json({ error: 'Failed to remove source'
|
|
394
|
+
res.status(400).json({ error: 'Failed to remove source' });
|
|
395
395
|
}
|
|
396
396
|
});
|
|
397
397
|
|
|
@@ -404,7 +404,7 @@ router.get('/', (req, res) => {
|
|
|
404
404
|
}));
|
|
405
405
|
res.json({ plugins });
|
|
406
406
|
} catch (err) {
|
|
407
|
-
res.status(500).json({ error: 'Failed to scan plugins'
|
|
407
|
+
res.status(500).json({ error: 'Failed to scan plugins' });
|
|
408
408
|
}
|
|
409
409
|
});
|
|
410
410
|
|
|
@@ -421,7 +421,7 @@ router.get('/:name/manifest', (req, res) => {
|
|
|
421
421
|
}
|
|
422
422
|
res.json(plugin);
|
|
423
423
|
} catch (err) {
|
|
424
|
-
res.status(500).json({ error: 'Failed to read plugin manifest'
|
|
424
|
+
res.status(500).json({ error: 'Failed to read plugin manifest' });
|
|
425
425
|
}
|
|
426
426
|
});
|
|
427
427
|
|
|
@@ -512,7 +512,7 @@ router.put('/:name/enable', async (req, res) => {
|
|
|
512
512
|
|
|
513
513
|
res.json({ success: true, name: req.params.name, enabled });
|
|
514
514
|
} catch (err) {
|
|
515
|
-
res.status(500).json({ error: 'Failed to update plugin'
|
|
515
|
+
res.status(500).json({ error: 'Failed to update plugin' });
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
|
|
@@ -545,7 +545,7 @@ router.post('/install', async (req, res) => {
|
|
|
545
545
|
|
|
546
546
|
res.json({ success: true, plugin: manifest });
|
|
547
547
|
} catch (err) {
|
|
548
|
-
res.status(400).json({ error: 'Failed to install plugin'
|
|
548
|
+
res.status(400).json({ error: 'Failed to install plugin' });
|
|
549
549
|
}
|
|
550
550
|
});
|
|
551
551
|
|
|
@@ -579,7 +579,7 @@ router.post('/:name/update', async (req, res) => {
|
|
|
579
579
|
|
|
580
580
|
res.json({ success: true, plugin: manifest });
|
|
581
581
|
} catch (err) {
|
|
582
|
-
res.status(400).json({ error: 'Failed to update plugin'
|
|
582
|
+
res.status(400).json({ error: 'Failed to update plugin' });
|
|
583
583
|
}
|
|
584
584
|
});
|
|
585
585
|
|
|
@@ -610,7 +610,7 @@ router.all(/^\/([a-zA-Z0-9_-]+)\/rpc\/(.*)$/, async (req, res) => {
|
|
|
610
610
|
try {
|
|
611
611
|
port = await startPluginServer(pluginName, pluginDir, plugin.server);
|
|
612
612
|
} catch (err) {
|
|
613
|
-
return res.status(503).json({ error: 'Plugin server failed to start'
|
|
613
|
+
return res.status(503).json({ error: 'Plugin server failed to start' });
|
|
614
614
|
}
|
|
615
615
|
}
|
|
616
616
|
|
|
@@ -646,7 +646,7 @@ router.all(/^\/([a-zA-Z0-9_-]+)\/rpc\/(.*)$/, async (req, res) => {
|
|
|
646
646
|
|
|
647
647
|
proxyReq.on('error', (err) => {
|
|
648
648
|
if (!res.headersSent) {
|
|
649
|
-
res.status(502).json({ error: 'Plugin server error'
|
|
649
|
+
res.status(502).json({ error: 'Plugin server error' });
|
|
650
650
|
} else {
|
|
651
651
|
res.end();
|
|
652
652
|
}
|
|
@@ -683,7 +683,7 @@ router.delete('/:name', async (req, res) => {
|
|
|
683
683
|
await uninstallPlugin(pluginName);
|
|
684
684
|
res.json({ success: true, name: pluginName });
|
|
685
685
|
} catch (err) {
|
|
686
|
-
res.status(400).json({ error: 'Failed to uninstall plugin'
|
|
686
|
+
res.status(400).json({ error: 'Failed to uninstall plugin' });
|
|
687
687
|
}
|
|
688
688
|
});
|
|
689
689
|
|
|
@@ -326,7 +326,7 @@ router.get('/:projectName/dir-status', requireAdmin, async (req, res) => {
|
|
|
326
326
|
}
|
|
327
327
|
} catch (error) {
|
|
328
328
|
console.error(`[projects] dir-status ${projectName}:`, error);
|
|
329
|
-
res.status(500).json({ error:
|
|
329
|
+
res.status(500).json({ error: 'Failed to check project directory' });
|
|
330
330
|
}
|
|
331
331
|
});
|
|
332
332
|
|
|
@@ -410,7 +410,7 @@ router.post('/quick-start', requireAdmin, async (req, res) => {
|
|
|
410
410
|
res.json({ success: true, project, suggestedName: name, reused: false });
|
|
411
411
|
} catch (error) {
|
|
412
412
|
console.error('[projects] quick-start failed:', error);
|
|
413
|
-
res.status(500).json({ error:
|
|
413
|
+
res.status(500).json({ error: 'Failed to quick-start project' });
|
|
414
414
|
}
|
|
415
415
|
});
|
|
416
416
|
|
package/server/routes/qwen.js
CHANGED
|
@@ -20,7 +20,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
|
|
|
20
20
|
res.json({ success: true });
|
|
21
21
|
} catch (error) {
|
|
22
22
|
console.error(`Error deleting Qwen session ${req.params.sessionId}:`, error);
|
|
23
|
-
res.status(500).json({ success: false, error: error
|
|
23
|
+
res.status(500).json({ success: false, error: "Internal server error" });
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
package/server/routes/remote.js
CHANGED
|
@@ -42,7 +42,7 @@ router.get('/control-room', async (_req, res) => {
|
|
|
42
42
|
controlRoom: await buildControlRoomSnapshot(),
|
|
43
43
|
});
|
|
44
44
|
} catch (error) {
|
|
45
|
-
res.status(500).json({ success: false, error: error
|
|
45
|
+
res.status(500).json({ success: false, error: "Internal server error" });
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ router.post('/test', async (req, res) => {
|
|
|
56
56
|
});
|
|
57
57
|
res.json({ success: true, result });
|
|
58
58
|
} catch (error) {
|
|
59
|
-
res.status(500).json({ success: false, error: error
|
|
59
|
+
res.status(500).json({ success: false, error: "Internal server error" });
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
3
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
5
|
import { Readable } from 'node:stream';
|
|
5
6
|
|
|
@@ -68,10 +69,14 @@ async function readLatestPackageMetadata() {
|
|
|
68
69
|
const latestVersion = metadata?.['dist-tags']?.latest;
|
|
69
70
|
const latestEntry = latestVersion ? metadata?.versions?.[latestVersion] : null;
|
|
70
71
|
const tarballUrl = latestEntry?.dist?.tarball;
|
|
72
|
+
const integrity = latestEntry?.dist?.integrity;
|
|
71
73
|
if (!latestVersion || !tarballUrl) {
|
|
72
74
|
throw new Error('Registry response missing latest version or tarball URL.');
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
if (!integrity) {
|
|
77
|
+
console.warn('[startup-update] Registry response missing integrity hash — proceeding without verification.');
|
|
78
|
+
}
|
|
79
|
+
return { latestVersion, tarballUrl, integrity };
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
async function installNpmGlobal(latestVersion, color) {
|
|
@@ -119,7 +124,7 @@ function readPackageVersion(appRoot) {
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
async function extractRuntimeTarball({ runtimeDir, tarballUrl, latestVersion, currentVersion, color }) {
|
|
127
|
+
async function extractRuntimeTarball({ runtimeDir, tarballUrl, latestVersion, currentVersion, color, integrity }) {
|
|
123
128
|
color?.info && console.log(`${color.info('[INFO]')} Updating runtime ${currentVersion} -> ${latestVersion} before opening the port...`);
|
|
124
129
|
|
|
125
130
|
const tarballRes = await fetch(tarballUrl);
|
|
@@ -127,6 +132,27 @@ async function extractRuntimeTarball({ runtimeDir, tarballUrl, latestVersion, cu
|
|
|
127
132
|
throw new Error(`Tarball fetch failed: HTTP ${tarballRes.status}`);
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
// Download tarball to a buffer first so we can verify integrity before extracting.
|
|
136
|
+
const tarballBuffer = Buffer.from(await tarballRes.arrayBuffer());
|
|
137
|
+
|
|
138
|
+
// Verify integrity hash (SRI format: "sha512-<base64>") if provided by the registry.
|
|
139
|
+
if (integrity && typeof integrity === 'string') {
|
|
140
|
+
const match = integrity.match(/^(sha512)-(.+)$/);
|
|
141
|
+
if (match) {
|
|
142
|
+
const algo = match[1];
|
|
143
|
+
const expectedHash = match[2];
|
|
144
|
+
const actualHash = crypto.createHash(algo).update(tarballBuffer).digest('base64');
|
|
145
|
+
if (actualHash !== expectedHash) {
|
|
146
|
+
throw new Error(`Integrity verification failed: expected ${algo}-${expectedHash.slice(0, 16)}..., got ${algo}-${actualHash.slice(0, 16)}...`);
|
|
147
|
+
}
|
|
148
|
+
color?.info && console.log(`${color.info('[INFO]')} Tarball integrity verified (${algo}).`);
|
|
149
|
+
} else {
|
|
150
|
+
console.warn('[startup-update] Unrecognized integrity format — skipping verification.');
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
console.warn('[startup-update] No integrity hash available — tarball extracted without verification.');
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
const stagingDir = path.join(runtimeDir, '.staging');
|
|
131
157
|
const backupDir = path.join(runtimeDir, '.previous');
|
|
132
158
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
@@ -137,9 +163,7 @@ async function extractRuntimeTarball({ runtimeDir, tarballUrl, latestVersion, cu
|
|
|
137
163
|
if (!tarExtract) throw new Error('tar extractor not available');
|
|
138
164
|
|
|
139
165
|
await new Promise((resolve, reject) => {
|
|
140
|
-
const nodeStream =
|
|
141
|
-
? Readable.fromWeb(tarballRes.body)
|
|
142
|
-
: tarballRes.body;
|
|
166
|
+
const nodeStream = Readable.from(tarballBuffer);
|
|
143
167
|
const extractor = tarExtract({ cwd: stagingDir, strip: 1 });
|
|
144
168
|
nodeStream.pipe(extractor);
|
|
145
169
|
extractor.on('finish', resolve);
|
|
@@ -199,7 +223,7 @@ export async function runStartupAutoUpdate({
|
|
|
199
223
|
return await updateGitCheckout(appRoot, color);
|
|
200
224
|
}
|
|
201
225
|
|
|
202
|
-
const { latestVersion, tarballUrl } = await readLatestPackageMetadata();
|
|
226
|
+
const { latestVersion, tarballUrl, integrity } = await readLatestPackageMetadata();
|
|
203
227
|
if (compareVersions(latestVersion, currentVersion) <= 0) {
|
|
204
228
|
return { updated: false, latestVersion };
|
|
205
229
|
}
|
|
@@ -211,6 +235,7 @@ export async function runStartupAutoUpdate({
|
|
|
211
235
|
latestVersion,
|
|
212
236
|
currentVersion,
|
|
213
237
|
color,
|
|
238
|
+
integrity,
|
|
214
239
|
});
|
|
215
240
|
return { updated: true, version: latestVersion, restartMode: 'exit42' };
|
|
216
241
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
|
|
3
4
|
import { telegramConfigDb, telegramLinksDb } from '../../database/db.js';
|
|
4
5
|
|
|
@@ -43,10 +44,11 @@ export const telegramEvents = new EventEmitter();
|
|
|
43
44
|
const now = () => Date.now();
|
|
44
45
|
|
|
45
46
|
const generate6DigitCode = () => {
|
|
46
|
-
// Zero-padded random 6-digit code. Using
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
47
|
+
// Zero-padded random 6-digit code. Using crypto.randomInt prevents
|
|
48
|
+
// modulo bias and ensures uniform distribution across the 000000–999999
|
|
49
|
+
// range. The code is short-lived (10min), single-use, and verified
|
|
50
|
+
// against a per-user row.
|
|
51
|
+
const n = crypto.randomInt(0, 1_000_000);
|
|
50
52
|
return n.toString().padStart(6, '0');
|
|
51
53
|
};
|
|
52
54
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
import { c } from './utils/colors.js';
|
|
9
|
+
|
|
10
|
+
const PIXCODE_DIR = path.join(os.homedir(), '.pixcode');
|
|
11
|
+
const SETUP_MARKER = path.join(PIXCODE_DIR, '.setup-complete');
|
|
12
|
+
const DEFAULT_PORT = 3001;
|
|
13
|
+
|
|
14
|
+
function isFirstRun() {
|
|
15
|
+
return !fs.existsSync(SETUP_MARKER);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function markSetupComplete(config) {
|
|
19
|
+
try {
|
|
20
|
+
fs.mkdirSync(PIXCODE_DIR, { recursive: true });
|
|
21
|
+
fs.writeFileSync(SETUP_MARKER, JSON.stringify({
|
|
22
|
+
completedAt: new Date().toISOString(),
|
|
23
|
+
...config,
|
|
24
|
+
}, null, 2));
|
|
25
|
+
} catch {
|
|
26
|
+
// Non-fatal — setup can complete without persistence
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readSetupConfig() {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(SETUP_MARKER, 'utf8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isPortAvailable(port) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const tester = net.createServer();
|
|
41
|
+
tester.once('error', () => resolve(false));
|
|
42
|
+
tester.once('listening', () => {
|
|
43
|
+
tester.close(() => resolve(true));
|
|
44
|
+
});
|
|
45
|
+
tester.listen(Number(port), '0.0.0.0');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
50
|
+
for (let port = startPort; port < startPort + maxAttempts; port++) {
|
|
51
|
+
if (await isPortAvailable(port)) return port;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isPortOpen(port, timeoutMs = 800) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const socket = net.createConnection({ host: '127.0.0.1', port: Number(port) });
|
|
59
|
+
let settled = false;
|
|
60
|
+
const done = (value) => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
settled = true;
|
|
63
|
+
socket.destroy();
|
|
64
|
+
resolve(value);
|
|
65
|
+
};
|
|
66
|
+
socket.setTimeout(timeoutMs);
|
|
67
|
+
socket.once('connect', () => done(true));
|
|
68
|
+
socket.once('timeout', () => done(false));
|
|
69
|
+
socket.once('error', () => done(false));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function waitForServerReady(port, timeoutMs = 30000) {
|
|
74
|
+
const deadline = Date.now() + timeoutMs;
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
if (await isPortOpen(port)) return true;
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ask(question) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
85
|
+
resolve(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
89
|
+
rl.question(question, (answer) => {
|
|
90
|
+
rl.close();
|
|
91
|
+
resolve(answer?.trim() || '');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function askSelect(question, options) {
|
|
97
|
+
while (true) {
|
|
98
|
+
const answer = await ask(question);
|
|
99
|
+
if (answer === null) return null;
|
|
100
|
+
const lower = answer.toLowerCase();
|
|
101
|
+
for (const opt of options) {
|
|
102
|
+
if (lower === opt.key || lower === String(opt.key)) return opt;
|
|
103
|
+
if (opt.aliases?.some(a => lower === a)) return opt;
|
|
104
|
+
}
|
|
105
|
+
console.log(`${c.warn(' Invalid choice. Please try again.')}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function openBrowser(url) {
|
|
110
|
+
const platform = process.platform;
|
|
111
|
+
// Use spawn with an argument array instead of exec with a shell string
|
|
112
|
+
// to prevent command injection through the url value.
|
|
113
|
+
let bin;
|
|
114
|
+
let args;
|
|
115
|
+
if (platform === 'darwin') {
|
|
116
|
+
bin = 'open';
|
|
117
|
+
args = [url];
|
|
118
|
+
} else if (platform === 'win32') {
|
|
119
|
+
bin = 'cmd';
|
|
120
|
+
args = ['/c', 'start', '', url];
|
|
121
|
+
} else {
|
|
122
|
+
bin = 'xdg-open';
|
|
123
|
+
args = [url];
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
spawn(bin, args, { stdio: 'ignore', timeout: 3000, detached: true, shell: false }).unref();
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function printBanner() {
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(c.dim(' ╔═══════════════════════════════════════════════════════════════╗'));
|
|
136
|
+
console.log(c.dim(' ║') + c.bright(' Pixcode Setup Wizard ') + c.dim('║'));
|
|
137
|
+
console.log(c.dim(' ╠═══════════════════════════════════════════════════════════════╣'));
|
|
138
|
+
console.log(c.dim(' ║') + c.info(' Self-hosted AI coding agent control room ') + c.dim('║'));
|
|
139
|
+
console.log(c.dim(' ║') + c.dim(' Claude Code, Cursor, Codex, Gemini, Qwen... ') + c.dim('║'));
|
|
140
|
+
console.log(c.dim(' ╚═══════════════════════════════════════════════════════════════╝'));
|
|
141
|
+
console.log('');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function printReady(port) {
|
|
145
|
+
const url = `http://localhost:${port}`;
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log(c.dim(' ╔═══════════════════════════════════════════════════════════════╗'));
|
|
148
|
+
console.log(c.dim(' ║') + c.bright(' Pixcode is Ready! ') + c.dim('║'));
|
|
149
|
+
console.log(c.dim(' ╠═══════════════════════════════════════════════════════════════╣'));
|
|
150
|
+
console.log(c.dim(' ║') + ` Open ${c.bright(url)} in your browser ` + c.dim('║'));
|
|
151
|
+
console.log(c.dim(' ╚═══════════════════════════════════════════════════════════════╝'));
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(` ${c.info('[INFO]')} Web UI: ${c.bright(url)}`);
|
|
154
|
+
console.log(` ${c.info('[INFO]')} Health: ${c.dim(url + '/health')}`);
|
|
155
|
+
console.log('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function runSetupWizard(existingOptions = {}) {
|
|
159
|
+
if (!isFirstRun()) return null;
|
|
160
|
+
|
|
161
|
+
printBanner();
|
|
162
|
+
|
|
163
|
+
console.log(` ${c.info('Welcome!')} Let's get Pixcode configured.\n`);
|
|
164
|
+
|
|
165
|
+
// Step 1: Port selection
|
|
166
|
+
let port = existingOptions.serverPort || process.env.SERVER_PORT || DEFAULT_PORT;
|
|
167
|
+
const portAvailable = await isPortAvailable(port);
|
|
168
|
+
|
|
169
|
+
if (!portAvailable) {
|
|
170
|
+
console.log(` ${c.warn('[WARN]')} Port ${c.bright(String(port))} is already in use.`);
|
|
171
|
+
const altPort = await findAvailablePort(Number(port) + 1);
|
|
172
|
+
if (altPort) {
|
|
173
|
+
console.log(` ${c.info('[INFO]')} Port ${c.bright(String(altPort))} is available.`);
|
|
174
|
+
const choice = await askSelect(
|
|
175
|
+
` Use port ${altPort}? [Y/n] `,
|
|
176
|
+
[
|
|
177
|
+
{ key: 'y', label: 'yes', aliases: ['yes', '', 'e', 'evet'] },
|
|
178
|
+
{ key: 'n', label: 'no', aliases: ['no', 'n', 'h', 'hayir'] },
|
|
179
|
+
],
|
|
180
|
+
);
|
|
181
|
+
if (choice?.key !== 'n') {
|
|
182
|
+
port = altPort;
|
|
183
|
+
} else {
|
|
184
|
+
const customPort = await ask(` Enter a port number (or press Enter for ${altPort}): `);
|
|
185
|
+
if (customPort && /^\d+$/.test(customPort)) {
|
|
186
|
+
port = Number(customPort);
|
|
187
|
+
} else {
|
|
188
|
+
port = altPort;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
const customPort = await ask(` No free ports found near ${port}. Enter a port manually: `);
|
|
193
|
+
if (customPort && /^\d+$/.test(customPort)) {
|
|
194
|
+
port = Number(customPort);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
console.log(` ${c.ok('[OK]')} Port ${c.bright(String(port))} is available.`);
|
|
199
|
+
const customPort = await ask(` Press Enter to use port ${port}, or enter a different port: `);
|
|
200
|
+
if (customPort && /^\d+$/.test(customPort)) {
|
|
201
|
+
const newPort = Number(customPort);
|
|
202
|
+
if (await isPortAvailable(newPort)) {
|
|
203
|
+
port = newPort;
|
|
204
|
+
console.log(` ${c.ok('[OK]')} Port ${c.bright(String(port))} is available.`);
|
|
205
|
+
} else {
|
|
206
|
+
console.log(` ${c.warn('[WARN]')} Port ${newPort} is in use. Keeping port ${port}.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
process.env.SERVER_PORT = String(port);
|
|
212
|
+
|
|
213
|
+
// Step 2: Mode selection (Linux only)
|
|
214
|
+
let useDaemon = existingOptions.noDaemon ? false : (process.platform === 'linux');
|
|
215
|
+
|
|
216
|
+
if (process.platform === 'linux' && !existingOptions.noDaemon) {
|
|
217
|
+
console.log('');
|
|
218
|
+
const modeChoice = await askSelect(
|
|
219
|
+
` ${c.info('Run mode:')} Keep running in background (daemon) or foreground?\n` +
|
|
220
|
+
` ${c.dim(' [1]')} Daemon (recommended — survives terminal close)\n` +
|
|
221
|
+
` ${c.dim(' [2]')} Foreground (stops when terminal closes)\n` +
|
|
222
|
+
` Choose [1/2] (default 1): `,
|
|
223
|
+
[
|
|
224
|
+
{ key: '1', label: 'daemon', aliases: ['d', 'daemon', ''] },
|
|
225
|
+
{ key: '2', label: 'foreground', aliases: ['f', 'fg', 'foreground'] },
|
|
226
|
+
],
|
|
227
|
+
);
|
|
228
|
+
useDaemon = modeChoice?.key !== '2';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 3: Summary
|
|
232
|
+
console.log('');
|
|
233
|
+
console.log(` ${c.info('[Setup]')} Configuration:`);
|
|
234
|
+
console.log(` Port: ${c.bright(String(port))}`);
|
|
235
|
+
if (process.platform === 'linux') {
|
|
236
|
+
console.log(` Mode: ${c.bright(useDaemon ? 'Daemon (background)' : 'Foreground')}`);
|
|
237
|
+
}
|
|
238
|
+
console.log('');
|
|
239
|
+
|
|
240
|
+
markSetupComplete({ port, mode: useDaemon ? 'daemon' : 'foreground' });
|
|
241
|
+
|
|
242
|
+
return { port, useDaemon };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function postStartupGuidance(port) {
|
|
246
|
+
const effectivePort = Number(port) || DEFAULT_PORT;
|
|
247
|
+
const url = `http://localhost:${effectivePort}`;
|
|
248
|
+
|
|
249
|
+
printReady(effectivePort);
|
|
250
|
+
|
|
251
|
+
// Try to open browser
|
|
252
|
+
const opened = openBrowser(url);
|
|
253
|
+
if (opened) {
|
|
254
|
+
console.log(` ${c.ok('[OK]')} Opening browser...`);
|
|
255
|
+
} else {
|
|
256
|
+
console.log(` ${c.tip('[TIP]')} Open ${c.bright(url)} in your browser to start using Pixcode.`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(` ${c.dim('Tip: Run "pixcode status" to see full configuration.')}`);
|
|
260
|
+
console.log('');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export { isFirstRun, isPortAvailable, findAvailablePort, openBrowser, waitForServerReady, isPortOpen };
|
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
5
|
|
|
6
|
+
import { securityLog } from './security-log.js';
|
|
7
|
+
|
|
6
8
|
const PLUGINS_DIR = path.join(os.homedir(), '.pixcode', 'plugins');
|
|
7
9
|
const PLUGINS_CONFIG_PATH = path.join(os.homedir(), '.pixcode', 'plugins.json');
|
|
8
10
|
|
|
@@ -290,6 +292,7 @@ export function installPluginFromGit(url) {
|
|
|
290
292
|
cleanupTemp();
|
|
291
293
|
return reject(new Error(`Failed to move plugin into place: ${err.message}`));
|
|
292
294
|
}
|
|
295
|
+
securityLog('plugin_installed', { reason: manifest.name });
|
|
293
296
|
resolve(manifest);
|
|
294
297
|
};
|
|
295
298
|
|
|
@@ -303,6 +306,7 @@ export function installPluginFromGit(url) {
|
|
|
303
306
|
gitProcess.on('close', (code) => {
|
|
304
307
|
if (code !== 0) {
|
|
305
308
|
cleanupTemp();
|
|
309
|
+
securityLog('plugin_install_failed', { reason: `git clone failed: exit code ${code}` });
|
|
306
310
|
return reject(new Error(`git clone failed (exit code ${code}): ${stderr.trim()}`));
|
|
307
311
|
}
|
|
308
312
|
|