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/client/dist/assets/index-BnXuHrpJ.js +523 -0
- package/client/dist/assets/index-BwxNox94.css +1 -0
- package/client/dist/index.html +66 -128
- package/client/dist/llms.txt +40 -0
- package/client/dist/manifest.json +15 -61
- package/client/dist/robots.txt +11 -0
- package/client/dist/sitemap.xml +45 -0
- package/client/dist/sw.js +55 -19
- package/package.json +1 -1
- package/server/cli-ui.js +634 -0
- package/server/cli.js +56 -155
- package/server/relay-client.js +67 -43
- package/server/routes/auth.js +39 -13
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
|
-
|
|
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
|
-
|
|
109
|
-
console.log(` Modified: ${c.dim(stats.mtime.toLocaleString())}`);
|
|
78
|
+
dbSize = (stats.size / 1024).toFixed(2) + ' KB';
|
|
110
79
|
}
|
|
111
80
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
148
|
+
spinner.stop(`Already on the latest version (${currentVersion})`);
|
|
254
149
|
return;
|
|
255
150
|
}
|
|
151
|
+
spinner.stop(`Update available: ${currentVersion} → ${latestVersion}`);
|
|
256
152
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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.
|
|
262
|
-
console.log(
|
|
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.
|
|
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(
|
|
185
|
+
console.log(` ${c.green('✓')} Installed ${c.violet('/' + file.replace('.md', ''))}`);
|
|
289
186
|
count++;
|
|
290
187
|
}
|
|
291
188
|
|
|
292
|
-
console.log(`\n${c.
|
|
293
|
-
console.log(
|
|
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(
|
|
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(
|
|
207
|
+
console.log(` ${c.green('✓')} Removed ${c.violet('/' + file.replace('.md', ''))}`);
|
|
311
208
|
}
|
|
312
209
|
|
|
313
|
-
console.log(`\n${c.
|
|
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
|
|
410
|
-
console.log(
|
|
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('
|
|
318
|
+
console.error(`\n ${c.red('✗')} Error: ${error.message}`);
|
|
418
319
|
process.exit(1);
|
|
419
320
|
});
|
package/server/relay-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|
|
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.
|
|
219
|
-
console.log('
|
|
220
|
-
console.log(
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
+
spinner.fail(`Server error: ${data.error}`);
|
|
275
289
|
return;
|
|
276
290
|
}
|
|
277
291
|
} catch (e) {
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
+
logRelayEvent('🔄', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
|
|
292
306
|
setTimeout(connect, delay);
|
|
293
307
|
} else {
|
|
294
|
-
|
|
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
|
-
|
|
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(
|
|
334
|
+
console.log('');
|
|
335
|
+
logRelayEvent('⚪', 'Disconnecting...', 'dim');
|
|
312
336
|
process.exit(0);
|
|
313
337
|
});
|
|
314
338
|
}
|
package/server/routes/auth.js
CHANGED
|
@@ -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: '
|
|
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
|
|
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
|
|
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(
|
|
135
|
+
const token = generateToken(updatedUser);
|
|
110
136
|
setSessionCookie(res, token);
|
|
111
|
-
await userDb.updateLastLogin(
|
|
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(
|
|
141
|
+
const existingTokens = await relayTokensDb.getTokens(updatedUser.id);
|
|
116
142
|
if (existingTokens.length === 0) {
|
|
117
|
-
await relayTokensDb.createToken(
|
|
143
|
+
await relayTokensDb.createToken(updatedUser.id, 'default');
|
|
118
144
|
}
|
|
119
|
-
const existingKeys = await apiKeysDb.getApiKeys(
|
|
145
|
+
const existingKeys = await apiKeysDb.getApiKeys(updatedUser.id);
|
|
120
146
|
if (existingKeys.length === 0) {
|
|
121
|
-
await apiKeysDb.createApiKey(
|
|
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(
|
|
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:
|
|
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
|
|