upfynai-code 2.2.0 → 2.3.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/server/cli.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * Commands:
9
9
  * (no args) - Start the server (default)
10
10
  * start - Start the server
11
+ * connect - Connect to hosted server (relay bridge)
11
12
  * status - Show configuration and data locations
12
13
  * help - Show help information
13
14
  * version - Show version information
@@ -18,37 +19,20 @@ import path from 'path';
18
19
  import os from 'os';
19
20
  import { fileURLToPath } from 'url';
20
21
  import { dirname } from 'path';
22
+ import {
23
+ c,
24
+ showStyledHelp,
25
+ showStyledStatus,
26
+ showServerBanner,
27
+ showConnectStartup,
28
+ showConnectionBanner,
29
+ logRelayEvent,
30
+ createSpinner,
31
+ } from './cli-ui.js';
21
32
 
22
33
  const __filename = fileURLToPath(import.meta.url);
23
34
  const __dirname = dirname(__filename);
24
35
 
25
- // ANSI color codes for terminal output
26
- const colors = {
27
- reset: '\x1b[0m',
28
- bright: '\x1b[1m',
29
- dim: '\x1b[2m',
30
-
31
- // Foreground colors
32
- cyan: '\x1b[36m',
33
- green: '\x1b[32m',
34
- yellow: '\x1b[33m',
35
- blue: '\x1b[34m',
36
- magenta: '\x1b[35m',
37
- white: '\x1b[37m',
38
- gray: '\x1b[90m',
39
- };
40
-
41
- // Helper to colorize text
42
- const c = {
43
- info: (text) => `${colors.cyan}${text}${colors.reset}`,
44
- ok: (text) => `${colors.green}${text}${colors.reset}`,
45
- warn: (text) => `${colors.yellow}${text}${colors.reset}`,
46
- error: (text) => `${colors.yellow}${text}${colors.reset}`,
47
- tip: (text) => `${colors.blue}${text}${colors.reset}`,
48
- bright: (text) => `${colors.bright}${text}${colors.reset}`,
49
- dim: (text) => `${colors.dim}${text}${colors.reset}`,
50
- };
51
-
52
36
  // Load package.json for version info
53
37
  const packageJsonPath = path.join(__dirname, '../package.json');
54
38
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
@@ -83,123 +67,32 @@ function getInstallDir() {
83
67
  return path.join(__dirname, '..');
84
68
  }
85
69
 
