upfyn-code 1.0.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/bin/cli.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { login, logout, status } from '../src/auth.js';
5
+ import { openHosted, startLocal } from '../src/launch.js';
6
+ import { connect } from '../src/connect.js';
7
+ import { mcp } from '../src/mcp.js';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('upfyn-code')
13
+ .description('Launch Upfyn AI coding environment from your terminal')
14
+ .version('1.0.0')
15
+ .option('--local', 'Start a local server instead of opening the hosted app')
16
+ .action(async (options) => {
17
+ if (options.local) {
18
+ await startLocal();
19
+ } else {
20
+ await openHosted();
21
+ }
22
+ });
23
+
24
+ program
25
+ .command('login')
26
+ .description('Authenticate with your Upfyn account')
27
+ .action(async () => {
28
+ await login();
29
+ });
30
+
31
+ program
32
+ .command('logout')
33
+ .description('Clear saved credentials')
34
+ .action(async () => {
35
+ await logout();
36
+ });
37
+
38
+ program
39
+ .command('status')
40
+ .description('Show current auth status and config')
41
+ .action(async () => {
42
+ await status();
43
+ });
44
+
45
+ program
46
+ .command('connect')
47
+ .description('Connect local machine to the remote server (bridges Claude Code, shell, files, git)')
48
+ .option('--server <url>', 'Server URL to connect to')
49
+ .option('--key <token>', 'Relay token (rt_xxx) — get from the web UI')
50
+ .action(async (options) => {
51
+ await connect(options);
52
+ });
53
+
54
+ program
55
+ .command('mcp')
56
+ .description('Show MCP integration config for Claude / Cursor')
57
+ .option('--server <url>', 'Server URL')
58
+ .option('--key <token>', 'Relay token')
59
+ .action(async (options) => {
60
+ await mcp(options);
61
+ });
62
+
63
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "upfyn-code",
3
+ "version": "1.0.0",
4
+ "description": "Launch Upfyn AI coding environment from your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "upfyn-code": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "dist/"
13
+ ],
14
+ "scripts": {
15
+ "start": "node bin/cli.js",
16
+ "build-frontend": "cd ../../frontend && npm run build && cp -r dist ../../packages/cli/dist"
17
+ },
18
+ "keywords": [
19
+ "upfyn",
20
+ "ai",
21
+ "coding",
22
+ "cli",
23
+ "terminal"
24
+ ],
25
+ "author": "Upfyn",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "chalk": "^5.3.0",
29
+ "commander": "^12.1.0",
30
+ "express": "^4.21.0",
31
+ "http-proxy-middleware": "^3.0.0",
32
+ "open": "^10.1.0",
33
+ "prompts": "^2.4.2",
34
+ "ws": "^8.18.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ }
39
+ }
package/src/auth.js ADDED
@@ -0,0 +1,115 @@
1
+ import prompts from 'prompts';
2
+ import chalk from 'chalk';
3
+ import { readConfig, writeConfig, clearConfig } from './config.js';
4
+
5
+ async function apiCall(path, options = {}) {
6
+ const config = readConfig();
7
+ const url = `${config.serverUrl}${path}`;
8
+ const res = await fetch(url, {
9
+ ...options,
10
+ headers: {
11
+ 'Content-Type': 'application/json',
12
+ ...(options.headers || {}),
13
+ },
14
+ });
15
+ return res;
16
+ }
17
+
18
+ export function getToken() {
19
+ const config = readConfig();
20
+ return config.token || null;
21
+ }
22
+
23
+ export async function validateToken() {
24
+ const token = getToken();
25
+ if (!token) return null;
26
+
27
+ try {
28
+ const res = await apiCall('/api/auth/user', {
29
+ headers: { Authorization: `Bearer ${token}` },
30
+ });
31
+ if (!res.ok) return null;
32
+ const data = await res.json();
33
+ return data.user || null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export async function login() {
40
+ console.log(chalk.bold('\n Upfyn Code — Login\n'));
41
+
42
+ const response = await prompts([
43
+ {
44
+ type: 'text',
45
+ name: 'username',
46
+ message: 'Username',
47
+ validate: v => (v.trim() ? true : 'Username is required'),
48
+ },
49
+ {
50
+ type: 'password',
51
+ name: 'password',
52
+ message: 'Password',
53
+ validate: v => (v ? true : 'Password is required'),
54
+ },
55
+ ], {
56
+ onCancel: () => {
57
+ console.log(chalk.dim('\n Login cancelled.\n'));
58
+ process.exit(0);
59
+ },
60
+ });
61
+
62
+ const { username, password } = response;
63
+
64
+ try {
65
+ const res = await apiCall('/api/auth/login', {
66
+ method: 'POST',
67
+ body: JSON.stringify({ username: username.trim(), password }),
68
+ });
69
+
70
+ const data = await res.json();
71
+
72
+ if (!res.ok) {
73
+ console.log(chalk.red(`\n Login failed: ${data.error || 'Unknown error'}\n`));
74
+ process.exit(1);
75
+ }
76
+
77
+ writeConfig({
78
+ token: data.token,
79
+ user: data.user,
80
+ });
81
+
82
+ const name = data.user.first_name || data.user.username;
83
+ console.log(chalk.green(`\n Logged in as ${chalk.bold(name)}!\n`));
84
+ } catch (err) {
85
+ console.log(chalk.red(`\n Connection error: ${err.message}\n`));
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ export async function logout() {
91
+ clearConfig();
92
+ console.log(chalk.green('\n Logged out. Credentials cleared.\n'));
93
+ }
94
+
95
+ export async function status() {
96
+ const config = readConfig();
97
+
98
+ if (!config.token) {
99
+ console.log(chalk.yellow('\n Not logged in. Run `upfyn-code login` to authenticate.\n'));
100
+ return;
101
+ }
102
+
103
+ console.log(chalk.dim('\n Checking session...'));
104
+ const user = await validateToken();
105
+
106
+ if (!user) {
107
+ console.log(chalk.yellow(' Session expired. Run `upfyn-code login` to re-authenticate.\n'));
108
+ return;
109
+ }
110
+
111
+ const name = user.first_name || user.username;
112
+ console.log(chalk.bold(` Logged in as: ${chalk.cyan(name)} (${chalk.dim(user.username)})`));
113
+ console.log(chalk.dim(` Server: ${config.serverUrl}`));
114
+ console.log(chalk.dim(` Local port: ${config.localPort}\n`));
115
+ }
package/src/config.js ADDED
@@ -0,0 +1,33 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join, dirname } from 'path';
4
+
5
+ const CONFIG_PATH = join(homedir(), '.upfynrc');
6
+
7
+ const DEFAULTS = {
8
+ serverUrl: 'https://upfynai.vercel.app',
9
+ localPort: 4200,
10
+ };
11
+
12
+ export function readConfig() {
13
+ try {
14
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
15
+ return { ...DEFAULTS, ...JSON.parse(raw) };
16
+ } catch {
17
+ return { ...DEFAULTS };
18
+ }
19
+ }
20
+
21
+ export function writeConfig(data) {
22
+ const existing = readConfig();
23
+ const merged = { ...existing, ...data };
24
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
25
+ writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
26
+ return merged;
27
+ }
28
+
29
+ export function clearConfig() {
30
+ writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULTS, null, 2) + '\n', 'utf-8');
31
+ }
32
+
33
+ export { CONFIG_PATH, DEFAULTS };
package/src/connect.js ADDED
@@ -0,0 +1,288 @@
1
+ import WebSocket from 'ws';
2
+ import os from 'os';
3
+ import { spawn } from 'child_process';
4
+ import { promises as fsPromises } from 'fs';
5
+ import path from 'path';
6
+ import chalk from 'chalk';
7
+ import { readConfig, writeConfig } from './config.js';
8
+ import { getToken, validateToken } from './auth.js';
9
+
10
+ /**
11
+ * Execute a shell command and return stdout
12
+ */
13
+ function execCommand(cmd, args, options = {}) {
14
+ return new Promise((resolve, reject) => {
15
+ const proc = spawn(cmd, args, {
16
+ shell: true,
17
+ cwd: options.cwd || os.homedir(),
18
+ env: { ...process.env, ...options.env },
19
+ stdio: ['pipe', 'pipe', 'pipe'],
20
+ });
21
+
22
+ let stdout = '';
23
+ let stderr = '';
24
+ proc.stdout.on('data', (d) => { stdout += d; });
25
+ proc.stderr.on('data', (d) => { stderr += d; });
26
+
27
+ const timeout = setTimeout(() => {
28
+ proc.kill();
29
+ reject(new Error('Command timed out'));
30
+ }, options.timeout || 30000);
31
+
32
+ proc.on('close', (code) => {
33
+ clearTimeout(timeout);
34
+ if (code === 0) resolve(stdout);
35
+ else reject(new Error(stderr || `Exit code ${code}`));
36
+ });
37
+
38
+ proc.on('error', (err) => {
39
+ clearTimeout(timeout);
40
+ reject(err);
41
+ });
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Build a file tree for a directory
47
+ */
48
+ async function buildFileTree(dirPath, maxDepth, currentDepth = 0) {
49
+ if (currentDepth >= maxDepth) return [];
50
+ try {
51
+ const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
52
+ const items = [];
53
+ for (const entry of entries.slice(0, 100)) {
54
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
55
+ const item = { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file' };
56
+ if (entry.isDirectory() && currentDepth < maxDepth - 1) {
57
+ item.children = await buildFileTree(path.join(dirPath, entry.name), maxDepth, currentDepth + 1);
58
+ }
59
+ items.push(item);
60
+ }
61
+ return items;
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Handle incoming relay commands from the server
69
+ */
70
+ async function handleRelayCommand(data, ws) {
71
+ const { requestId, action } = data;
72
+
73
+ try {
74
+ switch (action) {
75
+ case 'claude-query': {
76
+ const { command, options } = data;
77
+ console.log(chalk.cyan(` [relay] Claude query: ${command?.slice(0, 80)}...`));
78
+
79
+ const args = ['--print'];
80
+ if (options?.projectPath) args.push('--cwd', options.projectPath);
81
+ if (options?.sessionId) args.push('--continue', options.sessionId);
82
+
83
+ const proc = spawn('claude', [...args, command || ''], {
84
+ shell: true,
85
+ cwd: options?.projectPath || os.homedir(),
86
+ env: process.env,
87
+ });
88
+
89
+ proc.stdout.on('data', (chunk) => {
90
+ ws.send(JSON.stringify({
91
+ type: 'relay-stream',
92
+ requestId,
93
+ data: { type: 'claude-response', content: chunk.toString() },
94
+ }));
95
+ });
96
+
97
+ proc.stderr.on('data', (chunk) => {
98
+ ws.send(JSON.stringify({
99
+ type: 'relay-stream',
100
+ requestId,
101
+ data: { type: 'claude-error', content: chunk.toString() },
102
+ }));
103
+ });
104
+
105
+ proc.on('close', (code) => {
106
+ ws.send(JSON.stringify({
107
+ type: 'relay-complete',
108
+ requestId,
109
+ exitCode: code,
110
+ }));
111
+ });
112
+ break;
113
+ }
114
+
115
+ case 'shell-command': {
116
+ const { command: cmd, cwd } = data;
117
+ console.log(chalk.dim(` [relay] Shell: ${cmd?.slice(0, 60)}`));
118
+ const result = await execCommand(cmd, [], { cwd: cwd || os.homedir(), timeout: 60000 });
119
+ ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
120
+ break;
121
+ }
122
+
123
+ case 'file-read': {
124
+ const { filePath } = data;
125
+ const content = await fsPromises.readFile(filePath, 'utf8');
126
+ ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { content } }));
127
+ break;
128
+ }
129
+
130
+ case 'file-write': {
131
+ const { filePath: fp, content: fileContent } = data;
132
+ await fsPromises.writeFile(fp, fileContent, 'utf8');
133
+ ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { success: true } }));
134
+ break;
135
+ }
136
+
137
+ case 'file-tree': {
138
+ const { dirPath, depth = 3 } = data;
139
+ const tree = await buildFileTree(dirPath || os.homedir(), depth);
140
+ ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { tree } }));
141
+ break;
142
+ }
143
+
144
+ case 'git-operation': {
145
+ const { gitCommand, cwd: gitCwd } = data;
146
+ console.log(chalk.dim(` [relay] Git: ${gitCommand}`));
147
+ const result = await execCommand('git', [gitCommand], { cwd: gitCwd });
148
+ ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
149
+ break;
150
+ }
151
+
152
+ default:
153
+ ws.send(JSON.stringify({
154
+ type: 'relay-response',
155
+ requestId,
156
+ error: `Unknown action: ${action}`,
157
+ }));
158
+ }
159
+ } catch (err) {
160
+ ws.send(JSON.stringify({
161
+ type: 'relay-response',
162
+ requestId,
163
+ error: err.message,
164
+ }));
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Connect to the remote server via WebSocket relay.
170
+ * Bridges local Claude Code, shell, filesystem, and git to the web UI.
171
+ */
172
+ export async function connect(options = {}) {
173
+ const config = readConfig();
174
+ const serverUrl = options.server || config.serverUrl;
175
+ let relayKey = options.key;
176
+
177
+ // If no key provided, fetch one using the auth token
178
+ if (!relayKey) {
179
+ const token = getToken();
180
+ if (!token) {
181
+ console.log(chalk.yellow('\n No account found. Run `upfyn-code login` first.\n'));
182
+ process.exit(1);
183
+ }
184
+
185
+ console.log(chalk.dim('\n Validating session...'));
186
+ const user = await validateToken();
187
+ if (!user) {
188
+ console.log(chalk.yellow(' Session expired. Run `upfyn-code login` to re-authenticate.\n'));
189
+ process.exit(1);
190
+ }
191
+
192
+ // Fetch a relay token from the API
193
+ try {
194
+ const res = await fetch(`${serverUrl}/api/auth/connect-token`, {
195
+ headers: { Authorization: `Bearer ${token}` },
196
+ });
197
+ if (!res.ok) throw new Error('Failed to get connect token');
198
+ const data = await res.json();
199
+ relayKey = data.token;
200
+ } catch (err) {
201
+ console.log(chalk.red(`\n Could not get connection token: ${err.message}\n`));
202
+ process.exit(1);
203
+ }
204
+ }
205
+
206
+ // Save for future use
207
+ writeConfig({ relayKey });
208
+
209
+ const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
210
+
211
+ console.log(chalk.bold('\n Upfyn-Code Relay Client\n'));
212
+ console.log(` Server: ${chalk.cyan(serverUrl)}`);
213
+ console.log(` Machine: ${chalk.dim(os.hostname())}`);
214
+ console.log(` User: ${chalk.dim(os.userInfo().username)}\n`);
215
+
216
+ let reconnectAttempts = 0;
217
+ const MAX_RECONNECT = 10;
218
+
219
+ function doConnect() {
220
+ const ws = new WebSocket(wsUrl);
221
+
222
+ ws.on('open', () => {
223
+ reconnectAttempts = 0;
224
+ console.log(chalk.green(' Connected! Your local machine is now bridged to the server.'));
225
+ console.log(chalk.dim(' Claude Code is the AI brain. Press Ctrl+C to disconnect.\n'));
226
+
227
+ const heartbeat = setInterval(() => {
228
+ if (ws.readyState === WebSocket.OPEN) {
229
+ ws.send(JSON.stringify({ type: 'ping' }));
230
+ }
231
+ }, 30000);
232
+
233
+ ws.on('close', () => clearInterval(heartbeat));
234
+ });
235
+
236
+ ws.on('message', (rawMessage) => {
237
+ try {
238
+ const data = JSON.parse(rawMessage);
239
+
240
+ if (data.type === 'relay-connected') {
241
+ console.log(chalk.green(` ${data.message}`));
242
+ return;
243
+ }
244
+ if (data.type === 'relay-command') {
245
+ handleRelayCommand(data, ws);
246
+ return;
247
+ }
248
+ if (data.type === 'pong') return;
249
+ if (data.type === 'error') {
250
+ console.error(chalk.red(` Server error: ${data.error}`));
251
+ return;
252
+ }
253
+ } catch (e) {
254
+ console.error(chalk.red(` Error: ${e.message}`));
255
+ }
256
+ });
257
+
258
+ ws.on('close', (code) => {
259
+ if (code === 1000) {
260
+ console.log(chalk.dim(' Disconnected.'));
261
+ process.exit(0);
262
+ }
263
+
264
+ reconnectAttempts++;
265
+ if (reconnectAttempts <= MAX_RECONNECT) {
266
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
267
+ console.log(chalk.dim(` Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`));
268
+ setTimeout(doConnect, delay);
269
+ } else {
270
+ console.error(chalk.red(' Max reconnection attempts reached. Exiting.'));
271
+ process.exit(1);
272
+ }
273
+ });
274
+
275
+ ws.on('error', (err) => {
276
+ if (err.code === 'ECONNREFUSED') {
277
+ console.error(chalk.red(` Cannot reach ${serverUrl}. Is the server running?`));
278
+ }
279
+ });
280
+ }
281
+
282
+ doConnect();
283
+
284
+ process.on('SIGINT', () => {
285
+ console.log(chalk.dim('\n Disconnecting...'));
286
+ process.exit(0);
287
+ });
288
+ }
package/src/launch.js ADDED
@@ -0,0 +1,54 @@
1
+ import open from 'open';
2
+ import chalk from 'chalk';
3
+ import { readConfig } from './config.js';
4
+ import { getToken, validateToken } from './auth.js';
5
+ import { startServer } from './server.js';
6
+
7
+ export async function openHosted() {
8
+ const token = getToken();
9
+
10
+ if (!token) {
11
+ console.log(chalk.yellow('\n No account found. Run `upfyn-code login` first.\n'));
12
+ process.exit(1);
13
+ }
14
+
15
+ console.log(chalk.dim('\n Validating session...'));
16
+ const user = await validateToken();
17
+
18
+ if (!user) {
19
+ console.log(chalk.yellow(' Session expired. Run `upfyn-code login` to re-authenticate.\n'));
20
+ process.exit(1);
21
+ }
22
+
23
+ const config = readConfig();
24
+ const url = `${config.serverUrl}?token=${encodeURIComponent(token)}`;
25
+
26
+ const name = user.first_name || user.username;
27
+ console.log(chalk.green(` Welcome back, ${chalk.bold(name)}!`));
28
+ console.log(chalk.dim(` Opening ${config.serverUrl} ...\n`));
29
+
30
+ await open(url);
31
+ }
32
+
33
+ export async function startLocal() {
34
+ const token = getToken();
35
+
36
+ if (!token) {
37
+ console.log(chalk.yellow('\n No account found. Run `upfyn-code login` first.\n'));
38
+ process.exit(1);
39
+ }
40
+
41
+ console.log(chalk.dim('\n Validating session...'));
42
+ const user = await validateToken();
43
+
44
+ if (!user) {
45
+ console.log(chalk.yellow(' Session expired. Run `upfyn-code login` to re-authenticate.\n'));
46
+ process.exit(1);
47
+ }
48
+
49
+ const config = readConfig();
50
+ const name = user.first_name || user.username;
51
+ console.log(chalk.green(` Welcome back, ${chalk.bold(name)}!`));
52
+
53
+ await startServer(config.localPort, config.serverUrl, token);
54
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,59 @@
1
+ import chalk from 'chalk';
2
+ import { readConfig } from './config.js';
3
+ import { getToken, validateToken } from './auth.js';
4
+
5
+ /**
6
+ * Print MCP configuration for Claude / Cursor / other AI tools.
7
+ * Optionally accepts --server and --key overrides.
8
+ */
9
+ export async function mcp(options = {}) {
10
+ const config = readConfig();
11
+ const serverUrl = options.server || config.serverUrl;
12
+ let relayKey = options.key;
13
+
14
+ // If no key provided, fetch one using auth token
15
+ if (!relayKey) {
16
+ const token = getToken();
17
+ if (!token) {
18
+ console.log(chalk.yellow('\n No account found. Run `upfyn-code login` first.\n'));
19
+ process.exit(1);
20
+ }
21
+
22
+ const user = await validateToken();
23
+ if (!user) {
24
+ console.log(chalk.yellow('\n Session expired. Run `upfyn-code login` to re-authenticate.\n'));
25
+ process.exit(1);
26
+ }
27
+
28
+ try {
29
+ const res = await fetch(`${serverUrl}/api/auth/connect-token`, {
30
+ headers: { Authorization: `Bearer ${token}` },
31
+ });
32
+ if (!res.ok) throw new Error('Failed to get connect token');
33
+ const data = await res.json();
34
+ relayKey = data.token;
35
+ } catch (err) {
36
+ console.log(chalk.red(`\n Could not get connection token: ${err.message}\n`));
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ const mcpConfig = {
42
+ mcpServers: {
43
+ 'upfyn-code': {
44
+ url: `${serverUrl}/mcp`,
45
+ headers: {
46
+ Authorization: `Bearer ${relayKey}`,
47
+ },
48
+ },
49
+ },
50
+ };
51
+
52
+ console.log(chalk.bold('\n Upfyn-Code — MCP Integration\n'));
53
+ console.log(chalk.dim(' Add this to your Claude / Cursor MCP settings:\n'));
54
+ console.log(chalk.white(JSON.stringify(mcpConfig, null, 2)));
55
+ console.log('');
56
+ console.log(chalk.dim(' Or use the MCP URL directly:'));
57
+ console.log(chalk.cyan(` ${serverUrl}/mcp`));
58
+ console.log(chalk.dim(` Authorization: Bearer ${relayKey}\n`));
59
+ }
package/src/server.js ADDED
@@ -0,0 +1,55 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { dirname, join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import express from 'express';
5
+ import { createProxyMiddleware } from 'http-proxy-middleware';
6
+ import open from 'open';
7
+ import chalk from 'chalk';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const DIST_DIR = join(__dirname, '..', 'dist');
11
+
12
+ export async function startServer(port, serverUrl, token) {
13
+ if (!existsSync(DIST_DIR)) {
14
+ console.log(chalk.red('\n Error: No built frontend found at dist/.'));
15
+ console.log(chalk.dim(' Run the build-frontend script first, or use the default mode (no --local flag).\n'));
16
+ process.exit(1);
17
+ }
18
+
19
+ const app = express();
20
+
21
+ // Proxy API calls to the remote Vercel backend
22
+ app.use('/api', createProxyMiddleware({
23
+ target: serverUrl,
24
+ changeOrigin: true,
25
+ headers: {
26
+ Authorization: `Bearer ${token}`,
27
+ },
28
+ }));
29
+
30
+ // Serve the built React frontend
31
+ app.use(express.static(DIST_DIR));
32
+
33
+ // SPA fallback — serve index.html for all non-API, non-static routes
34
+ app.get('*', (req, res) => {
35
+ res.sendFile(join(DIST_DIR, 'index.html'));
36
+ });
37
+
38
+ return new Promise((resolve) => {
39
+ const server = app.listen(port, async () => {
40
+ const url = `http://localhost:${port}?token=${encodeURIComponent(token)}`;
41
+ console.log(chalk.green(`\n Local server running at ${chalk.bold(`http://localhost:${port}`)}`));
42
+ console.log(chalk.dim(` Proxying API → ${serverUrl}`));
43
+ console.log(chalk.dim(' Press Ctrl+C to stop.\n'));
44
+ await open(url);
45
+ resolve(server);
46
+ });
47
+
48
+ const shutdown = () => {
49
+ console.log(chalk.dim('\n Shutting down...'));
50
+ server.close(() => process.exit(0));
51
+ };
52
+ process.on('SIGINT', shutdown);
53
+ process.on('SIGTERM', shutdown);
54
+ });
55
+ }