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.
- package/LICENSE +37 -0
- package/README.md +161 -0
- package/commands/ab-tests.js +437 -0
- package/commands/agents.js +226 -0
- package/commands/data.js +966 -0
- package/commands/deploy.js +166 -0
- package/commands/dev.js +569 -0
- package/commands/init.js +126 -0
- package/commands/interface/boilerplate.js +52 -0
- package/commands/interface/git-utils.js +85 -0
- package/commands/interface/index.js +7 -0
- package/commands/interface/init.js +375 -0
- package/commands/interface/path.js +74 -0
- package/commands/interface.js +125 -0
- package/commands/knowledge.js +339 -0
- package/commands/link.js +127 -0
- package/commands/list.js +97 -0
- package/commands/login.js +247 -0
- package/commands/logout.js +19 -0
- package/commands/logs.js +182 -0
- package/commands/pricing.js +328 -0
- package/commands/project.js +704 -0
- package/commands/secrets.js +129 -0
- package/commands/servers.js +411 -0
- package/commands/storage.js +177 -0
- package/commands/up.js +211 -0
- package/commands/validate-data-lux.js +502 -0
- package/commands/voice-agents.js +1055 -0
- package/commands/webview.js +393 -0
- package/commands/workflows.js +836 -0
- package/lib/config.js +403 -0
- package/lib/helpers.js +189 -0
- package/lib/node-helper.js +120 -0
- package/lux.js +268 -0
- package/package.json +56 -0
- package/templates/next-env.d.ts +6 -0
|
@@ -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 };
|