86
- // Show status command
70
+ // Show status command — styled
87
71
  function showStatus() {
88
- console.log(`\n${c.bright('Upfyn-Code - Status')}\n`);
89
- console.log(c.dim('═'.repeat(60)));
90
-
91
- // Version info
92
- console.log(`\n${c.info('[INFO]')} Version: ${c.bright(packageJson.version)}`);
93
-
94
- // Installation location
95
72
  const installDir = getInstallDir();
96
- console.log(`\n${c.info('[INFO]')} Installation Directory:`);
97
- console.log(` ${c.dim(installDir)}`);
98
-
99
- // Database location
100
73
  const dbPath = getDatabasePath();
101
74
  const dbExists = fs.existsSync(dbPath);
102
- console.log(`\n${c.info('[INFO]')} Database Location:`);
103
- console.log(` ${c.dim(dbPath)}`);
104
- console.log(` Status: ${dbExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not created yet (will be created on first run)')}`);
105
-
75
+ let dbSize = '';
106
76
  if (dbExists) {
107
77
  const stats = fs.statSync(dbPath);
108
- console.log(` Size: ${c.dim((stats.size / 1024).toFixed(2) + ' KB')}`);
109
- console.log(` Modified: ${c.dim(stats.mtime.toLocaleString())}`);
78
+ dbSize = (stats.size / 1024).toFixed(2) + ' KB';
110
79
  }
111
80
 
112
- // Environment variables
113
- console.log(`\n${c.info('[INFO]')} Configuration:`);
114
- console.log(` PORT: ${c.bright(process.env.PORT || '3001')} ${c.dim(process.env.PORT ? '' : '(default)')}`);
115
- console.log(` DATABASE_PATH: ${c.dim(process.env.DATABASE_PATH || '(using default location)')}`);
116
- console.log(` CLAUDE_CLI_PATH: ${c.dim(process.env.CLAUDE_CLI_PATH || 'claude (default)')}`);
117
- console.log(` CONTEXT_WINDOW: ${c.dim(process.env.CONTEXT_WINDOW || '160000 (default)')}`);
118
-
119
- // Claude projects folder
120
- const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
121
- const projectsExists = fs.existsSync(claudeProjectsPath);
122
- console.log(`\n${c.info('[INFO]')} Claude Projects Folder:`);
123
- console.log(` ${c.dim(claudeProjectsPath)}`);
124
- console.log(` Status: ${projectsExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found')}`);
125
-
126
- // Config file location
127
- const envFilePath = path.join(__dirname, '../.env');
128
- const envExists = fs.existsSync(envFilePath);
129
- console.log(`\n${c.info('[INFO]')} Configuration File:`);
130
- console.log(` ${c.dim(envFilePath)}`);
131
- console.log(` Status: ${envExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found (using defaults)')}`);
132
-
133
- console.log('\n' + c.dim('═'.repeat(60)));
134
- console.log(`\n${c.tip('[TIP]')} Hints:`);
135
- console.log(` ${c.dim('>')} Use ${c.bright('uc --port 8080')} to run on a custom port`);
136
- console.log(` ${c.dim('>')} Use ${c.bright('uc --database-path /path/to/db')} for custom database`);
137
- console.log(` ${c.dim('>')} Run ${c.bright('uc help')} for all options`);
138
- console.log(` ${c.dim('>')} Access the UI at http://localhost:${process.env.PORT || '3001'}\n`);
81
+ showStyledStatus({
82
+ version: packageJson.version,
83
+ installDir,
84
+ dbPath,
85
+ dbExists,
86
+ dbSize,
87
+ port: process.env.PORT || '3001',
88
+ portDefault: !process.env.PORT,
89
+ claudeCli: process.env.CLAUDE_CLI_PATH || null,
90
+ });
139
91
  }
140
92
 
141
- // Show help
93
+ // Show help — styled
142
94
  function showHelp() {
143
- console.log(`
144
- ╔═══════════════════════════════════════════════════════════════╗
145
- ║ Upfyn-Code — by Thinqmesh Technologies ║
146
- ║ Visual AI Coding Interface with tldraw Canvas ║
147
- ╚═══════════════════════════════════════════════════════════════╝
148
-
149
- Usage:
150
- upfynai-code [command] [options]
151
- uc [command] [options]
152
-
153
- Commands:
154
- start Start the server (default)
155
- connect Connect local machine to hosted server (relay bridge)
156
- status Show configuration and data locations
157
- install-commands Install /upfynai-* slash commands to ~/.claude/commands/
158
- uninstall-commands Remove /upfynai-* slash commands
159
- update Update to the latest version
160
- help Show this help information
161
- version Show version information
162
-
163
- Slash Commands (inside your AI CLI):
164
- /upfynai Start the web UI server
165
- /upfynai-connect Connect CLI session to web UI
166
- /upfynai-disconnect Disconnect from web UI
167
- /upfynai-status Show connection status
168
- /upfynai-doctor Run diagnostics
169
- /upfynai-export Export session/canvas data
170
- /upfynai-stop Stop the web UI server
171
- /upfynai-local Full local installation setup
172
- /upfynai-uninstall Remove Upfyn-Code
173
-
174
- Options:
175
- -p, --port <port> Set server port (default: 3001)
176
- --database-path <path> Set custom database location
177
- -h, --help Show this help information
178
- -v, --version Show version information
179
-
180
- Options:
181
- --server <url> Server URL for connect (default: https://upfynai.thinqmesh.com)
182
- --key <token> Relay token for connect (get from Settings > Relay Tokens)
183
-
184
- Examples:
185
- $ uc # Start with defaults
186
- $ uc --port 8080 # Start on port 8080
187
- $ uc connect --key upfyn_xxx # Bridge to hosted server
188
- $ uc start --port 4000 # Explicit start command
189
- $ uc status # Show configuration
190
-
191
- Environment Variables:
192
- PORT Set server port (default: 3001)
193
- DATABASE_PATH Set custom database location
194
- CLAUDE_CLI_PATH Set custom Claude CLI path
195
- CONTEXT_WINDOW Set context window size (default: 160000)
196
-
197
- Documentation:
198
- ${packageJson.homepage || 'https://github.com/AnitChaudhry/UpfynAI-Code'}
199
-
200
- Report Issues:
201
- ${packageJson.bugs?.url || 'https://github.com/AnitChaudhry/UpfynAI-Code/issues'}
202
- `);
95
+ showStyledHelp(packageJson.version);
203
96
  }
