luxlabs 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.
@@ -0,0 +1,247 @@
1
+ const express = require('express');
2
+ const open = require('open');
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const inquirer = require('inquirer');
6
+ const axios = require('axios');
7
+ const { saveConfig, getApiUrl, loadConfig } = require('../lib/config');
8
+
9
+ async function login(options) {
10
+ // Non-interactive mode with --key flag
11
+ if (options.key) {
12
+ return await manualLogin(options.key);
13
+ }
14
+
15
+ // Interactive mode - ask user how they want to authenticate
16
+ const { method } = await inquirer.prompt([
17
+ {
18
+ type: 'list',
19
+ name: 'method',
20
+ message: 'How do you want to authenticate?',
21
+ choices: [
22
+ { name: '🌐 Browser (recommended)', value: 'browser' },
23
+ { name: 'šŸ”‘ Enter API key manually', value: 'manual' },
24
+ ],
25
+ },
26
+ ]);
27
+
28
+ if (method === 'browser') {
29
+ return await browserLogin();
30
+ } else {
31
+ return await manualLogin();
32
+ }
33
+ }
34
+
35
+ async function browserLogin() {
36
+ const app = express();
37
+ const port = 8976;
38
+
39
+ console.log(chalk.cyan('\nšŸ” Authenticating with Lux...\n'));
40
+
41
+ return new Promise((resolve, reject) => {
42
+ let tokenReceived = false;
43
+
44
+ app.get('/callback', async (req, res) => {
45
+ const { token, orgId } = req.query;
46
+
47
+ if (!token || !orgId) {
48
+ res.send(`
49
+ <!DOCTYPE html>
50
+ <html>
51
+ <head>
52
+ <title>Authentication Failed</title>
53
+ <style>
54
+ body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f7fafc; }
55
+ .container { text-align: center; padding: 2rem; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
56
+ .error { color: #e53e3e; font-size: 1.5rem; margin-bottom: 1rem; }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="container">
61
+ <div class="error">āŒ Authentication Failed</div>
62
+ <p>Please try again from your terminal.</p>
63
+ </div>
64
+ </body>
65
+ </html>
66
+ `);
67
+
68
+ server.close();
69
+ reject(new Error('Authentication failed - missing token or org ID'));
70
+ return;
71
+ }
72
+
73
+ tokenReceived = true;
74
+
75
+ // Save token and org ID
76
+ const config = {
77
+ token,
78
+ orgId,
79
+ timestamp: Date.now(),
80
+ };
81
+
82
+ saveConfig(config);
83
+
84
+ res.send(`
85
+ <!DOCTYPE html>
86
+ <html>
87
+ <head>
88
+ <title>Authentication Successful</title>
89
+ <style>
90
+ body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f7fafc; }
91
+ .container { text-align: center; padding: 2rem; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
92
+ .success { color: #38a169; font-size: 1.5rem; margin-bottom: 1rem; }
93
+ .info { color: #718096; margin-top: 1rem; }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <div class="container">
98
+ <div class="success">āœ“ Authentication Successful!</div>
99
+ <p>You can now close this window and return to your terminal.</p>
100
+ <p class="info">Org ID: ${orgId}</p>
101
+ </div>
102
+ </body>
103
+ </html>
104
+ `);
105
+
106
+ server.close();
107
+ resolve(config);
108
+ });
109
+
110
+ const server = app.listen(port, async () => {
111
+ const apiUrl = getApiUrl();
112
+ const authUrl = `${apiUrl}/cli-auth?port=${port}`;
113
+
114
+ console.log(chalk.dim(`Opening browser to: ${authUrl}\n`));
115
+
116
+ try {
117
+ await open(authUrl);
118
+ console.log(chalk.yellow('ā³ Waiting for authentication...\n'));
119
+ } catch (error) {
120
+ console.log(chalk.red('Could not open browser automatically.'));
121
+ console.log(chalk.yellow(`\nPlease open this URL in your browser:\n`));
122
+ console.log(chalk.cyan(` ${authUrl}\n`));
123
+ }
124
+ });
125
+
126
+ // Timeout after 5 minutes
127
+ setTimeout(
128
+ () => {
129
+ if (!tokenReceived) {
130
+ server.close();
131
+ reject(new Error('Authentication timed out'));
132
+ }
133
+ },
134
+ 5 * 60 * 1000
135
+ );
136
+ })
137
+ .then((config) => {
138
+ console.log(chalk.green('āœ“ Successfully authenticated!\n'));
139
+ console.log(chalk.dim(`Org ID: ${config.orgId}\n`));
140
+ console.log(chalk.cyan('You can now use Lux CLI commands.\n'));
141
+ process.exit(0);
142
+ })
143
+ .catch((error) => {
144
+ console.error(chalk.red('\nāŒ Authentication failed:'), error.message);
145
+ process.exit(1);
146
+ });
147
+ }
148
+
149
+ async function manualLogin(providedKey) {
150
+ let apiKey = providedKey;
151
+
152
+ // If key not provided via flag, prompt for it
153
+ if (!apiKey) {
154
+ const { key } = await inquirer.prompt([
155
+ {
156
+ type: 'password',
157
+ name: 'key',
158
+ message: 'Enter your API key:',
159
+ mask: '*',
160
+ validate: (input) => {
161
+ if (!input || input.trim().length === 0) {
162
+ return 'API key is required';
163
+ }
164
+ if (!input.startsWith('lux_')) {
165
+ return 'Invalid API key format (should start with "lux_")';
166
+ }
167
+ return true;
168
+ },
169
+ },
170
+ ]);
171
+ apiKey = key;
172
+ }
173
+
174
+ const spinner = ora('Validating API key...').start();
175
+
176
+ try {
177
+ const apiUrl = getApiUrl();
178
+
179
+ // Validate the API key by making a test request
180
+ const response = await axios.get(`${apiUrl}/api/workflows`, {
181
+ headers: {
182
+ Authorization: `Bearer ${apiKey}`,
183
+ 'X-Org-Id': extractOrgIdFromKey(apiKey),
184
+ },
185
+ });
186
+
187
+ if (response.status === 200) {
188
+ spinner.succeed(chalk.green('āœ“ API key validated successfully!'));
189
+
190
+ // Save config
191
+ const config = {
192
+ token: apiKey,
193
+ orgId: extractOrgIdFromKey(apiKey),
194
+ timestamp: Date.now(),
195
+ };
196
+
197
+ saveConfig(config);
198
+
199
+ console.log(chalk.green('\nāœ“ Successfully authenticated!\n'));
200
+ console.log(chalk.dim(`Org ID: ${config.orgId}\n`));
201
+ console.log(chalk.cyan('You can now use Lux CLI commands.\n'));
202
+ }
203
+ } catch (error) {
204
+ spinner.fail(chalk.red('API key validation failed'));
205
+
206
+ if (error.response?.status === 401 || error.response?.status === 403) {
207
+ console.error(
208
+ chalk.red('\nāŒ Invalid or expired API key. Please check your key and try again.\n')
209
+ );
210
+ } else {
211
+ console.error(
212
+ chalk.red('\nāŒ Error:'),
213
+ error.response?.data?.error || error.message
214
+ );
215
+ }
216
+
217
+ process.exit(1);
218
+ }
219
+ }
220
+
221
+ function extractOrgIdFromKey(apiKey) {
222
+ // API keys are formatted as: lux_{orgId}_{randomHex}
223
+ const parts = apiKey.split('_');
224
+ if (parts.length >= 2) {
225
+ return parts[1];
226
+ }
227
+ throw new Error('Invalid API key format');
228
+ }
229
+
230
+ async function logout() {
231
+ const config = loadConfig();
232
+
233
+ if (!config) {
234
+ console.log(chalk.yellow('Not currently logged in.'));
235
+ return;
236
+ }
237
+
238
+ const { saveConfig } = require('../lib/config');
239
+ saveConfig({});
240
+
241
+ console.log(chalk.green('āœ“ Successfully logged out'));
242
+ }
243
+
244
+ module.exports = {
245
+ login,
246
+ logout,
247
+ };
@@ -0,0 +1,19 @@
1
+ const chalk = require('chalk');
2
+ const { loadConfig, saveConfig } = require('../lib/config');
3
+
4
+ async function logout() {
5
+ const config = loadConfig();
6
+
7
+ if (!config || !config.token) {
8
+ console.log(chalk.yellow('Not currently logged in.'));
9
+ return;
10
+ }
11
+
12
+ saveConfig({});
13
+
14
+ console.log(chalk.green('āœ“ Successfully logged out'));
15
+ }
16
+
17
+ module.exports = {
18
+ logout,
19
+ };
@@ -0,0 +1,182 @@
1
+ const axios = require('axios');
2
+ const chalk = require('chalk');
3
+ const {
4
+ loadInterfaceConfig,
5
+ getApiUrl,
6
+ getAuthHeaders,
7
+ isAuthenticated,
8
+ } = require('../lib/config');
9
+
10
+ async function logs(options) {
11
+ // Check authentication
12
+ if (!isAuthenticated()) {
13
+ console.log(
14
+ chalk.red('āŒ Not authenticated. Run'),
15
+ chalk.white('lux login'),
16
+ chalk.red('first.')
17
+ );
18
+ process.exit(1);
19
+ }
20
+
21
+ // Check if initialized
22
+ const interfaceConfig = loadInterfaceConfig();
23
+ if (!interfaceConfig || !interfaceConfig.id) {
24
+ console.log(
25
+ chalk.red('āŒ No interface found. Run'),
26
+ chalk.white('lux up'),
27
+ chalk.red('first.')
28
+ );
29
+ process.exit(1);
30
+ }
31
+
32
+ const apiUrl = getApiUrl();
33
+ const interfaceId = interfaceConfig.id;
34
+ const logType = options.type || 'runtime';
35
+
36
+ try {
37
+ if (options.follow) {
38
+ // Follow mode - poll for new logs
39
+ await followLogs(interfaceId, apiUrl, logType);
40
+ } else {
41
+ // One-time fetch
42
+ await fetchLogs(interfaceId, apiUrl, logType);
43
+ }
44
+ } catch (error) {
45
+ console.error(
46
+ chalk.red('\nāŒ Error:'),
47
+ error.response?.data?.error || error.message
48
+ );
49
+
50
+ if (error.response?.status === 401) {
51
+ console.log(
52
+ chalk.yellow('\nYour session may have expired. Try running:'),
53
+ chalk.white('lux login')
54
+ );
55
+ }
56
+
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ async function fetchLogs(interfaceId, apiUrl, logType) {
62
+ const endpoint =
63
+ logType === 'build'
64
+ ? `/api/interfaces/${interfaceId}/build-logs`
65
+ : `/api/interfaces/${interfaceId}/runtime-logs`;
66
+
67
+ const { data } = await axios.get(`${apiUrl}${endpoint}`, {
68
+ headers: getAuthHeaders(),
69
+ });
70
+
71
+ if (logType === 'build') {
72
+ displayBuildLogs(data.logs);
73
+ } else {
74
+ displayRuntimeLogs(data.logs);
75
+ }
76
+ }
77
+
78
+ async function followLogs(interfaceId, apiUrl, logType) {
79
+ console.log(
80
+ chalk.cyan(`\nšŸ“‹ Following ${logType} logs... (Ctrl+C to stop)\n`)
81
+ );
82
+
83
+ let lastTimestamp = null;
84
+
85
+ while (true) {
86
+ try {
87
+ const endpoint =
88
+ logType === 'build'
89
+ ? `/api/interfaces/${interfaceId}/build-logs`
90
+ : `/api/interfaces/${interfaceId}/runtime-logs`;
91
+
92
+ const params = lastTimestamp ? { since: lastTimestamp } : {};
93
+
94
+ const { data } = await axios.get(`${apiUrl}${endpoint}`, {
95
+ headers: getAuthHeaders(),
96
+ params,
97
+ });
98
+
99
+ if (data.logs && data.logs.length > 0) {
100
+ if (logType === 'build') {
101
+ displayBuildLogs(data.logs);
102
+ } else {
103
+ displayRuntimeLogs(data.logs);
104
+ }
105
+
106
+ // Update last timestamp
107
+ const lastLog = data.logs[data.logs.length - 1];
108
+ if (lastLog.timestamp) {
109
+ lastTimestamp = lastLog.timestamp;
110
+ }
111
+ }
112
+
113
+ // Wait 2 seconds before next poll
114
+ await new Promise((resolve) => setTimeout(resolve, 2000));
115
+ } catch (error) {
116
+ console.error(chalk.red('Error fetching logs:'), error.message);
117
+ await new Promise((resolve) => setTimeout(resolve, 5000));
118
+ }
119
+ }
120
+ }
121
+
122
+ function displayBuildLogs(logs) {
123
+ if (!logs || logs.length === 0) {
124
+ console.log(chalk.dim('No logs available yet.'));
125
+ return;
126
+ }
127
+
128
+ for (const log of logs) {
129
+ const timestamp = log.timestamp
130
+ ? new Date(log.timestamp).toLocaleTimeString()
131
+ : '';
132
+ const level = log.level || 'info';
133
+
134
+ let color = chalk.white;
135
+ if (level === 'error') color = chalk.red;
136
+ else if (level === 'warn') color = chalk.yellow;
137
+ else if (level === 'success') color = chalk.green;
138
+
139
+ console.log(
140
+ color(`[${timestamp}] ${log.message || log.text || JSON.stringify(log)}`)
141
+ );
142
+ }
143
+ }
144
+
145
+ function displayRuntimeLogs(logs) {
146
+ if (!logs || logs.length === 0) {
147
+ console.log(chalk.dim('No logs available yet.'));
148
+ return;
149
+ }
150
+
151
+ for (const log of logs) {
152
+ const timestamp = new Date(log.timestamp).toLocaleTimeString();
153
+ const level = log.level || 'log';
154
+ const message = log.message || '';
155
+
156
+ let prefix = '';
157
+ let color = chalk.white;
158
+
159
+ switch (level) {
160
+ case 'error':
161
+ prefix = 'āŒ';
162
+ color = chalk.red;
163
+ break;
164
+ case 'warn':
165
+ prefix = 'āš ļø ';
166
+ color = chalk.yellow;
167
+ break;
168
+ case 'info':
169
+ prefix = 'ā„¹ļø ';
170
+ color = chalk.cyan;
171
+ break;
172
+ default:
173
+ prefix = ' ';
174
+ }
175
+
176
+ console.log(`${chalk.dim(`[${timestamp}]`)} ${prefix}${color(message)}`);
177
+ }
178
+ }
179
+
180
+ module.exports = {
181
+ logs,
182
+ };