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
package/commands/dev.js
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
const { spawn, execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const axios = require('axios');
|
|
8
|
+
const chokidar = require('chokidar');
|
|
9
|
+
const {
|
|
10
|
+
loadConfig,
|
|
11
|
+
loadInterfaceConfig,
|
|
12
|
+
getDashboardUrl,
|
|
13
|
+
getAuthHeaders,
|
|
14
|
+
isAuthenticated,
|
|
15
|
+
LUX_STUDIO_DIR,
|
|
16
|
+
} = require('../lib/config');
|
|
17
|
+
const { info, warn, error, success } = require('../lib/helpers');
|
|
18
|
+
const { getNodePath, getNpmPath, getNodeEnv, isBundledNodeAvailable } = require('../lib/node-helper');
|
|
19
|
+
|
|
20
|
+
// Cloudflared binary paths
|
|
21
|
+
const CLOUDFLARED_DIR = path.join(LUX_STUDIO_DIR, 'bin');
|
|
22
|
+
const CLOUDFLARED_PATH = path.join(
|
|
23
|
+
CLOUDFLARED_DIR,
|
|
24
|
+
process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared'
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Download URLs for cloudflared
|
|
28
|
+
const CLOUDFLARED_URLS = {
|
|
29
|
+
darwin: {
|
|
30
|
+
arm64:
|
|
31
|
+
'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz',
|
|
32
|
+
x64: 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz',
|
|
33
|
+
},
|
|
34
|
+
linux: {
|
|
35
|
+
arm64:
|
|
36
|
+
'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64',
|
|
37
|
+
x64: 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',
|
|
38
|
+
},
|
|
39
|
+
win32: {
|
|
40
|
+
x64: 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// State
|
|
45
|
+
let devServerProcess = null;
|
|
46
|
+
let tunnelProcess = null;
|
|
47
|
+
let syncWatcher = null;
|
|
48
|
+
let heartbeatInterval = null;
|
|
49
|
+
let tunnelUrl = null;
|
|
50
|
+
let isShuttingDown = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Main dev command
|
|
54
|
+
*/
|
|
55
|
+
async function dev(options = {}) {
|
|
56
|
+
const port = options.port || 3000;
|
|
57
|
+
const noTunnel = options.noTunnel || false;
|
|
58
|
+
const noSync = options.noSync || false;
|
|
59
|
+
|
|
60
|
+
console.log(chalk.cyan('\nš Lux Dev Server\n'));
|
|
61
|
+
|
|
62
|
+
// Check authentication
|
|
63
|
+
if (!isAuthenticated()) {
|
|
64
|
+
console.log(
|
|
65
|
+
chalk.red('ā Not authenticated. Run'),
|
|
66
|
+
chalk.white('lux login'),
|
|
67
|
+
chalk.red('first.')
|
|
68
|
+
);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check if this is a Lux interface
|
|
73
|
+
const interfaceConfig = loadInterfaceConfig();
|
|
74
|
+
if (!interfaceConfig) {
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.yellow('ā ļø No .lux/interface.json found.'),
|
|
77
|
+
chalk.dim('Run'),
|
|
78
|
+
chalk.white('lux init'),
|
|
79
|
+
chalk.dim('first, or run from an interface directory.')
|
|
80
|
+
);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for package.json
|
|
85
|
+
if (!fs.existsSync('package.json')) {
|
|
86
|
+
console.log(chalk.red('ā No package.json found in current directory.'));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Setup graceful shutdown
|
|
91
|
+
setupShutdownHandlers();
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Step 1: Check/install dependencies
|
|
95
|
+
await checkDependencies();
|
|
96
|
+
|
|
97
|
+
// Step 2: Start Next.js dev server
|
|
98
|
+
await startDevServer(port);
|
|
99
|
+
|
|
100
|
+
// Step 3: Start tunnel (unless disabled)
|
|
101
|
+
if (!noTunnel) {
|
|
102
|
+
await startTunnel(port);
|
|
103
|
+
|
|
104
|
+
// Step 4: Register tunnel with Lux cloud
|
|
105
|
+
if (tunnelUrl && interfaceConfig.id) {
|
|
106
|
+
await registerTunnel(interfaceConfig.id, tunnelUrl);
|
|
107
|
+
|
|
108
|
+
// Step 4b: Start heartbeat to keep tunnel alive
|
|
109
|
+
startHeartbeat(interfaceConfig.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Step 5: Start file sync (unless disabled)
|
|
114
|
+
if (!noSync && interfaceConfig.githubRepoUrl) {
|
|
115
|
+
startFileSync();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Print status
|
|
119
|
+
printStatus(port, noTunnel, noSync, interfaceConfig);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(chalk.red('\nā Failed to start dev server:'), err.message);
|
|
122
|
+
await cleanup();
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check and install npm dependencies if needed
|
|
129
|
+
*/
|
|
130
|
+
async function checkDependencies() {
|
|
131
|
+
const nodeModulesExists = fs.existsSync('node_modules');
|
|
132
|
+
const nextBinExists = fs.existsSync('./node_modules/.bin/next');
|
|
133
|
+
|
|
134
|
+
if (!nodeModulesExists || !nextBinExists) {
|
|
135
|
+
console.log(chalk.yellow('š¦ Installing dependencies...'));
|
|
136
|
+
try {
|
|
137
|
+
const npmPath = getNpmPath();
|
|
138
|
+
const nodeEnv = getNodeEnv();
|
|
139
|
+
|
|
140
|
+
// Use bundled npm if available
|
|
141
|
+
if (isBundledNodeAvailable()) {
|
|
142
|
+
execSync(`"${npmPath}" install`, { stdio: 'inherit', env: nodeEnv });
|
|
143
|
+
} else {
|
|
144
|
+
execSync('npm install', { stdio: 'inherit' });
|
|
145
|
+
}
|
|
146
|
+
console.log(chalk.green('ā Dependencies installed\n'));
|
|
147
|
+
} catch (err) {
|
|
148
|
+
throw new Error('npm install failed');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Start the Next.js dev server
|
|
155
|
+
*/
|
|
156
|
+
async function startDevServer(port) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
console.log(chalk.dim(`Starting Next.js on port ${port}...`));
|
|
159
|
+
|
|
160
|
+
const nextBin = './node_modules/.bin/next';
|
|
161
|
+
const nodePath = getNodePath();
|
|
162
|
+
const nodeEnv = getNodeEnv();
|
|
163
|
+
|
|
164
|
+
// Use bundled Node.js to run Next.js dev server
|
|
165
|
+
devServerProcess = spawn(nodePath, [nextBin, 'dev', '-p', String(port)], {
|
|
166
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
167
|
+
env: { ...nodeEnv, FORCE_COLOR: '1' },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
let started = false;
|
|
171
|
+
|
|
172
|
+
devServerProcess.stdout.on('data', (data) => {
|
|
173
|
+
const output = data.toString();
|
|
174
|
+
process.stdout.write(chalk.dim(output));
|
|
175
|
+
|
|
176
|
+
// Detect when server is ready
|
|
177
|
+
if (!started && (output.includes('Ready') || output.includes('started'))) {
|
|
178
|
+
started = true;
|
|
179
|
+
console.log(chalk.green(`\nā Dev server running on http://localhost:${port}\n`));
|
|
180
|
+
resolve();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
devServerProcess.stderr.on('data', (data) => {
|
|
185
|
+
const output = data.toString();
|
|
186
|
+
// Filter out noisy warnings
|
|
187
|
+
if (!output.includes('ExperimentalWarning')) {
|
|
188
|
+
process.stderr.write(chalk.yellow(output));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
devServerProcess.on('error', (err) => {
|
|
193
|
+
reject(new Error(`Failed to start dev server: ${err.message}`));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
devServerProcess.on('exit', (code) => {
|
|
197
|
+
if (!isShuttingDown && code !== 0) {
|
|
198
|
+
console.error(chalk.red(`\nDev server exited with code ${code}`));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Timeout for server start
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
if (!started) {
|
|
205
|
+
started = true;
|
|
206
|
+
console.log(chalk.yellow('\nā ļø Server may still be starting...\n'));
|
|
207
|
+
resolve();
|
|
208
|
+
}
|
|
209
|
+
}, 30000);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Download cloudflared if not present
|
|
215
|
+
*/
|
|
216
|
+
async function ensureCloudflared() {
|
|
217
|
+
if (fs.existsSync(CLOUDFLARED_PATH)) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log(chalk.dim('Downloading cloudflared tunnel client...'));
|
|
222
|
+
|
|
223
|
+
// Create bin directory
|
|
224
|
+
if (!fs.existsSync(CLOUDFLARED_DIR)) {
|
|
225
|
+
fs.mkdirSync(CLOUDFLARED_DIR, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const platform = process.platform;
|
|
229
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
230
|
+
|
|
231
|
+
const urls = CLOUDFLARED_URLS[platform];
|
|
232
|
+
if (!urls || !urls[arch]) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Unsupported platform: ${platform}/${arch}. Please install cloudflared manually.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const url = urls[arch];
|
|
239
|
+
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const downloadFile = (downloadUrl, destPath, callback) => {
|
|
242
|
+
const file = fs.createWriteStream(destPath);
|
|
243
|
+
|
|
244
|
+
https
|
|
245
|
+
.get(downloadUrl, (response) => {
|
|
246
|
+
// Handle redirects
|
|
247
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
248
|
+
downloadFile(response.headers.location, destPath, callback);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
response.pipe(file);
|
|
253
|
+
file.on('finish', () => {
|
|
254
|
+
file.close(callback);
|
|
255
|
+
});
|
|
256
|
+
})
|
|
257
|
+
.on('error', (err) => {
|
|
258
|
+
fs.unlink(destPath, () => {});
|
|
259
|
+
reject(err);
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const isTarball = url.endsWith('.tgz');
|
|
264
|
+
const downloadPath = isTarball
|
|
265
|
+
? path.join(CLOUDFLARED_DIR, 'cloudflared.tgz')
|
|
266
|
+
: CLOUDFLARED_PATH;
|
|
267
|
+
|
|
268
|
+
downloadFile(url, downloadPath, async () => {
|
|
269
|
+
try {
|
|
270
|
+
if (isTarball) {
|
|
271
|
+
// Extract tarball (macOS)
|
|
272
|
+
execSync(`tar -xzf "${downloadPath}" -C "${CLOUDFLARED_DIR}"`, {
|
|
273
|
+
stdio: 'ignore',
|
|
274
|
+
});
|
|
275
|
+
fs.unlinkSync(downloadPath);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Make executable
|
|
279
|
+
if (platform !== 'win32') {
|
|
280
|
+
fs.chmodSync(CLOUDFLARED_PATH, '755');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log(chalk.green('ā cloudflared installed\n'));
|
|
284
|
+
resolve();
|
|
285
|
+
} catch (err) {
|
|
286
|
+
reject(new Error(`Failed to setup cloudflared: ${err.message}`));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Start cloudflare tunnel
|
|
294
|
+
*/
|
|
295
|
+
async function startTunnel(port) {
|
|
296
|
+
await ensureCloudflared();
|
|
297
|
+
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
console.log(chalk.dim('Starting tunnel...'));
|
|
300
|
+
|
|
301
|
+
tunnelProcess = spawn(CLOUDFLARED_PATH, [
|
|
302
|
+
'tunnel',
|
|
303
|
+
'--url',
|
|
304
|
+
`http://localhost:${port}`,
|
|
305
|
+
'--no-autoupdate',
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
let resolved = false;
|
|
309
|
+
|
|
310
|
+
tunnelProcess.stderr.on('data', (data) => {
|
|
311
|
+
const output = data.toString();
|
|
312
|
+
|
|
313
|
+
// Extract tunnel URL from output
|
|
314
|
+
const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
315
|
+
if (urlMatch && !resolved) {
|
|
316
|
+
tunnelUrl = urlMatch[0];
|
|
317
|
+
resolved = true;
|
|
318
|
+
console.log(chalk.green(`ā Tunnel active: ${chalk.cyan(tunnelUrl)}\n`));
|
|
319
|
+
resolve();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Debug output (only show errors)
|
|
323
|
+
if (output.includes('ERR') || output.includes('error')) {
|
|
324
|
+
console.error(chalk.red(output));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
tunnelProcess.on('error', (err) => {
|
|
329
|
+
reject(new Error(`Failed to start tunnel: ${err.message}`));
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
tunnelProcess.on('exit', (code) => {
|
|
333
|
+
if (!isShuttingDown && code !== 0) {
|
|
334
|
+
console.error(chalk.red(`\nTunnel exited with code ${code}`));
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Timeout
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
if (!resolved) {
|
|
341
|
+
resolved = true;
|
|
342
|
+
console.log(chalk.yellow('\nā ļø Tunnel may still be connecting...\n'));
|
|
343
|
+
resolve();
|
|
344
|
+
}
|
|
345
|
+
}, 30000);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Register tunnel URL with Lux cloud (dashboard)
|
|
351
|
+
*/
|
|
352
|
+
async function registerTunnel(interfaceId, url) {
|
|
353
|
+
try {
|
|
354
|
+
const dashboardUrl = getDashboardUrl();
|
|
355
|
+
const headers = getAuthHeaders();
|
|
356
|
+
|
|
357
|
+
await axios.post(
|
|
358
|
+
`${dashboardUrl}/api/tunnel/register`,
|
|
359
|
+
{
|
|
360
|
+
interface_id: interfaceId,
|
|
361
|
+
tunnel_url: url,
|
|
362
|
+
},
|
|
363
|
+
{ headers }
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
console.log(chalk.green('ā Tunnel registered with Lux cloud\n'));
|
|
367
|
+
} catch (err) {
|
|
368
|
+
console.log(
|
|
369
|
+
chalk.yellow('ā ļø Could not register tunnel with cloud:'),
|
|
370
|
+
err.response?.data?.error || err.message
|
|
371
|
+
);
|
|
372
|
+
console.log(chalk.dim('Preview in dashboard may not work.\n'));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Unregister tunnel when shutting down
|
|
378
|
+
*/
|
|
379
|
+
async function unregisterTunnel(interfaceId) {
|
|
380
|
+
try {
|
|
381
|
+
const dashboardUrl = getDashboardUrl();
|
|
382
|
+
const headers = getAuthHeaders();
|
|
383
|
+
|
|
384
|
+
await axios.post(
|
|
385
|
+
`${dashboardUrl}/api/tunnel/unregister`,
|
|
386
|
+
{ interface_id: interfaceId },
|
|
387
|
+
{ headers, timeout: 3000 }
|
|
388
|
+
);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
// Ignore errors during shutdown
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Start heartbeat to keep tunnel alive
|
|
396
|
+
*/
|
|
397
|
+
function startHeartbeat(interfaceId) {
|
|
398
|
+
const dashboardUrl = getDashboardUrl();
|
|
399
|
+
const headers = getAuthHeaders();
|
|
400
|
+
|
|
401
|
+
// Send heartbeat every 30 seconds
|
|
402
|
+
heartbeatInterval = setInterval(async () => {
|
|
403
|
+
try {
|
|
404
|
+
await axios.post(
|
|
405
|
+
`${dashboardUrl}/api/tunnel/heartbeat`,
|
|
406
|
+
{ interface_id: interfaceId },
|
|
407
|
+
{ headers, timeout: 5000 }
|
|
408
|
+
);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
// Silently ignore heartbeat failures
|
|
411
|
+
// The tunnel will be marked stale after 2 minutes without heartbeat
|
|
412
|
+
}
|
|
413
|
+
}, 30000);
|
|
414
|
+
|
|
415
|
+
// Send initial heartbeat
|
|
416
|
+
axios.post(
|
|
417
|
+
`${dashboardUrl}/api/tunnel/heartbeat`,
|
|
418
|
+
{ interface_id: interfaceId },
|
|
419
|
+
{ headers, timeout: 5000 }
|
|
420
|
+
).catch(() => {});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Start file watcher for GitHub sync
|
|
425
|
+
*/
|
|
426
|
+
function startFileSync() {
|
|
427
|
+
console.log(chalk.dim('Starting file sync...\n'));
|
|
428
|
+
|
|
429
|
+
// Debounce sync operations
|
|
430
|
+
let syncTimeout = null;
|
|
431
|
+
const pendingChanges = new Set();
|
|
432
|
+
|
|
433
|
+
const doSync = () => {
|
|
434
|
+
if (pendingChanges.size === 0) return;
|
|
435
|
+
|
|
436
|
+
const changes = Array.from(pendingChanges);
|
|
437
|
+
pendingChanges.clear();
|
|
438
|
+
|
|
439
|
+
console.log(chalk.dim(`Syncing ${changes.length} file(s) to GitHub...`));
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
// Add all changes
|
|
443
|
+
execSync('git add -A', { stdio: 'ignore' });
|
|
444
|
+
|
|
445
|
+
// Check if there are changes to commit
|
|
446
|
+
const status = execSync('git status --porcelain').toString();
|
|
447
|
+
if (status.trim()) {
|
|
448
|
+
// Commit with auto message
|
|
449
|
+
const message = `Auto-sync: ${changes.slice(0, 3).join(', ')}${changes.length > 3 ? ` +${changes.length - 3} more` : ''}`;
|
|
450
|
+
execSync(`git commit -m "${message}"`, { stdio: 'ignore' });
|
|
451
|
+
|
|
452
|
+
// Push to dev branch
|
|
453
|
+
execSync('git push origin dev', { stdio: 'ignore' });
|
|
454
|
+
|
|
455
|
+
console.log(chalk.green('ā Synced to GitHub\n'));
|
|
456
|
+
}
|
|
457
|
+
} catch (err) {
|
|
458
|
+
console.log(chalk.yellow('ā ļø Sync failed:'), err.message);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Watch for file changes
|
|
463
|
+
syncWatcher = chokidar.watch('.', {
|
|
464
|
+
ignored: [
|
|
465
|
+
/node_modules/,
|
|
466
|
+
/\.next/,
|
|
467
|
+
/\.git/,
|
|
468
|
+
/\.lux/,
|
|
469
|
+
/\.DS_Store/,
|
|
470
|
+
],
|
|
471
|
+
ignoreInitial: true,
|
|
472
|
+
persistent: true,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
syncWatcher.on('all', (event, filePath) => {
|
|
476
|
+
pendingChanges.add(filePath);
|
|
477
|
+
|
|
478
|
+
// Debounce: sync after 5 seconds of no changes
|
|
479
|
+
clearTimeout(syncTimeout);
|
|
480
|
+
syncTimeout = setTimeout(doSync, 5000);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
console.log(chalk.green('ā File sync active (auto-commits to GitHub)\n'));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Print final status
|
|
488
|
+
*/
|
|
489
|
+
function printStatus(port, noTunnel, noSync, interfaceConfig) {
|
|
490
|
+
console.log(chalk.cyan('ā'.repeat(50)));
|
|
491
|
+
console.log(chalk.cyan.bold('\nš Dev Server Status\n'));
|
|
492
|
+
|
|
493
|
+
console.log(chalk.white(' Local:'), `http://localhost:${port}`);
|
|
494
|
+
|
|
495
|
+
if (!noTunnel && tunnelUrl) {
|
|
496
|
+
console.log(chalk.white(' Tunnel:'), chalk.cyan(tunnelUrl));
|
|
497
|
+
console.log(chalk.white(' Preview:'), chalk.dim('Available in Lux dashboard'));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!noSync) {
|
|
501
|
+
console.log(chalk.white(' Sync:'), chalk.green('Active'), chalk.dim('(auto-push to GitHub)'));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
console.log(chalk.white(' Interface:'), interfaceConfig.name || interfaceConfig.id);
|
|
505
|
+
|
|
506
|
+
console.log(chalk.cyan('\nā'.repeat(50)));
|
|
507
|
+
console.log(chalk.dim('\nPress Ctrl+C to stop\n'));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Setup graceful shutdown handlers
|
|
512
|
+
*/
|
|
513
|
+
function setupShutdownHandlers() {
|
|
514
|
+
const shutdown = async (signal) => {
|
|
515
|
+
if (isShuttingDown) return;
|
|
516
|
+
isShuttingDown = true;
|
|
517
|
+
|
|
518
|
+
console.log(chalk.yellow(`\n\nReceived ${signal}, shutting down...\n`));
|
|
519
|
+
|
|
520
|
+
await cleanup();
|
|
521
|
+
process.exit(0);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
525
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
526
|
+
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Cleanup all processes
|
|
531
|
+
*/
|
|
532
|
+
async function cleanup() {
|
|
533
|
+
const interfaceConfig = loadInterfaceConfig();
|
|
534
|
+
|
|
535
|
+
// Stop heartbeat
|
|
536
|
+
if (heartbeatInterval) {
|
|
537
|
+
clearInterval(heartbeatInterval);
|
|
538
|
+
console.log(chalk.dim('Stopped heartbeat'));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Unregister tunnel
|
|
542
|
+
if (interfaceConfig?.id) {
|
|
543
|
+
await unregisterTunnel(interfaceConfig.id);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Stop file watcher
|
|
547
|
+
if (syncWatcher) {
|
|
548
|
+
await syncWatcher.close();
|
|
549
|
+
console.log(chalk.dim('Stopped file sync'));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Stop tunnel
|
|
553
|
+
if (tunnelProcess) {
|
|
554
|
+
tunnelProcess.kill();
|
|
555
|
+
console.log(chalk.dim('Stopped tunnel'));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Stop dev server
|
|
559
|
+
if (devServerProcess) {
|
|
560
|
+
devServerProcess.kill();
|
|
561
|
+
console.log(chalk.dim('Stopped dev server'));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
console.log(chalk.green('\nā Shutdown complete\n'));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
module.exports = {
|
|
568
|
+
dev,
|
|
569
|
+
};
|
package/commands/init.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { saveInterfaceConfig, loadInterfaceConfig } = require('../lib/config');
|
|
5
|
+
|
|
6
|
+
async function init(options) {
|
|
7
|
+
console.log(chalk.cyan('\nš¦ Initialize Lux Interface\n'));
|
|
8
|
+
|
|
9
|
+
// Check if already initialized
|
|
10
|
+
const existing = loadInterfaceConfig();
|
|
11
|
+
if (existing) {
|
|
12
|
+
console.log(
|
|
13
|
+
chalk.yellow(
|
|
14
|
+
'ā ļø This directory is already initialized as a Lux interface.'
|
|
15
|
+
)
|
|
16
|
+
);
|
|
17
|
+
console.log(chalk.dim(`Interface: ${existing.name} (${existing.id})\n`));
|
|
18
|
+
|
|
19
|
+
const { overwrite } = await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'confirm',
|
|
22
|
+
name: 'overwrite',
|
|
23
|
+
message: 'Do you want to reinitialize?',
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
if (!overwrite) {
|
|
29
|
+
console.log(chalk.dim('Cancelled.'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get interface details
|
|
35
|
+
let answers;
|
|
36
|
+
|
|
37
|
+
// If name is provided via flags, skip interactive prompts
|
|
38
|
+
if (options.name) {
|
|
39
|
+
answers = {
|
|
40
|
+
name: options.name,
|
|
41
|
+
description: options.description || '',
|
|
42
|
+
};
|
|
43
|
+
console.log(chalk.dim(`Name: ${answers.name}`));
|
|
44
|
+
if (answers.description) {
|
|
45
|
+
console.log(chalk.dim(`Description: ${answers.description}`));
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// Interactive prompts
|
|
49
|
+
answers = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: 'input',
|
|
52
|
+
name: 'name',
|
|
53
|
+
message: 'Interface name:',
|
|
54
|
+
default: options.name || require('path').basename(process.cwd()),
|
|
55
|
+
validate: (input) => {
|
|
56
|
+
if (!input || input.trim().length === 0) {
|
|
57
|
+
return 'Interface name is required';
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'input',
|
|
64
|
+
name: 'description',
|
|
65
|
+
message: 'Description (optional):',
|
|
66
|
+
default: options.description || '',
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create interface config
|
|
72
|
+
const config = {
|
|
73
|
+
name: answers.name,
|
|
74
|
+
description: answers.description,
|
|
75
|
+
createdAt: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Save config
|
|
79
|
+
saveInterfaceConfig(config);
|
|
80
|
+
|
|
81
|
+
console.log(chalk.green('\nā Interface initialized successfully!\n'));
|
|
82
|
+
console.log(chalk.dim('Configuration saved to .lux/interface.json\n'));
|
|
83
|
+
console.log(chalk.cyan('Next steps:'));
|
|
84
|
+
console.log(
|
|
85
|
+
chalk.dim(' 1. Run'),
|
|
86
|
+
chalk.white('lux up'),
|
|
87
|
+
chalk.dim('to upload your code')
|
|
88
|
+
);
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.dim(' 2. Run'),
|
|
91
|
+
chalk.white('lux deploy'),
|
|
92
|
+
chalk.dim('to deploy to production\n')
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Create .gitignore entry if needed
|
|
96
|
+
addToGitignore();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function addToGitignore() {
|
|
100
|
+
const gitignorePath = '.gitignore';
|
|
101
|
+
const entry = '.lux/interface.json';
|
|
102
|
+
|
|
103
|
+
if (fs.existsSync(gitignorePath)) {
|
|
104
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
105
|
+
|
|
106
|
+
if (!content.includes(entry)) {
|
|
107
|
+
const { confirm } = require('inquirer').prompt([
|
|
108
|
+
{
|
|
109
|
+
type: 'confirm',
|
|
110
|
+
name: 'confirm',
|
|
111
|
+
message: 'Add .lux/interface.json to .gitignore?',
|
|
112
|
+
default: true,
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
if (confirm) {
|
|
117
|
+
fs.appendFileSync(gitignorePath, `\n# Lux CLI\n${entry}\n`);
|
|
118
|
+
console.log(chalk.dim('ā Added to .gitignore'));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
init,
|
|
126
|
+
};
|