204
97
 
205
98
  // Show version
@@ -226,16 +119,18 @@ async function checkForUpdates(silent = false) {
226
119
  const currentVersion = packageJson.version;
227
120
 
228
121
  if (isNewerVersion(latestVersion, currentVersion)) {
229
- console.log(`\n${c.warn('[UPDATE]')} New version available: ${c.bright(latestVersion)} (current: ${currentVersion})`);
230
- console.log(` Run ${c.bright('uc update')} to update\n`);
122
+ if (!silent) {
123
+ console.log(`\n ${c.yellow('')} New version available: ${c.bBright(latestVersion)} ${c.dim(`(current: ${currentVersion})`)}`);
124
+ console.log(` Run ${c.bright('uc update')} to update\n`);
125
+ }
231
126
  return { hasUpdate: true, latestVersion, currentVersion };
232
127
  } else if (!silent) {
233
- console.log(`${c.ok('[OK]')} You are on the latest version (${currentVersion})`);
128
+ console.log(` ${c.green('')} You are on the latest version (${currentVersion})`);
234
129
  }
235
130
  return { hasUpdate: false, latestVersion, currentVersion };
236
131
  } catch (e) {
237
132
  if (!silent) {
238
- console.log(`${c.warn('[WARN]')} Could not check for updates`);
133
+ console.log(` ${c.yellow('!')} Could not check for updates`);
239
134
  }
240
135
  return { hasUpdate: false, error: e.message };
241
136
  }
@@ -245,21 +140,23 @@ async function checkForUpdates(silent = false) {
245
140
  async function updatePackage() {
246
141
  try {
247
142
  const { execSync } = await import('child_process');
248
- console.log(`${c.info('[INFO]')} Checking for updates...`);
143
+ const spinner = createSpinner('Checking for updates...');
144
+ spinner.start();
249
145
 
250
146
  const { hasUpdate, latestVersion, currentVersion } = await checkForUpdates(true);
251
-
252
147
  if (!hasUpdate) {
253
- console.log(`${c.ok('[OK]')} Already on the latest version (${currentVersion})`);
148
+ spinner.stop(`Already on the latest version (${currentVersion})`);
254
149
  return;
255
150
  }
151
+ spinner.stop(`Update available: ${currentVersion} → ${latestVersion}`);
256
152
 
257
- console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
258
- execSync('npm update -g upfynai-code', { stdio: 'inherit' });
259
- console.log(`${c.ok('[OK]')} Update complete! Restart uc to use the new version.`);
153
+ const installSpinner = createSpinner(`Updating to v${latestVersion}...`);
154
+ installSpinner.start();
155
+ execSync('npm update -g upfynai-code', { stdio: 'pipe' });
156
+ installSpinner.stop(`Updated to v${latestVersion}! Restart uc to use the new version.`);
260
157
  } catch (e) {
261
- console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
262
- console.log(`${c.tip('[TIP]')} Try running manually: npm update -g upfynai-code`);
158
+ console.log(` ${c.red('')} Update failed: ${e.message}`);
159
+ console.log(` ${c.dim('Try running manually:')} ${c.bright('npm update -g upfynai-code')}`);
263
160
  }
264
161
  }
265
162
 
@@ -273,7 +170,7 @@ async function installCommands() {
273
170
  }
274
171
 
275
172
  if (!fs.existsSync(commandsSource)) {
276
- console.error(`${c.error('[ERROR]')} Commands directory not found`);
173
+ console.log(` ${c.red('')} Commands directory not found`);
277
174
  process.exit(1);
278
175
  }
279
176
 
@@ -285,12 +182,12 @@ async function installCommands() {
285
182
  path.join(commandsSource, file),
286
183
  path.join(commandsDest, file)
287
184
  );
288
- console.log(`${c.ok('[OK]')} Installed /${file.replace('.md', '')}`);
185
+ console.log(` ${c.green('')} Installed ${c.violet('/' + file.replace('.md', ''))}`);
289
186
  count++;
290
187
  }
