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,129 @@
1
+ const axios = require('axios');
2
+ const chalk = require('chalk');
3
+ const {
4
+ getApiUrl,
5
+ getAuthHeaders,
6
+ isAuthenticated,
7
+ } = require('../lib/config');
8
+ const { error, success, info, formatTable, requireArgs } = require('../lib/helpers');
9
+
10
+ async function handleSecrets(args) {
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
+ const command = args[0];
22
+
23
+ if (!command) {
24
+ console.log(`
25
+ ${chalk.bold('Usage:')} lux secrets <command> [args]
26
+
27
+ ${chalk.bold('Commands:')}
28
+ list List all org secrets
29
+ set <key> <value> Set a secret
30
+ get <key> Get secret value
31
+ delete <key> Delete a secret
32
+
33
+ ${chalk.bold('Examples:')}
34
+ lux secrets list
35
+ lux secrets set OPENAI_API_KEY sk-xxx
36
+ lux secrets get OPENAI_API_KEY
37
+ lux secrets delete OPENAI_API_KEY
38
+ `);
39
+ process.exit(0);
40
+ }
41
+
42
+ const apiUrl = getApiUrl();
43
+
44
+ try {
45
+ switch (command) {
46
+ case 'list': {
47
+ info('Loading secrets...');
48
+ const { data } = await axios.get(`${apiUrl}/api/org/secrets`, {
49
+ headers: getAuthHeaders(),
50
+ });
51
+
52
+ if (!data.secrets || data.secrets.length === 0) {
53
+ console.log('\n(No secrets found)\n');
54
+ } else {
55
+ console.log(`\nFound ${data.secrets.length} secret(s):\n`);
56
+ const formatted = data.secrets.map((s) => ({
57
+ key: s.key,
58
+ created: new Date(s.created_at).toLocaleDateString(),
59
+ updated: new Date(s.updated_at).toLocaleDateString(),
60
+ }));
61
+ formatTable(formatted);
62
+ console.log('');
63
+ }
64
+ break;
65
+ }
66
+
67
+ case 'set': {
68
+ requireArgs(args.slice(1), 2, 'lux secrets set <key> <value>');
69
+ const key = args[1];
70
+ const value = args.slice(2).join(' '); // Allow spaces in value
71
+
72
+ info(`Setting secret: ${key}`);
73
+ await axios.post(
74
+ `${apiUrl}/api/org/secrets`,
75
+ { key, value },
76
+ { headers: getAuthHeaders() }
77
+ );
78
+
79
+ success(`Secret set: ${key}`);
80
+ break;
81
+ }
82
+
83
+ case 'get': {
84
+ requireArgs(args.slice(1), 1, 'lux secrets get <key>');
85
+ const key = args[1];
86
+
87
+ info(`Getting secret: ${key}`);
88
+ const { data } = await axios.get(
89
+ `${apiUrl}/api/org/secrets/${encodeURIComponent(key)}`,
90
+ { headers: getAuthHeaders() }
91
+ );
92
+
93
+ if (!data.secret) {
94
+ error(`Secret not found: ${key}`);
95
+ }
96
+
97
+ console.log(`\n${chalk.bold(key)}:`);
98
+ console.log(data.secret.value);
99
+ console.log('');
100
+ break;
101
+ }
102
+
103
+ case 'delete': {
104
+ requireArgs(args.slice(1), 1, 'lux secrets delete <key>');
105
+ const key = args[1];
106
+
107
+ info(`Deleting secret: ${key}`);
108
+ await axios.delete(
109
+ `${apiUrl}/api/org/secrets/${encodeURIComponent(key)}`,
110
+ { headers: getAuthHeaders() }
111
+ );
112
+
113
+ success(`Secret deleted: ${key}`);
114
+ break;
115
+ }
116
+
117
+ default:
118
+ error(
119
+ `Unknown command: ${command}\n\nRun 'lux secrets' to see available commands`
120
+ );
121
+ }
122
+ } catch (err) {
123
+ const errorMessage =
124
+ err.response?.data?.error || err.message || 'Unknown error';
125
+ error(`${errorMessage}`);
126
+ }
127
+ }
128
+
129
+ module.exports = { handleSecrets };
@@ -0,0 +1,411 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const net = require('net');
4
+ const chalk = require('chalk');
5
+ const { LUX_STUDIO_DIR } = require('../lib/config');
6
+
7
+ // Path to the server registry file (written by Lux Studio Electron app)
8
+ const REGISTRY_FILE = path.join(LUX_STUDIO_DIR, 'running-servers.json');
9
+
10
+ /**
11
+ * Check if a port is actually in use (server is really running)
12
+ */
13
+ function isPortInUse(port) {
14
+ return new Promise((resolve) => {
15
+ const server = net.createServer();
16
+ server.once('error', (err) => {
17
+ if (err.code === 'EADDRINUSE') {
18
+ resolve(true); // Port is in use
19
+ } else {
20
+ resolve(false);
21
+ }
22
+ });
23
+ server.once('listening', () => {
24
+ server.close();
25
+ resolve(false); // Port is free, server not running
26
+ });
27
+ server.listen(port);
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Load the server registry and verify servers are actually running
33
+ */
34
+ async function loadRegistry(verifyPorts = false) {
35
+ if (!fs.existsSync(REGISTRY_FILE)) {
36
+ return { servers: [], updatedAt: null };
37
+ }
38
+
39
+ try {
40
+ const content = fs.readFileSync(REGISTRY_FILE, 'utf8');
41
+ const registry = JSON.parse(content);
42
+
43
+ if (verifyPorts && registry.servers && registry.servers.length > 0) {
44
+ // Verify each server is actually running by checking if port is in use
45
+ const verifiedServers = [];
46
+ for (const server of registry.servers) {
47
+ const isRunning = await isPortInUse(server.port);
48
+ if (isRunning) {
49
+ verifiedServers.push(server);
50
+ }
51
+ }
52
+
53
+ // Update registry if stale servers were found
54
+ if (verifiedServers.length !== registry.servers.length) {
55
+ registry.servers = verifiedServers;
56
+ registry.updatedAt = new Date().toISOString();
57
+ // Write cleaned registry back
58
+ try {
59
+ fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2));
60
+ } catch {
61
+ // Ignore write errors
62
+ }
63
+ }
64
+ }
65
+
66
+ return registry;
67
+ } catch (error) {
68
+ console.error(chalk.red('Error reading server registry:'), error.message);
69
+ return { servers: [], updatedAt: null };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * List all running servers (verifies ports are actually in use)
75
+ */
76
+ async function listServers() {
77
+ // Verify ports to clean up stale entries
78
+ const registry = await loadRegistry(true);
79
+
80
+ if (!registry.servers || registry.servers.length === 0) {
81
+ console.log(chalk.yellow('\nNo dev servers are currently running.'));
82
+ console.log(chalk.dim('Start a server in Lux Studio to see it here.\n'));
83
+ return;
84
+ }
85
+
86
+ console.log(chalk.cyan('\n📡 Running Dev Servers\n'));
87
+
88
+ for (const server of registry.servers) {
89
+ console.log(chalk.white(` ${server.appName || server.appId}`));
90
+ console.log(chalk.dim(` ID: ${server.appId}`));
91
+ console.log(chalk.dim(` Port: ${server.port}`));
92
+ console.log(chalk.dim(` URL: http://localhost:${server.port}`));
93
+ console.log(chalk.dim(` Started: ${formatTime(server.startedAt)}`));
94
+ if (server.logFile) {
95
+ console.log(chalk.dim(` Logs: ${server.logFile}`));
96
+ }
97
+ console.log('');
98
+ }
99
+
100
+ if (registry.updatedAt) {
101
+ console.log(chalk.dim(`Last updated: ${formatTime(registry.updatedAt)}\n`));
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get logs for a specific app
107
+ */
108
+ async function getLogs(appNameOrId, options = {}) {
109
+ // Verify ports to clean up stale entries
110
+ const registry = await loadRegistry(true);
111
+
112
+ if (!registry.servers || registry.servers.length === 0) {
113
+ console.log(chalk.yellow('\nNo dev servers are currently running.'));
114
+ console.log(chalk.dim('Start a server in Lux Studio to see logs.\n'));
115
+ return;
116
+ }
117
+
118
+ // Find the server by name or ID (case-insensitive partial match)
119
+ const searchTerm = appNameOrId.toLowerCase();
120
+ const server = registry.servers.find(s =>
121
+ s.appId.toLowerCase().includes(searchTerm) ||
122
+ (s.appName && s.appName.toLowerCase().includes(searchTerm))
123
+ );
124
+
125
+ if (!server) {
126
+ console.log(chalk.red(`\nNo running server found matching "${appNameOrId}".`));
127
+ console.log(chalk.dim('\nAvailable servers:'));
128
+ registry.servers.forEach(s => {
129
+ console.log(chalk.dim(` - ${s.appName || s.appId}`));
130
+ });
131
+ console.log('');
132
+ return;
133
+ }
134
+
135
+ if (!server.logFile || !fs.existsSync(server.logFile)) {
136
+ console.log(chalk.yellow(`\nNo log file found for ${server.appName || server.appId}`));
137
+ return;
138
+ }
139
+
140
+ // Read and display logs
141
+ const lines = options.lines || 50;
142
+ const follow = options.follow || false;
143
+
144
+ console.log(chalk.cyan(`\n📝 Logs for ${server.appName || server.appId}`));
145
+ console.log(chalk.dim(` Port: ${server.port} | Log file: ${server.logFile}\n`));
146
+ console.log(chalk.dim('─'.repeat(60)));
147
+
148
+ // Read last N lines
149
+ const content = fs.readFileSync(server.logFile, 'utf8');
150
+ const allLines = content.split('\n').filter(l => l.trim());
151
+ const displayLines = allLines.slice(-lines);
152
+
153
+ displayLines.forEach(line => {
154
+ printLogLine(line);
155
+ });
156
+
157
+ console.log(chalk.dim('─'.repeat(60)));
158
+
159
+ if (follow) {
160
+ console.log(chalk.dim('\nWatching for new logs... (Ctrl+C to stop)\n'));
161
+
162
+ // Watch the file for changes
163
+ let lastSize = fs.statSync(server.logFile).size;
164
+
165
+ const watcher = fs.watchFile(server.logFile, { interval: 500 }, (curr, prev) => {
166
+ if (curr.size > lastSize) {
167
+ // Read new content
168
+ const fd = fs.openSync(server.logFile, 'r');
169
+ const buffer = Buffer.alloc(curr.size - lastSize);
170
+ fs.readSync(fd, buffer, 0, buffer.length, lastSize);
171
+ fs.closeSync(fd);
172
+
173
+ const newContent = buffer.toString('utf8');
174
+ newContent.split('\n').filter(l => l.trim()).forEach(line => {
175
+ printLogLine(line);
176
+ });
177
+
178
+ lastSize = curr.size;
179
+ }
180
+ });
181
+
182
+ // Handle Ctrl+C
183
+ process.on('SIGINT', () => {
184
+ fs.unwatchFile(server.logFile);
185
+ console.log(chalk.dim('\n\nStopped watching logs.\n'));
186
+ process.exit(0);
187
+ });
188
+ } else {
189
+ console.log(chalk.dim(`\nShowing last ${displayLines.length} lines. Use -f to follow.\n`));
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Print a log line with appropriate coloring
195
+ */
196
+ function printLogLine(line) {
197
+ if (line.includes('error') || line.includes('Error') || line.includes('ERROR') || line.includes('❌')) {
198
+ console.log(chalk.red(line));
199
+ } else if (line.includes('warn') || line.includes('Warn') || line.includes('WARN') || line.includes('⚠️')) {
200
+ console.log(chalk.yellow(line));
201
+ } else if (line.includes('✓') || line.includes('Ready') || line.includes('success')) {
202
+ console.log(chalk.green(line));
203
+ } else if (line.includes('[stderr]')) {
204
+ console.log(chalk.yellow(line));
205
+ } else {
206
+ console.log(chalk.dim(line));
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Format a timestamp for display
212
+ */
213
+ function formatTime(isoString) {
214
+ if (!isoString) return 'Unknown';
215
+
216
+ const date = new Date(isoString);
217
+ const now = new Date();
218
+ const diff = now - date;
219
+
220
+ // Less than a minute
221
+ if (diff < 60000) {
222
+ return 'just now';
223
+ }
224
+
225
+ // Less than an hour
226
+ if (diff < 3600000) {
227
+ const mins = Math.floor(diff / 60000);
228
+ return `${mins} minute${mins === 1 ? '' : 's'} ago`;
229
+ }
230
+
231
+ // Less than a day
232
+ if (diff < 86400000) {
233
+ const hours = Math.floor(diff / 3600000);
234
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
235
+ }
236
+
237
+ // Otherwise show date
238
+ return date.toLocaleString();
239
+ }
240
+
241
+ /**
242
+ * Handle the servers command
243
+ */
244
+ async function handleServers(args = []) {
245
+ const subcommand = args[0];
246
+
247
+ if (!subcommand || subcommand === 'list' || subcommand === 'ls') {
248
+ await listServers();
249
+ return;
250
+ }
251
+
252
+ // Show help
253
+ console.log(chalk.cyan('\nUsage:'));
254
+ console.log(' lux servers List all running dev servers');
255
+ console.log(' lux servers list List all running dev servers');
256
+ console.log('');
257
+ }
258
+
259
+ /**
260
+ * Get browser console logs for a specific app
261
+ */
262
+ async function getBrowserConsoleLogs(appNameOrId, options = {}) {
263
+ const registry = await loadRegistry(true);
264
+
265
+ if (!registry.servers || registry.servers.length === 0) {
266
+ console.log(chalk.yellow('\nNo dev servers are currently running.'));
267
+ console.log(chalk.dim('Start a server in Lux Studio to see logs.\n'));
268
+ return;
269
+ }
270
+
271
+ // Find the server by name or ID (case-insensitive partial match)
272
+ const searchTerm = appNameOrId.toLowerCase();
273
+ const server = registry.servers.find(s =>
274
+ s.appId.toLowerCase().includes(searchTerm) ||
275
+ (s.appName && s.appName.toLowerCase().includes(searchTerm))
276
+ );
277
+
278
+ if (!server) {
279
+ console.log(chalk.red(`\nNo running server found matching "${appNameOrId}".`));
280
+ console.log(chalk.dim('\nAvailable servers:'));
281
+ registry.servers.forEach(s => {
282
+ console.log(chalk.dim(` - ${s.appName || s.appId}`));
283
+ });
284
+ console.log('');
285
+ return;
286
+ }
287
+
288
+ // Browser console logs are stored in a separate file
289
+ const safeAppId = server.appId.replace(/[^a-zA-Z0-9_-]/g, '_');
290
+ const consoleLogFile = path.join(LUX_STUDIO_DIR, 'logs', `${safeAppId}.console.log`);
291
+
292
+ if (!fs.existsSync(consoleLogFile)) {
293
+ console.log(chalk.yellow(`\nNo browser console logs found for ${server.appName || server.appId}`));
294
+ console.log(chalk.dim('Interact with your app in the preview to generate console logs.\n'));
295
+ return;
296
+ }
297
+
298
+ const lines = options.lines || 50;
299
+ const follow = options.follow || false;
300
+
301
+ console.log(chalk.cyan(`\n🖥️ Browser Console Logs for ${server.appName || server.appId}`));
302
+ console.log(chalk.dim(` Port: ${server.port} | Log file: ${consoleLogFile}\n`));
303
+ console.log(chalk.dim('─'.repeat(60)));
304
+
305
+ // Read last N lines
306
+ const content = fs.readFileSync(consoleLogFile, 'utf8');
307
+ const allLines = content.split('\n').filter(l => l.trim());
308
+ const displayLines = allLines.slice(-lines);
309
+
310
+ displayLines.forEach(line => {
311
+ printConsoleLine(line);
312
+ });
313
+
314
+ console.log(chalk.dim('─'.repeat(60)));
315
+
316
+ if (follow) {
317
+ console.log(chalk.dim('\nWatching for new logs... (Ctrl+C to stop)\n'));
318
+
319
+ let lastSize = fs.statSync(consoleLogFile).size;
320
+
321
+ fs.watchFile(consoleLogFile, { interval: 500 }, (curr) => {
322
+ if (curr.size > lastSize) {
323
+ const fd = fs.openSync(consoleLogFile, 'r');
324
+ const buffer = Buffer.alloc(curr.size - lastSize);
325
+ fs.readSync(fd, buffer, 0, buffer.length, lastSize);
326
+ fs.closeSync(fd);
327
+
328
+ const newContent = buffer.toString('utf8');
329
+ newContent.split('\n').filter(l => l.trim()).forEach(line => {
330
+ printConsoleLine(line);
331
+ });
332
+
333
+ lastSize = curr.size;
334
+ }
335
+ });
336
+
337
+ process.on('SIGINT', () => {
338
+ fs.unwatchFile(consoleLogFile);
339
+ console.log(chalk.dim('\n\nStopped watching logs.\n'));
340
+ process.exit(0);
341
+ });
342
+ } else {
343
+ console.log(chalk.dim(`\nShowing last ${displayLines.length} lines. Use -f to follow.\n`));
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Print a browser console log line with level-based coloring
349
+ */
350
+ function printConsoleLine(line) {
351
+ // Format: [timestamp] [LEVEL] message
352
+ if (line.includes('[ERROR]')) {
353
+ console.log(chalk.red(line));
354
+ } else if (line.includes('[WARN]')) {
355
+ console.log(chalk.yellow(line));
356
+ } else if (line.includes('[INFO]')) {
357
+ console.log(chalk.blue(line));
358
+ } else if (line.includes('[DEBUG]')) {
359
+ console.log(chalk.dim(line));
360
+ } else {
361
+ console.log(chalk.white(line));
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Handle the logs command
367
+ */
368
+ async function handleLogs(args = [], options = {}) {
369
+ if (args.length === 0) {
370
+ // No app specified - show help or list servers
371
+ const registry = await loadRegistry(true);
372
+
373
+ if (!registry.servers || registry.servers.length === 0) {
374
+ console.log(chalk.yellow('\nNo dev servers are currently running.'));
375
+ return;
376
+ }
377
+
378
+ if (registry.servers.length === 1) {
379
+ // Only one server, show its logs
380
+ if (options.console) {
381
+ await getBrowserConsoleLogs(registry.servers[0].appId, options);
382
+ } else {
383
+ await getLogs(registry.servers[0].appId, options);
384
+ }
385
+ return;
386
+ }
387
+
388
+ // Multiple servers - ask user to specify
389
+ console.log(chalk.cyan('\nMultiple servers running. Specify which app:\n'));
390
+ registry.servers.forEach(s => {
391
+ console.log(chalk.white(` lux logs ${s.appName || s.appId}`));
392
+ });
393
+ console.log('');
394
+ return;
395
+ }
396
+
397
+ const appNameOrId = args[0];
398
+ if (options.console) {
399
+ await getBrowserConsoleLogs(appNameOrId, options);
400
+ } else {
401
+ await getLogs(appNameOrId, options);
402
+ }
403
+ }
404
+
405
+ module.exports = {
406
+ handleServers,
407
+ handleLogs,
408
+ listServers,
409
+ getLogs,
410
+ getBrowserConsoleLogs,
411
+ };
@@ -0,0 +1,177 @@
1
+ const axios = require('axios');
2
+ const chalk = require('chalk');
3
+ const fs = require('fs');
4
+ const {
5
+ getApiUrl,
6
+ getAuthHeaders,
7
+ isAuthenticated,
8
+ loadInterfaceConfig,
9
+ } = require('../lib/config');
10
+ const {
11
+ error,
12
+ success,
13
+ info,
14
+ formatTable,
15
+ formatSize,
16
+ formatDate,
17
+ requireArgs,
18
+ } = require('../lib/helpers');
19
+
20
+ async function handleStorage(args) {
21
+ // Check authentication
22
+ if (!isAuthenticated()) {
23
+ console.log(
24
+ chalk.red('❌ Not authenticated. Run'),
25
+ chalk.white('lux login'),
26
+ chalk.red('first.')
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ const command = args[0];
32
+
33
+ if (!command) {
34
+ console.log(`
35
+ ${chalk.bold('Usage:')} lux storage <command> [args]
36
+
37
+ ${chalk.bold('Commands:')}
38
+ ls [prefix] List files in R2 bucket
39
+ get <key> [output-file] Download file from R2
40
+ put <key> <file> Upload file to R2
41
+ rm <key> Delete file from R2
42
+
43
+ ${chalk.bold('Examples:')}
44
+ lux storage ls workflows/
45
+ lux storage get workflows/flow_123/draft-config.json
46
+ lux storage put workflows/flow_123/config.json ./local-config.json
47
+ lux storage rm workflows/flow_123/old-config.json
48
+ `);
49
+ process.exit(0);
50
+ }
51
+
52
+ const apiUrl = getApiUrl();
53
+
54
+ try {
55
+ switch (command) {
56
+ case 'ls': {
57
+ const prefix = args[1] || '';
58
+ info(
59
+ prefix ? `Listing files with prefix: ${prefix}` : 'Listing all files...'
60
+ );
61
+
62
+ // Use files API to list files
63
+ const { data } = await axios.get(`${apiUrl}/api/files`, {
64
+ headers: getAuthHeaders(),
65
+ params: prefix ? { prefix } : {},
66
+ });
67
+
68
+ if (!data.files || data.files.length === 0) {
69
+ console.log('\n(No files found)\n');
70
+ } else {
71
+ console.log(`\nFound ${data.files.length} file(s):\n`);
72
+ const formatted = data.files.map((f) => ({
73
+ key: f.key || f.name,
74
+ size: formatSize(f.size || 0),
75
+ lastModified: formatDate(f.lastModified || f.updated_at),
76
+ }));
77
+ formatTable(formatted);
78
+ console.log('');
79
+ }
80
+ break;
81
+ }
82
+
83
+ case 'get': {
84
+ requireArgs(args.slice(1), 1, 'lux storage get <key> [output-file]');
85
+ const key = args[1];
86
+ const outputPath = args[2];
87
+
88
+ info(`Downloading: ${key}`);
89
+
90
+ // Get download URL
91
+ const { data } = await axios.get(`${apiUrl}/api/files/${encodeURIComponent(key)}`, {
92
+ headers: getAuthHeaders(),
93
+ });
94
+
95
+ if (!data.url) {
96
+ error('Failed to get download URL');
97
+ }
98
+
99
+ // Download file
100
+ const response = await axios.get(data.url, {
101
+ responseType: 'arraybuffer',
102
+ });
103
+
104
+ if (outputPath) {
105
+ fs.writeFileSync(outputPath, response.data);
106
+ success(
107
+ `Downloaded to: ${outputPath} (${formatSize(response.data.length)})`
108
+ );
109
+ } else {
110
+ console.log(response.data.toString('utf-8'));
111
+ }
112
+ break;
113
+ }
114
+
115
+ case 'put': {
116
+ requireArgs(args.slice(1), 2, 'lux storage put <key> <file>');
117
+ const key = args[1];
118
+ const filePath = args[2];
119
+
120
+ if (!fs.existsSync(filePath)) {
121
+ error(`File not found: ${filePath}`);
122
+ }
123
+
124
+ info(`Uploading: ${key}`);
125
+
126
+ const fileBuffer = fs.readFileSync(filePath);
127
+
128
+ // Get upload URL
129
+ const interfaceConfig = loadInterfaceConfig();
130
+ if (!interfaceConfig || !interfaceConfig.id) {
131
+ error('No interface found. This command requires an interface context.');
132
+ }
133
+
134
+ const { data: uploadData } = await axios.post(
135
+ `${apiUrl}/api/interfaces/${interfaceConfig.id}/presigned-urls`,
136
+ { action: 'upload' },
137
+ { headers: getAuthHeaders() }
138
+ );
139
+
140
+ // Upload to R2
141
+ await axios.put(uploadData.uploadUrl, fileBuffer, {
142
+ headers: {
143
+ 'Content-Type': 'application/octet-stream',
144
+ },
145
+ });
146
+
147
+ success(`Uploaded: ${key} (${formatSize(fileBuffer.length)})`);
148
+ break;
149
+ }
150
+
151
+ case 'rm': {
152
+ requireArgs(args.slice(1), 1, 'lux storage rm <key>');
153
+ const key = args[1];
154
+
155
+ info(`Deleting: ${key}`);
156
+
157
+ await axios.delete(`${apiUrl}/api/files/${encodeURIComponent(key)}`, {
158
+ headers: getAuthHeaders(),
159
+ });
160
+
161
+ success(`Deleted: ${key}`);
162
+ break;
163
+ }
164
+
165
+ default:
166
+ error(
167
+ `Unknown command: ${command}\n\nRun 'lux storage' to see available commands`
168
+ );
169
+ }
170
+ } catch (err) {
171
+ const errorMessage =
172
+ err.response?.data?.error || err.message || 'Unknown error';
173
+ error(`${errorMessage}`);
174
+ }
175
+ }
176
+
177
+ module.exports = { handleStorage };