@pixelbyte-software/pixcode 1.30.2 → 1.31.0
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/LICENSE +718 -718
- package/README.de.md +248 -248
- package/README.ja.md +240 -240
- package/README.ko.md +240 -240
- package/README.md +295 -285
- package/README.ru.md +248 -248
- package/README.tr.md +250 -250
- package/README.zh-CN.md +240 -240
- package/dist/api-docs.html +879 -879
- package/dist/assets/index-BRRJ47XQ.css +32 -0
- package/dist/assets/index-EQohwyiC.js +837 -0
- package/dist/clear-cache.html +85 -85
- package/dist/convert-icons.md +52 -52
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +7 -8
- package/dist/generate-icons.js +48 -48
- package/dist/icons/codex-white.svg +3 -3
- package/dist/icons/codex.svg +3 -3
- package/dist/icons/cursor-white.svg +11 -11
- package/dist/icons/icon-128x128.png +0 -0
- package/dist/icons/icon-128x128.svg +9 -12
- package/dist/icons/icon-144x144.png +0 -0
- package/dist/icons/icon-144x144.svg +9 -12
- package/dist/icons/icon-152x152.png +0 -0
- package/dist/icons/icon-152x152.svg +9 -12
- package/dist/icons/icon-192x192.png +0 -0
- package/dist/icons/icon-192x192.svg +9 -12
- package/dist/icons/icon-384x384.png +0 -0
- package/dist/icons/icon-384x384.svg +9 -12
- package/dist/icons/icon-512x512.png +0 -0
- package/dist/icons/icon-512x512.svg +9 -12
- package/dist/icons/icon-72x72.png +0 -0
- package/dist/icons/icon-72x72.svg +9 -12
- package/dist/icons/icon-96x96.png +0 -0
- package/dist/icons/icon-96x96.svg +9 -12
- package/dist/icons/icon-template.svg +9 -12
- package/dist/icons/qwen-ai-icon.png +0 -0
- package/dist/index.html +59 -49
- package/dist/logo.png +0 -0
- package/dist/logo.svg +11 -16
- package/dist/manifest.json +60 -60
- package/dist/sw.js +124 -124
- package/dist-server/server/cli.js +100 -97
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/daemon/manager.js +33 -33
- package/dist-server/server/daemon-manager.js +62 -62
- package/dist-server/server/database/db.js +114 -22
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/database/schema.js +122 -89
- package/dist-server/server/database/schema.js.map +1 -1
- package/dist-server/server/gemini-cli.js +6 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/index.js +234 -64
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +29 -2
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +22 -2
- 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 +2 -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 +14 -2
- 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 +132 -0
- package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js +87 -0
- package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js +201 -0
- package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/qwen/qwen.provider.js +19 -0
- package/dist-server/server/modules/providers/list/qwen/qwen.provider.js.map +1 -0
- package/dist-server/server/modules/providers/provider.registry.js +2 -0
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +310 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/projects.js +197 -6
- package/dist-server/server/projects.js.map +1 -1
- package/dist-server/server/qwen-code-cli.js +350 -0
- package/dist-server/server/qwen-code-cli.js.map +1 -0
- package/dist-server/server/qwen-response-handler.js +70 -0
- package/dist-server/server/qwen-response-handler.js.map +1 -0
- package/dist-server/server/routes/commands.js +25 -25
- package/dist-server/server/routes/git.js +17 -17
- package/dist-server/server/routes/network.js +116 -0
- package/dist-server/server/routes/network.js.map +1 -0
- package/dist-server/server/routes/projects.js +43 -0
- package/dist-server/server/routes/projects.js.map +1 -1
- package/dist-server/server/routes/qwen.js +23 -0
- package/dist-server/server/routes/qwen.js.map +1 -0
- package/dist-server/server/routes/taskmaster.js +419 -419
- package/dist-server/server/routes/telegram.js +119 -0
- package/dist-server/server/routes/telegram.js.map +1 -0
- package/dist-server/server/services/external-access.js +228 -0
- package/dist-server/server/services/external-access.js.map +1 -0
- package/dist-server/server/services/install-jobs.js +394 -0
- package/dist-server/server/services/install-jobs.js.map +1 -0
- package/dist-server/server/services/notification-orchestrator.js +19 -5
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/provider-credentials.js +154 -0
- package/dist-server/server/services/provider-credentials.js.map +1 -0
- package/dist-server/server/services/provider-models.js +218 -0
- package/dist-server/server/services/provider-models.js.map +1 -0
- package/dist-server/server/services/telegram/bot.js +259 -0
- package/dist-server/server/services/telegram/bot.js.map +1 -0
- package/dist-server/server/services/telegram/translations.js +160 -0
- package/dist-server/server/services/telegram/translations.js.map +1 -0
- package/dist-server/server/utils/port-access.js +196 -0
- package/dist-server/server/utils/port-access.js.map +1 -0
- package/dist-server/shared/modelConstants.js +18 -0
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +177 -168
- package/scripts/fix-node-pty.js +67 -67
- package/server/claude-sdk.js +834 -834
- package/server/cli.js +940 -937
- package/server/constants/config.js +4 -4
- package/server/cursor-cli.js +342 -342
- package/server/daemon/manager.js +564 -564
- package/server/daemon-manager.js +920 -920
- package/server/database/db.js +696 -593
- package/server/database/schema.js +138 -102
- package/server/gemini-cli.js +475 -469
- package/server/gemini-response-handler.js +79 -79
- package/server/index.js +2730 -2556
- package/server/load-env.js +34 -34
- package/server/middleware/auth.js +132 -132
- package/server/modules/providers/list/claude/claude-auth.provider.ts +145 -123
- package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
- package/server/modules/providers/list/claude/claude.provider.ts +15 -15
- package/server/modules/providers/list/codex/codex-auth.provider.ts +115 -100
- package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
- package/server/modules/providers/list/codex/codex.provider.ts +15 -15
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
- package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
- package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +163 -151
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
- package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
- package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -0
- package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -0
- package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +218 -0
- package/server/modules/providers/list/qwen/qwen.provider.ts +21 -0
- package/server/modules/providers/provider.registry.ts +38 -36
- package/server/modules/providers/provider.routes.ts +583 -217
- package/server/modules/providers/services/mcp.service.ts +94 -94
- package/server/modules/providers/services/provider-auth.service.ts +26 -26
- package/server/modules/providers/services/sessions.service.ts +45 -45
- package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
- package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
- package/server/modules/providers/tests/mcp.test.ts +293 -293
- package/server/openai-codex.js +426 -426
- package/server/projects.js +2993 -2792
- package/server/qwen-code-cli.js +392 -0
- package/server/qwen-response-handler.js +73 -0
- package/server/routes/agent.js +1245 -1245
- package/server/routes/auth.js +134 -134
- package/server/routes/codex.js +19 -19
- package/server/routes/commands.js +554 -554
- package/server/routes/cursor.js +52 -52
- package/server/routes/gemini.js +24 -24
- package/server/routes/git.js +1488 -1488
- package/server/routes/mcp-utils.js +31 -31
- package/server/routes/messages.js +61 -61
- package/server/routes/network.js +128 -0
- package/server/routes/plugins.js +307 -307
- package/server/routes/projects.js +675 -627
- package/server/routes/qwen.js +27 -0
- package/server/routes/settings.js +286 -286
- package/server/routes/taskmaster.js +1471 -1471
- package/server/routes/telegram.js +125 -0
- package/server/routes/user.js +123 -123
- package/server/services/external-access.js +240 -0
- package/server/services/install-jobs.js +410 -0
- package/server/services/notification-orchestrator.js +242 -227
- package/server/services/provider-credentials.js +151 -0
- package/server/services/provider-models.js +225 -0
- package/server/services/telegram/bot.js +280 -0
- package/server/services/telegram/translations.js +170 -0
- package/server/services/vapid-keys.js +35 -35
- package/server/sessionManager.js +225 -225
- package/server/shared/interfaces.ts +54 -54
- package/server/shared/types.ts +172 -172
- package/server/shared/utils.ts +193 -193
- package/server/tsconfig.json +36 -36
- package/server/utils/colors.js +21 -21
- package/server/utils/commandParser.js +303 -303
- package/server/utils/frontmatter.js +18 -18
- package/server/utils/gitConfig.js +34 -34
- package/server/utils/mcp-detector.js +147 -147
- package/server/utils/plugin-loader.js +457 -457
- package/server/utils/plugin-process-manager.js +184 -184
- package/server/utils/port-access.js +209 -0
- package/server/utils/runtime-paths.js +37 -37
- package/server/utils/taskmaster-websocket.js +128 -128
- package/server/utils/url-detection.js +71 -71
- package/server/vite-daemon.js +78 -78
- package/shared/modelConstants.js +117 -97
- package/shared/networkHosts.js +22 -22
- package/dist/assets/index-C2c9QNwK.css +0 -32
- package/dist/assets/index-DyXDZED-.js +0 -1277
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
import { telegramLinksDb } from '../database/db.js';
|
|
4
|
+
import {
|
|
5
|
+
createPairingCode,
|
|
6
|
+
getBotState,
|
|
7
|
+
getPublicConfig,
|
|
8
|
+
removeBotConfig,
|
|
9
|
+
startBot,
|
|
10
|
+
stopBot,
|
|
11
|
+
} from '../services/telegram/bot.js';
|
|
12
|
+
import { SUPPORTED_LANGUAGES } from '../services/telegram/translations.js';
|
|
13
|
+
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
|
|
16
|
+
const sanitizeLanguage = (raw) => {
|
|
17
|
+
if (typeof raw !== 'string') return 'en';
|
|
18
|
+
return SUPPORTED_LANGUAGES.includes(raw) ? raw : 'en';
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// GET /api/telegram/status — combined bot + personal link state
|
|
22
|
+
router.get('/status', (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const bot = getBotState();
|
|
25
|
+
const config = getPublicConfig();
|
|
26
|
+
const link = telegramLinksDb.getByUserId(req.user.id);
|
|
27
|
+
res.json({
|
|
28
|
+
bot: { ...bot, ...config },
|
|
29
|
+
link: link
|
|
30
|
+
? {
|
|
31
|
+
paired: Boolean(link.chat_id && link.verified_at),
|
|
32
|
+
telegramUsername: link.telegram_username,
|
|
33
|
+
language: link.language,
|
|
34
|
+
notificationsEnabled: Boolean(link.notifications_enabled),
|
|
35
|
+
bridgeEnabled: Boolean(link.bridge_enabled),
|
|
36
|
+
pairingCode: link.pairing_code,
|
|
37
|
+
pairingExpiresAt: link.pairing_code_expires_at,
|
|
38
|
+
verifiedAt: link.verified_at,
|
|
39
|
+
}
|
|
40
|
+
: null,
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('telegram/status failed:', error);
|
|
44
|
+
res.status(500).json({ error: 'Failed to read Telegram status' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// POST /api/telegram/bot — save token and start the bot
|
|
49
|
+
router.post('/bot', async (req, res) => {
|
|
50
|
+
const { token } = req.body || {};
|
|
51
|
+
if (typeof token !== 'string' || token.length < 10) {
|
|
52
|
+
return res.status(400).json({ error: 'A valid bot token is required' });
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const info = await startBot({ token });
|
|
56
|
+
res.json({ success: true, bot: { ...getBotState(), configured: true, username: info.username } });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('telegram/bot start failed:', error);
|
|
59
|
+
const status = error?.code === 'INVALID_TOKEN' ? 400 : 502;
|
|
60
|
+
res.status(status).json({ error: error?.message || 'Failed to start bot' });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// DELETE /api/telegram/bot — stop and remove the configured bot
|
|
65
|
+
router.delete('/bot', async (req, res) => {
|
|
66
|
+
try {
|
|
67
|
+
await removeBotConfig();
|
|
68
|
+
res.json({ success: true, bot: getBotState() });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('telegram/bot remove failed:', error);
|
|
71
|
+
res.status(502).json({ error: 'Failed to remove bot' });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// POST /api/telegram/bot/stop — stop polling but keep the token
|
|
76
|
+
router.post('/bot/stop', async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
await stopBot();
|
|
79
|
+
res.json({ success: true, bot: getBotState() });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('telegram/bot/stop failed:', error);
|
|
82
|
+
res.status(502).json({ error: 'Failed to stop bot' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// POST /api/telegram/pairing-code — (re)generate a 6-digit code for this user
|
|
87
|
+
router.post('/pairing-code', (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const language = sanitizeLanguage(req.body?.language);
|
|
90
|
+
const { code, expiresAt } = createPairingCode(req.user.id, language);
|
|
91
|
+
res.json({ success: true, code, expiresAt, language });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('telegram/pairing-code failed:', error);
|
|
94
|
+
res.status(500).json({ error: 'Failed to generate pairing code' });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// PATCH /api/telegram/link — update language / toggles on the user's link
|
|
99
|
+
router.patch('/link', (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const { language, notificationsEnabled, bridgeEnabled } = req.body || {};
|
|
102
|
+
const payload = {};
|
|
103
|
+
if (language !== undefined) payload.language = sanitizeLanguage(language);
|
|
104
|
+
if (notificationsEnabled !== undefined) payload.notificationsEnabled = Boolean(notificationsEnabled);
|
|
105
|
+
if (bridgeEnabled !== undefined) payload.bridgeEnabled = Boolean(bridgeEnabled);
|
|
106
|
+
telegramLinksDb.updatePreferences(req.user.id, payload);
|
|
107
|
+
res.json({ success: true, link: telegramLinksDb.getByUserId(req.user.id) });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('telegram/link patch failed:', error);
|
|
110
|
+
res.status(500).json({ error: 'Failed to update link' });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// DELETE /api/telegram/link — unpair
|
|
115
|
+
router.delete('/link', (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
telegramLinksDb.unlink(req.user.id);
|
|
118
|
+
res.json({ success: true });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('telegram/link delete failed:', error);
|
|
121
|
+
res.status(500).json({ error: 'Failed to unpair' });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export default router;
|
package/server/routes/user.js
CHANGED
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { userDb } from '../database/db.js';
|
|
3
|
-
import { authenticateToken } from '../middleware/auth.js';
|
|
4
|
-
import { getSystemGitConfig } from '../utils/gitConfig.js';
|
|
5
|
-
import { spawn } from 'child_process';
|
|
6
|
-
|
|
7
|
-
const router = express.Router();
|
|
8
|
-
|
|
9
|
-
function spawnAsync(command, args, options = {}) {
|
|
10
|
-
return new Promise((resolve, reject) => {
|
|
11
|
-
const child = spawn(command, args, { ...options, shell: false });
|
|
12
|
-
let stdout = '';
|
|
13
|
-
let stderr = '';
|
|
14
|
-
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
15
|
-
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
16
|
-
child.on('error', (error) => { reject(error); });
|
|
17
|
-
child.on('close', (code) => {
|
|
18
|
-
if (code === 0) { resolve({ stdout, stderr }); return; }
|
|
19
|
-
const error = new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
20
|
-
error.code = code;
|
|
21
|
-
error.stdout = stdout;
|
|
22
|
-
error.stderr = stderr;
|
|
23
|
-
reject(error);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
router.get('/git-config', authenticateToken, async (req, res) => {
|
|
29
|
-
try {
|
|
30
|
-
const userId = req.user.id;
|
|
31
|
-
let gitConfig = userDb.getGitConfig(userId);
|
|
32
|
-
|
|
33
|
-
// If database is empty, try to get from system git config
|
|
34
|
-
if (!gitConfig || (!gitConfig.git_name && !gitConfig.git_email)) {
|
|
35
|
-
const systemConfig = await getSystemGitConfig();
|
|
36
|
-
|
|
37
|
-
// If system has values, save them to database for this user
|
|
38
|
-
if (systemConfig.git_name || systemConfig.git_email) {
|
|
39
|
-
userDb.updateGitConfig(userId, systemConfig.git_name, systemConfig.git_email);
|
|
40
|
-
gitConfig = systemConfig;
|
|
41
|
-
console.log(`Auto-populated git config from system for user ${userId}: ${systemConfig.git_name} <${systemConfig.git_email}>`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
res.json({
|
|
46
|
-
success: true,
|
|
47
|
-
gitName: gitConfig?.git_name || null,
|
|
48
|
-
gitEmail: gitConfig?.git_email || null
|
|
49
|
-
});
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('Error getting git config:', error);
|
|
52
|
-
res.status(500).json({ error: 'Failed to get git configuration' });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Apply git config globally via git config --global
|
|
57
|
-
router.post('/git-config', authenticateToken, async (req, res) => {
|
|
58
|
-
try {
|
|
59
|
-
const userId = req.user.id;
|
|
60
|
-
const { gitName, gitEmail } = req.body;
|
|
61
|
-
|
|
62
|
-
if (!gitName || !gitEmail) {
|
|
63
|
-
return res.status(400).json({ error: 'Git name and email are required' });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Validate email format
|
|
67
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
68
|
-
if (!emailRegex.test(gitEmail)) {
|
|
69
|
-
return res.status(400).json({ error: 'Invalid email format' });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
userDb.updateGitConfig(userId, gitName, gitEmail);
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await spawnAsync('git', ['config', '--global', 'user.name', gitName]);
|
|
76
|
-
await spawnAsync('git', ['config', '--global', 'user.email', gitEmail]);
|
|
77
|
-
console.log(`Applied git config globally: ${gitName} <${gitEmail}>`);
|
|
78
|
-
} catch (gitError) {
|
|
79
|
-
console.error('Error applying git config:', gitError);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
res.json({
|
|
83
|
-
success: true,
|
|
84
|
-
gitName,
|
|
85
|
-
gitEmail
|
|
86
|
-
});
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error('Error updating git config:', error);
|
|
89
|
-
res.status(500).json({ error: 'Failed to update git configuration' });
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
router.post('/complete-onboarding', authenticateToken, async (req, res) => {
|
|
94
|
-
try {
|
|
95
|
-
const userId = req.user.id;
|
|
96
|
-
userDb.completeOnboarding(userId);
|
|
97
|
-
|
|
98
|
-
res.json({
|
|
99
|
-
success: true,
|
|
100
|
-
message: 'Onboarding completed successfully'
|
|
101
|
-
});
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error('Error completing onboarding:', error);
|
|
104
|
-
res.status(500).json({ error: 'Failed to complete onboarding' });
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
router.get('/onboarding-status', authenticateToken, async (req, res) => {
|
|
109
|
-
try {
|
|
110
|
-
const userId = req.user.id;
|
|
111
|
-
const hasCompleted = userDb.hasCompletedOnboarding(userId);
|
|
112
|
-
|
|
113
|
-
res.json({
|
|
114
|
-
success: true,
|
|
115
|
-
hasCompletedOnboarding: hasCompleted
|
|
116
|
-
});
|
|
117
|
-
} catch (error) {
|
|
118
|
-
console.error('Error checking onboarding status:', error);
|
|
119
|
-
res.status(500).json({ error: 'Failed to check onboarding status' });
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
export default router;
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { userDb } from '../database/db.js';
|
|
3
|
+
import { authenticateToken } from '../middleware/auth.js';
|
|
4
|
+
import { getSystemGitConfig } from '../utils/gitConfig.js';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
|
|
9
|
+
function spawnAsync(command, args, options = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(command, args, { ...options, shell: false });
|
|
12
|
+
let stdout = '';
|
|
13
|
+
let stderr = '';
|
|
14
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
15
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
16
|
+
child.on('error', (error) => { reject(error); });
|
|
17
|
+
child.on('close', (code) => {
|
|
18
|
+
if (code === 0) { resolve({ stdout, stderr }); return; }
|
|
19
|
+
const error = new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
20
|
+
error.code = code;
|
|
21
|
+
error.stdout = stdout;
|
|
22
|
+
error.stderr = stderr;
|
|
23
|
+
reject(error);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
router.get('/git-config', authenticateToken, async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const userId = req.user.id;
|
|
31
|
+
let gitConfig = userDb.getGitConfig(userId);
|
|
32
|
+
|
|
33
|
+
// If database is empty, try to get from system git config
|
|
34
|
+
if (!gitConfig || (!gitConfig.git_name && !gitConfig.git_email)) {
|
|
35
|
+
const systemConfig = await getSystemGitConfig();
|
|
36
|
+
|
|
37
|
+
// If system has values, save them to database for this user
|
|
38
|
+
if (systemConfig.git_name || systemConfig.git_email) {
|
|
39
|
+
userDb.updateGitConfig(userId, systemConfig.git_name, systemConfig.git_email);
|
|
40
|
+
gitConfig = systemConfig;
|
|
41
|
+
console.log(`Auto-populated git config from system for user ${userId}: ${systemConfig.git_name} <${systemConfig.git_email}>`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
res.json({
|
|
46
|
+
success: true,
|
|
47
|
+
gitName: gitConfig?.git_name || null,
|
|
48
|
+
gitEmail: gitConfig?.git_email || null
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Error getting git config:', error);
|
|
52
|
+
res.status(500).json({ error: 'Failed to get git configuration' });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Apply git config globally via git config --global
|
|
57
|
+
router.post('/git-config', authenticateToken, async (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const userId = req.user.id;
|
|
60
|
+
const { gitName, gitEmail } = req.body;
|
|
61
|
+
|
|
62
|
+
if (!gitName || !gitEmail) {
|
|
63
|
+
return res.status(400).json({ error: 'Git name and email are required' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate email format
|
|
67
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
68
|
+
if (!emailRegex.test(gitEmail)) {
|
|
69
|
+
return res.status(400).json({ error: 'Invalid email format' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
userDb.updateGitConfig(userId, gitName, gitEmail);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await spawnAsync('git', ['config', '--global', 'user.name', gitName]);
|
|
76
|
+
await spawnAsync('git', ['config', '--global', 'user.email', gitEmail]);
|
|
77
|
+
console.log(`Applied git config globally: ${gitName} <${gitEmail}>`);
|
|
78
|
+
} catch (gitError) {
|
|
79
|
+
console.error('Error applying git config:', gitError);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
res.json({
|
|
83
|
+
success: true,
|
|
84
|
+
gitName,
|
|
85
|
+
gitEmail
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Error updating git config:', error);
|
|
89
|
+
res.status(500).json({ error: 'Failed to update git configuration' });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
router.post('/complete-onboarding', authenticateToken, async (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const userId = req.user.id;
|
|
96
|
+
userDb.completeOnboarding(userId);
|
|
97
|
+
|
|
98
|
+
res.json({
|
|
99
|
+
success: true,
|
|
100
|
+
message: 'Onboarding completed successfully'
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error completing onboarding:', error);
|
|
104
|
+
res.status(500).json({ error: 'Failed to complete onboarding' });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
router.get('/onboarding-status', authenticateToken, async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const userId = req.user.id;
|
|
111
|
+
const hasCompleted = userDb.hasCompletedOnboarding(userId);
|
|
112
|
+
|
|
113
|
+
res.json({
|
|
114
|
+
success: true,
|
|
115
|
+
hasCompletedOnboarding: hasCompleted
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Error checking onboarding status:', error);
|
|
119
|
+
res.status(500).json({ error: 'Failed to check onboarding status' });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export default router;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
const requireCjs = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// nat-upnp is CommonJS and callback-based. We wrap it in promises and keep
|
|
7
|
+
// one shared client per process. The client is lazily created so importing
|
|
8
|
+
// this module does not try to bind SSDP sockets at boot.
|
|
9
|
+
let upnpClient = null;
|
|
10
|
+
const getUpnpClient = () => {
|
|
11
|
+
if (!upnpClient) {
|
|
12
|
+
const nat = requireCjs('nat-upnp');
|
|
13
|
+
upnpClient = nat.createClient();
|
|
14
|
+
}
|
|
15
|
+
return upnpClient;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let upnpState = {
|
|
19
|
+
mapped: false,
|
|
20
|
+
port: null,
|
|
21
|
+
externalIp: null,
|
|
22
|
+
externalUrl: null,
|
|
23
|
+
error: null,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// A UPnP mapping request can hang forever if the router never answers SSDP.
|
|
27
|
+
// Cap every call so the HTTP endpoint doesn't dangle — we surface a clean
|
|
28
|
+
// failure and the user can try tunnel mode instead.
|
|
29
|
+
const withTimeout = (promise, ms, label) =>
|
|
30
|
+
Promise.race([
|
|
31
|
+
promise,
|
|
32
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const promisifyUpnp = (method, arg) =>
|
|
36
|
+
new Promise((resolve, reject) => {
|
|
37
|
+
const client = getUpnpClient();
|
|
38
|
+
const cb = (err, result) => (err ? reject(err) : resolve(result));
|
|
39
|
+
if (arg === undefined) {
|
|
40
|
+
client[method](cb);
|
|
41
|
+
} else {
|
|
42
|
+
client[method](arg, cb);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const enableUpnp = async ({ port }) => {
|
|
47
|
+
upnpState = { ...upnpState, error: null };
|
|
48
|
+
try {
|
|
49
|
+
await withTimeout(
|
|
50
|
+
promisifyUpnp('portMapping', {
|
|
51
|
+
public: port,
|
|
52
|
+
private: port,
|
|
53
|
+
// ttl:0 is documented as "never expire" — routers honor it differently,
|
|
54
|
+
// but it's the least surprising default. We leave renewal to the user
|
|
55
|
+
// clicking "enable" again if the router drops the lease.
|
|
56
|
+
ttl: 0,
|
|
57
|
+
description: 'Pixcode',
|
|
58
|
+
protocol: 'tcp',
|
|
59
|
+
}),
|
|
60
|
+
8000,
|
|
61
|
+
'UPnP portMapping',
|
|
62
|
+
);
|
|
63
|
+
const externalIp = await withTimeout(promisifyUpnp('externalIp'), 5000, 'UPnP externalIp');
|
|
64
|
+
upnpState = {
|
|
65
|
+
mapped: true,
|
|
66
|
+
port,
|
|
67
|
+
externalIp,
|
|
68
|
+
externalUrl: externalIp ? `http://${externalIp}:${port}` : null,
|
|
69
|
+
error: null,
|
|
70
|
+
};
|
|
71
|
+
return upnpState;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
upnpState = {
|
|
74
|
+
mapped: false,
|
|
75
|
+
port,
|
|
76
|
+
externalIp: null,
|
|
77
|
+
externalUrl: null,
|
|
78
|
+
error: err?.message || String(err),
|
|
79
|
+
};
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const disableUpnp = async ({ port }) => {
|
|
85
|
+
try {
|
|
86
|
+
await withTimeout(promisifyUpnp('portUnmapping', { public: port, protocol: 'tcp' }), 5000, 'UPnP portUnmapping');
|
|
87
|
+
} catch (err) {
|
|
88
|
+
upnpState = { ...upnpState, error: err?.message || String(err) };
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
upnpState = { mapped: false, port: null, externalIp: null, externalUrl: null, error: null };
|
|
92
|
+
return upnpState;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const getUpnpState = () => upnpState;
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Tunnel: detect cloudflared / ngrok and spawn; extract the public URL from
|
|
99
|
+
// stdout. We keep a single live tunnel per process — starting a new one
|
|
100
|
+
// stops the previous one to avoid dangling child processes.
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
let tunnelProc = null;
|
|
104
|
+
let tunnelState = {
|
|
105
|
+
running: false,
|
|
106
|
+
binary: null, // 'cloudflared' | 'ngrok'
|
|
107
|
+
url: null,
|
|
108
|
+
error: null,
|
|
109
|
+
log: [],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const appendLog = (line) => {
|
|
113
|
+
// Tunnels can be noisy. Cap the tail we retain so a long-running tunnel
|
|
114
|
+
// doesn't grow the log into an OOM risk.
|
|
115
|
+
tunnelState.log.push(line);
|
|
116
|
+
if (tunnelState.log.length > 200) tunnelState.log.shift();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const detectBinary = async () => {
|
|
120
|
+
const candidates = ['cloudflared', 'ngrok'];
|
|
121
|
+
for (const name of candidates) {
|
|
122
|
+
try {
|
|
123
|
+
// `which` isn't guaranteed on Windows; we probe with `--version` instead
|
|
124
|
+
// so the same code path works on Unix and Windows Command Prompt.
|
|
125
|
+
await new Promise((resolve, reject) => {
|
|
126
|
+
const child = spawn(name, ['--version'], { stdio: 'ignore' });
|
|
127
|
+
child.on('error', reject);
|
|
128
|
+
child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`exit ${code}`))));
|
|
129
|
+
});
|
|
130
|
+
return name;
|
|
131
|
+
} catch {
|
|
132
|
+
// try next
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const cloudflareUrlRegex = /https?:\/\/[a-z0-9.-]+trycloudflare\.com/i;
|
|
139
|
+
const ngrokUrlRegex = /https?:\/\/[a-z0-9.-]+\.ngrok(-free)?\.(app|io)/i;
|
|
140
|
+
|
|
141
|
+
const buildTunnelArgs = (binary, port) => {
|
|
142
|
+
if (binary === 'cloudflared') return ['tunnel', '--url', `http://localhost:${port}`];
|
|
143
|
+
if (binary === 'ngrok') return ['http', String(port), '--log', 'stdout', '--log-format', 'logfmt'];
|
|
144
|
+
throw new Error(`Unsupported tunnel binary: ${binary}`);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const extractUrl = (binary, text) => {
|
|
148
|
+
if (binary === 'cloudflared') return text.match(cloudflareUrlRegex)?.[0] ?? null;
|
|
149
|
+
if (binary === 'ngrok') return text.match(ngrokUrlRegex)?.[0] ?? null;
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const startTunnel = async ({ port }) => {
|
|
154
|
+
if (tunnelProc) {
|
|
155
|
+
// Already running — tell the caller to stop it first rather than silently
|
|
156
|
+
// replacing, which would orphan the old child and lie about state.
|
|
157
|
+
throw new Error('Tunnel already running; stop it first');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const binary = await detectBinary();
|
|
161
|
+
if (!binary) {
|
|
162
|
+
tunnelState = { running: false, binary: null, url: null, error: 'No tunnel binary found', log: [] };
|
|
163
|
+
const err = new Error('No tunnel binary found (tried cloudflared, ngrok)');
|
|
164
|
+
err.code = 'ENOENT_TUNNEL';
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const args = buildTunnelArgs(binary, port);
|
|
169
|
+
const child = spawn(binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
170
|
+
tunnelProc = child;
|
|
171
|
+
tunnelState = { running: true, binary, url: null, error: null, log: [] };
|
|
172
|
+
|
|
173
|
+
const handleChunk = (chunk) => {
|
|
174
|
+
const text = chunk.toString();
|
|
175
|
+
text.split(/\r?\n/).filter(Boolean).forEach(appendLog);
|
|
176
|
+
if (!tunnelState.url) {
|
|
177
|
+
const url = extractUrl(binary, text);
|
|
178
|
+
if (url) tunnelState.url = url;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
child.stdout.on('data', handleChunk);
|
|
183
|
+
child.stderr.on('data', handleChunk);
|
|
184
|
+
child.on('exit', (code) => {
|
|
185
|
+
tunnelProc = null;
|
|
186
|
+
tunnelState = {
|
|
187
|
+
running: false,
|
|
188
|
+
binary,
|
|
189
|
+
url: null,
|
|
190
|
+
error: code === 0 ? null : `Tunnel exited with code ${code}`,
|
|
191
|
+
log: tunnelState.log,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Wait up to 15s for the public URL to appear in the log. We don't block
|
|
196
|
+
// indefinitely — if the binary is hanging on login/auth, the UI should see
|
|
197
|
+
// a clear failure instead of a spinner that never resolves.
|
|
198
|
+
const start = Date.now();
|
|
199
|
+
while (Date.now() - start < 15000) {
|
|
200
|
+
if (tunnelState.url) return tunnelState;
|
|
201
|
+
if (!tunnelProc) break; // process died early
|
|
202
|
+
// eslint-disable-next-line no-await-in-loop
|
|
203
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!tunnelState.url) {
|
|
207
|
+
// If we never captured a URL, kill the child so we don't leak it.
|
|
208
|
+
try { child.kill(); } catch { /* ignore */ }
|
|
209
|
+
tunnelProc = null;
|
|
210
|
+
tunnelState = { ...tunnelState, running: false, error: 'Tunnel did not report a public URL' };
|
|
211
|
+
throw new Error(tunnelState.error);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return tunnelState;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const stopTunnel = async () => {
|
|
218
|
+
if (!tunnelProc) {
|
|
219
|
+
tunnelState = { running: false, binary: null, url: null, error: null, log: [] };
|
|
220
|
+
return tunnelState;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
tunnelProc.kill();
|
|
224
|
+
} catch {
|
|
225
|
+
// already dead
|
|
226
|
+
}
|
|
227
|
+
tunnelProc = null;
|
|
228
|
+
tunnelState = { running: false, binary: null, url: null, error: null, log: [] };
|
|
229
|
+
return tunnelState;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const getTunnelState = () => tunnelState;
|
|
233
|
+
|
|
234
|
+
// Explicit cleanup so the server process can shut down without leaking the
|
|
235
|
+
// child tunnel process.
|
|
236
|
+
process.on('exit', () => {
|
|
237
|
+
if (tunnelProc) {
|
|
238
|
+
try { tunnelProc.kill(); } catch { /* ignore */ }
|
|
239
|
+
}
|
|
240
|
+
});
|