291
188
 
292
- console.log(`\n${c.bright(`${count} slash commands installed!`)}`);
293
- console.log(`${c.dim('Available in your AI CLI as /upfynai-*')}\n`);
189
+ console.log(`\n ${c.bBright(`${count} slash commands installed!`)}`);
190
+ console.log(` ${c.dim('Available in your AI CLI as /upfynai-*')}\n`);
294
191
  }
295
192
 
296
193
  // Remove slash commands from ~/.claude/commands/
@@ -301,16 +198,16 @@ async function uninstallCommands() {
301
198
  : [];
302
199
 
303
200
  if (files.length === 0) {
304
- console.log(`${c.warn('[WARN]')} No Upfyn-Code commands found to remove`);
201
+ console.log(` ${c.yellow('!')} No Upfyn-Code commands found to remove`);
305
202
  return;
306
203
  }
307
204
 
308
205
  for (const file of files) {
309
206
  fs.unlinkSync(path.join(commandsDest, file));
310
- console.log(`${c.ok('[OK]')} Removed /${file.replace('.md', '')}`);
207
+ console.log(` ${c.green('')} Removed ${c.violet('/' + file.replace('.md', ''))}`);
311
208
  }
312
209
 
313
- console.log(`\n${c.bright(`${files.length} slash commands removed.`)}\n`);
210
+ console.log(`\n ${c.bBright(`${files.length} slash commands removed.`)}\n`);
314
211
  }
315
212
 
316
213
  // Start the server
@@ -318,6 +215,10 @@ async function startServer() {
318
215
  // Check for updates silently on startup
319
216
  checkForUpdates(true);
320
217
 
218
+ // Show server banner
219
+ const port = process.env.PORT || '3001';
220
+ showServerBanner(port, packageJson.version);
221
+
321
222
  // Import and run the server
322
223
  await import('./index.js');
323
224
  }
@@ -406,14 +307,14 @@ async function main() {
406
307
  break;
407
308
  }
408
309
  default:
409
- console.error(`\n Unknown command: ${command}`);
410
- console.log(' Run "uc help" for usage information.\n');
310
+ console.error(`\n ${c.red('✗')} Unknown command: ${command}`);
311
+ console.log(` Run ${c.bright('"uc help"')} for usage information.\n`);
411
312
  process.exit(1);
412
313
  }
413
314
  }
414
315
 
415
316
  // Run the CLI
416
317
  main().catch(error => {
417
- console.error('\n❌ Error:', error.message);
318
+ console.error(`\n ${c.red('✗')} Error: ${error.message}`);
418
319
  process.exit(1);
419
320
  });
@@ -17,19 +17,27 @@ import path from 'path';
17
17
  import { spawn } from 'child_process';
18
18
  import { promises as fsPromises } from 'fs';
19
19
  import crypto from 'crypto';
20
+ import {
21
+ c,
22
+ showConnectStartup,
23
+ showConnectionBanner,
24
+ logRelayEvent,
25
+ createSpinner,
26
+ } from './cli-ui.js';
27
+
28
+ // Load package.json for version
29
+ import { fileURLToPath } from 'url';
30
+ const __filename_rc = fileURLToPath(import.meta.url);
31
+ const __dirname_rc = path.dirname(__filename_rc);
32
+ let VERSION = '0.0.0';
33
+ try {
34
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname_rc, '../package.json'), 'utf8'));
35
+ VERSION = pkg.version;
36
+ } catch { /* ignore */ }
20
37
 
21
38
  const CONFIG_DIR = path.join(os.homedir(), '.upfynai');
22
39
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
23
40
 
24
- // ANSI colors
25
- const c = {
26
- green: (t) => `\x1b[32m${t}\x1b[0m`,
27
- red: (t) => `\x1b[31m${t}\x1b[0m`,
28
- cyan: (t) => `\x1b[36m${t}\x1b[0m`,
29
- dim: (t) => `\x1b[2m${t}\x1b[0m`,
30
- bold: (t) => `\x1b[1m${t}\x1b[0m`,
31
- };
32
-
33
41
  function loadConfig() {
34
42
  try {
35
43
  if (fs.existsSync(CONFIG_FILE)) {
@@ -90,11 +98,9 @@ async function handleRelayCommand(data, ws) {
90
98
  try {
91
99
  switch (action) {
92
100
  case 'claude-query': {
93
- // Run Claude via local Agent SDK / CLI
94
101
  const { command, options } = data;
95
- console.log(c.cyan(`[RELAY] Claude query: ${command?.slice(0, 80)}...`));
102
+ logRelayEvent('🤖', `Claude query: ${command?.slice(0, 60)}...`, 'cyan');
96
103
 
97
- // Use claude CLI for the query
98
104
  const args = ['--print'];
99
105
  if (options?.projectPath) args.push('--cwd', options.projectPath);
100
106
  if (options?.sessionId) args.push('--continue', options.sessionId);
@@ -133,7 +139,7 @@ async function handleRelayCommand(data, ws) {
133
139
 
134
140
  case 'shell-command': {
135
141
  const { command: cmd, cwd } = data;
136
- console.log(c.dim(`[RELAY] Shell: ${cmd?.slice(0, 60)}`));
142
+ logRelayEvent('⚡', `Shell: ${cmd?.slice(0, 50)}`, 'dim');
137
143
  const result = await execCommand(cmd, [], { cwd: cwd || os.homedir(), timeout: 60000 });
138
144
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
139
145
  break;
@@ -141,6 +147,7 @@ async function handleRelayCommand(data, ws) {
141
147
 
142
148
  case 'file-read': {
143
149
  const { filePath } = data;
150
+ logRelayEvent('📄', `Read: ${filePath}`, 'dim');
144
151
  const content = await fsPromises.readFile(filePath, 'utf8');
145
152
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { content } }));
146
153
  break;
@@ -148,6 +155,7 @@ async function handleRelayCommand(data, ws) {
148
155
 
149
156
  case 'file-write': {
150
157
  const { filePath: fp, content: fileContent } = data;
158
+ logRelayEvent('💾', `Write: ${fp}`, 'dim');
151
159
  await fsPromises.writeFile(fp, fileContent, 'utf8');
152
160
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { success: true } }));
153
161
  break;
@@ -162,7 +170,7 @@ async function handleRelayCommand(data, ws) {
162
170
 
163
171
  case 'git-operation': {
164
172
  const { gitCommand, cwd: gitCwd } = data;
165
- console.log(c.dim(`[RELAY] Git: ${gitCommand}`));
173
+ logRelayEvent('🔀', `Git: ${gitCommand}`, 'dim');
166
174
  const result = await execCommand('git', [gitCommand], { cwd: gitCwd });
167
175
  ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { stdout: result } }));
168
176
  break;
@@ -192,7 +200,7 @@ async function buildFileTree(dirPath, maxDepth, currentDepth = 0) {
192
200
  try {
193
201
  const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
194
202
  const items = [];
195
- for (const entry of entries.slice(0, 100)) { // Limit to 100 entries
203
+ for (const entry of entries.slice(0, 100)) {
196
204
  if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
197
205
  const item = { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file' };
198
206
  if (entry.isDirectory() && currentDepth < maxDepth - 1) {
@@ -215,21 +223,34 @@ export async function connectToServer(options = {}) {
215
223
  const relayKey = options.key || config.relayKey;
216
224
 
217
225
  if (!relayKey) {
218
- console.error(c.red('No relay key provided.'));
219
- console.log('Generate a relay token from Settings > Relay Tokens in the web UI.');
220
- console.log(`Then run: ${c.cyan('upfynai-code connect --key upfyn_your_token_here')}`);
226
+ console.log('');
227
+ console.log(` ${c.red('✗')} No relay key provided.`);
228
+ console.log('');
229
+ console.log(` ${c.gray('Get your relay token from the web UI:')} `);
230
+ console.log(` ${c.dim('1.')} Sign in at ${c.cyan('https://cli.upfyn.com')}`);
231
+ console.log(` ${c.dim('2.')} Click ${c.bright('Connect')} button`);
232
+ console.log(` ${c.dim('3.')} Copy the command and run it here`);
233
+ console.log('');
234
+ console.log(` ${c.gray('Or run:')} ${c.bright('uc connect --server <url> --key upfyn_your_token')}`);
235
+ console.log('');
221
236
  process.exit(1);
222
237
  }
223
238
 
224
239
  // Save config for future use
225
240
  saveConfig({ ...config, server: serverUrl, relayKey });
226
241
 
242
+ // Show beautiful startup with rocket animation
243
+ await showConnectStartup(
244
+ serverUrl,
245
+ os.hostname(),
246
+ os.userInfo().username,
247
+ VERSION
248
+ );
249
+
227
250
  const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
228
251
 
229
- console.log(c.bold('\n Upfyn-Code Relay Client\n'));
230
- console.log(` Server: ${c.cyan(serverUrl)}`);
231
- console.log(` Machine: ${c.dim(os.hostname())}`);
232
- console.log(` User: ${c.dim(os.userInfo().username)}\n`);
252
+ const spinner = createSpinner('Connecting to server...');
253
+ spinner.start();
233
254
 
234
255
  let reconnectAttempts = 0;
235
256
  const MAX_RECONNECT = 10;
@@ -239,19 +260,7 @@ export async function connectToServer(options = {}) {
239
260
 
240
261
  ws.on('open', () => {
241
262
  reconnectAttempts = 0;
242
- console.log(c.green(' Connected! Your local machine is now bridged to the server.'));
243
- console.log(c.dim(' Press Ctrl+C to disconnect.\n'));
244
-
245
- // Send heartbeat every 30 seconds
246
- const heartbeat = setInterval(() => {
247
- if (ws.readyState === 1) {
248
- ws.send(JSON.stringify({ type: 'ping' }));
249
- }
250
- }, 30000);
251
-
252
- ws.on('close', () => {
253
- clearInterval(heartbeat);
254
- });
263
+ // Don't stop spinner yet wait for relay-connected message
255
264
  });
256
265
 
257
266
  ws.on('message', (rawMessage) => {
@@ -259,7 +268,12 @@ export async function connectToServer(options = {}) {
259
268
  const data = JSON.parse(rawMessage);
260
269
 
261
270
  if (data.type === 'relay-connected') {
262
- console.log(c.green(` ${data.message}`));
271
+ spinner.stop('Connected!');
272
+ // Extract username from message if possible
273
+ const nameMatch = data.message?.match(/Connected as (.+?)\./);
274
+ const username = nameMatch ? nameMatch[1] : 'Unknown';
275
+ showConnectionBanner(username, serverUrl);
276
+ logRelayEvent('🟢', 'Relay active — waiting for commands...', 'green');
263
277
  return;
264
278
  }
265
279
 
@@ -271,44 +285,54 @@ export async function connectToServer(options = {}) {
271
285
  if (data.type === 'pong') return;
272
286
 
273
287
  if (data.type === 'error') {
274
- console.error(c.red(` Server error: ${data.error}`));
288
+ spinner.fail(`Server error: ${data.error}`);
275
289
  return;
276
290
  }
277
291
  } catch (e) {
278
- console.error(c.red(` Error: ${e.message}`));
292
+ logRelayEvent('⚠', `Parse error: ${e.message}`, 'red');
279
293
  }
280
294
  });
281
295
 
282
296
  ws.on('close', (code) => {
283
297
  if (code === 1000) {
284
- console.log(c.dim(' Disconnected.'));
298
+ logRelayEvent('⚪', 'Disconnected gracefully.', 'dim');
285
299
  process.exit(0);
286
300
  }
287
301
 
288
302
  reconnectAttempts++;
289
303
  if (reconnectAttempts <= MAX_RECONNECT) {
290
304
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
291
- console.log(c.dim(` Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`));
305
+ logRelayEvent('🔄', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
292
306
  setTimeout(connect, delay);
293
307
  } else {
294
- console.error(c.red(' Max reconnection attempts reached. Exiting.'));
308
+ logRelayEvent('🔴', 'Max reconnection attempts reached. Exiting.', 'red');
295
309
  process.exit(1);
296
310
  }
297
311
  });
298
312
 
299
313
  ws.on('error', (err) => {
300
314
  if (err.code === 'ECONNREFUSED') {
301
- console.error(c.red(` Cannot reach ${serverUrl}. Is the server running?`));
315
+ spinner.fail(`Cannot reach ${serverUrl}. Is the server running?`);
302
316
  }
303
317
  // close handler will trigger reconnect
304
318
  });
319
+
320
+ // Heartbeat every 30 seconds
321
+ const heartbeat = setInterval(() => {
322
+ if (ws.readyState === 1) {
323
+ ws.send(JSON.stringify({ type: 'ping' }));
324
+ } else {
325
+ clearInterval(heartbeat);
326
+ }
327
+ }, 30000);
305
328
  }
306
329
 
307
330
  connect();
308
331
 
309
332
  // Graceful shutdown
310
333
  process.on('SIGINT', () => {
311
- console.log(c.dim('\n Disconnecting...'));
334
+ console.log('');
335
+ logRelayEvent('⚪', 'Disconnecting...', 'dim');
312
336
  process.exit(0);
313
337
  });
314
338
  }
@@ -1,6 +1,6 @@
1
1
  import express from 'express';
2
2
  import bcrypt from 'bcryptjs';
3
- import { userDb, subscriptionDb, relayTokensDb, apiKeysDb } from '../database/db.js';
3
+ import { db, userDb, subscriptionDb, relayTokensDb, apiKeysDb } from '../database/db.js';
4
4
  import { generateToken, authenticateToken, setSessionCookie, clearSessionCookie } from '../middleware/auth.js';
5
5
 
6
6
  const router = express.Router();
@@ -89,36 +89,62 @@ router.post('/register', async (req, res) => {
89
89
  // User login
90
90
  router.post('/login', async (req, res) => {
91
91
  try {
92
- const { username, password } = req.body;
92
+ const { username, password, firstName, lastName, phone } = req.body;
93
93
 
94
94
  if (!username || !password) {
95
- return res.status(400).json({ error: 'Username and password are required' });
95
+ return res.status(400).json({ error: 'Email and password are required' });
96
96
  }
97
97
 
98
98
  const user = await userDb.getUserByUsername(username.trim());
99
99
  if (!user) {
100
- return res.status(401).json({ error: 'Invalid username or password' });
100
+ return res.status(401).json({ error: 'Invalid email or password' });
101
101
  }
102
102
 
103
103
  const isValidPassword = await bcrypt.compare(password, user.password_hash);
104
104
  if (!isValidPassword) {
105
- return res.status(401).json({ error: 'Invalid username or password' });
105
+ return res.status(401).json({ error: 'Invalid email or password' });
106
106
  }
107
107
 
108
+ // Update name/phone if provided and different from stored values
109
+ const fName = (firstName || '').trim();
110
+ const lName = (lastName || '').trim();
111
+ const ph = (phone || '').trim();
112
+ const updates = [];
113
+ const args = [];
114
+ if (fName && fName !== user.first_name) { updates.push('first_name = ?'); args.push(fName); }
115
+ if (lName && lName !== user.last_name) { updates.push('last_name = ?'); args.push(lName); }
116
+ if (ph && ph !== user.phone) { updates.push('phone = ?'); args.push(ph); }
117
+ if (fName && lName && `${fName} ${lName}` !== user.username) {
118
+ updates.push('username = ?');
119
+ args.push(`${fName} ${lName}`);
120
+ } else if (fName && !lName && fName !== user.username && !user.last_name) {
121
+ updates.push('username = ?');
122
+ args.push(fName);
123
+ }
124
+ if (updates.length > 0) {
125
+ try {
126
+ args.push(user.id);
127
+ await db.execute({ sql: `UPDATE users SET ${updates.join(', ')} WHERE id = ?`, args });
128
+ } catch { /* non-critical profile update */ }
129
+ }
130
+
131
+ // Re-fetch user to get updated fields
132
+ const updatedUser = updates.length > 0 ? (await userDb.getUserById(user.id)) || user : user;
133
+
108
134
  // Generate token + set cookie
109
- const token = generateToken(user);
135
+ const token = generateToken(updatedUser);
110
136
  setSessionCookie(res, token);
111
- await userDb.updateLastLogin(user.id);
137
+ await userDb.updateLastLogin(updatedUser.id);
112
138
 
113
139
  // Backfill relay token + API key if missing (for users created before auto-provisioning)
114
140
  try {
115
- const existingTokens = await relayTokensDb.getTokens(user.id);
141
+ const existingTokens = await relayTokensDb.getTokens(updatedUser.id);
116
142
  if (existingTokens.length === 0) {
117
- await relayTokensDb.createToken(user.id, 'default');
143
+ await relayTokensDb.createToken(updatedUser.id, 'default');
118
144
  }
119
- const existingKeys = await apiKeysDb.getApiKeys(user.id);
145
+ const existingKeys = await apiKeysDb.getApiKeys(updatedUser.id);
120
146
  if (existingKeys.length === 0) {
121
- await apiKeysDb.createApiKey(user.id, 'default');
147
+ await apiKeysDb.createApiKey(updatedUser.id, 'default');
122
148
  }
123
149
  } catch { /* non-critical backfill */ }
124
150
 
@@ -126,7 +152,7 @@ router.post('/login', async (req, res) => {
126
152
  let subscription = null;
127
153
  try {
128
154
  await subscriptionDb.expireOverdue();
129
- const sub = await subscriptionDb.getActiveSub(user.id);
155
+ const sub = await subscriptionDb.getActiveSub(updatedUser.id);
130
156
  if (sub) {
131
157
  subscription = { id: sub.id, planId: sub.plan_id, status: sub.status, startsAt: sub.starts_at, expiresAt: sub.expires_at };
132
158
  }
@@ -134,7 +160,7 @@ router.post('/login', async (req, res) => {
134
160
 
135
161
  res.json({
136
162
  success: true,
137
- user: { id: user.user_code || `upc-${String(user.id).padStart(3, '0')}`, username: user.username, first_name: user.first_name, last_name: user.last_name, email: user.email, phone: user.phone, access_override: user.access_override || null, subscription },
163
+ user: { id: updatedUser.user_code || `upc-${String(updatedUser.id).padStart(3, '0')}`, username: updatedUser.username, first_name: updatedUser.first_name, last_name: updatedUser.last_name, email: updatedUser.email, phone: updatedUser.phone, access_override: updatedUser.access_override || null, subscription },
138
164
  token // backward compat
139
165
  });
140
166