luxlabs 1.0.21 → 1.0.24

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.
Files changed (37) hide show
  1. package/README.md +16 -21
  2. package/commands/ab-tests.js +14 -11
  3. package/commands/agents.js +11 -11
  4. package/commands/data.js +19 -17
  5. package/commands/deploy.js +145 -82
  6. package/commands/flows.js +152 -133
  7. package/commands/interface/init.js +36 -35
  8. package/commands/interface.js +135 -10
  9. package/commands/knowledge.js +3 -3
  10. package/commands/list.js +6 -24
  11. package/commands/login.js +31 -26
  12. package/commands/logout.js +13 -4
  13. package/commands/logs.js +17 -66
  14. package/commands/project.js +74 -47
  15. package/commands/secrets.js +1 -1
  16. package/commands/servers.js +9 -113
  17. package/commands/storage.js +1 -1
  18. package/commands/tools.js +4 -4
  19. package/commands/validate-data-lux.js +5 -2
  20. package/commands/voice-agents.js +22 -18
  21. package/lib/config.js +235 -83
  22. package/lib/helpers.js +6 -4
  23. package/lux.js +4 -94
  24. package/package.json +6 -1
  25. package/templates/interface-boilerplate/components/auth/sign-in-form.tsx +41 -34
  26. package/templates/interface-boilerplate/components/auth/sign-up-form.tsx +41 -34
  27. package/templates/interface-boilerplate/components/providers/posthog-provider.tsx +41 -26
  28. package/templates/interface-boilerplate/gitignore.template +4 -0
  29. package/templates/interface-boilerplate/lib/auth.config.ts +3 -2
  30. package/templates/interface-boilerplate/lib/knowledge.ts +2 -2
  31. package/templates/interface-boilerplate/middleware.ts +14 -3
  32. package/templates/interface-boilerplate/next-env.d.ts +6 -0
  33. package/templates/interface-boilerplate/package-lock.json +432 -8
  34. package/commands/dev.js +0 -578
  35. package/commands/init.js +0 -126
  36. package/commands/link.js +0 -127
  37. package/commands/up.js +0 -211
package/commands/dev.js DELETED
@@ -1,578 +0,0 @@
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
- * Get the correct next binary path for the current platform
129
- * Points directly to Next.js CLI JS file to bypass .bin shell script wrappers (cross-platform)
130
- */
131
- function getNextBinPath() {
132
- return './node_modules/next/dist/bin/next';
133
- }
134
-
135
- /**
136
- * Check and install npm dependencies if needed
137
- */
138
- async function checkDependencies() {
139
- const nodeModulesExists = fs.existsSync('node_modules');
140
- const nextBinPath = getNextBinPath();
141
- const nextBinExists = fs.existsSync(nextBinPath);
142
-
143
- if (!nodeModulesExists || !nextBinExists) {
144
- console.log(chalk.yellow('šŸ“¦ Installing dependencies...'));
145
- try {
146
- const npmPath = getNpmPath();
147
- const nodeEnv = getNodeEnv();
148
-
149
- // Use bundled npm if available
150
- if (isBundledNodeAvailable()) {
151
- execSync(`"${npmPath}" install`, { stdio: 'inherit', env: nodeEnv });
152
- } else {
153
- execSync('npm install', { stdio: 'inherit' });
154
- }
155
- console.log(chalk.green('āœ“ Dependencies installed\n'));
156
- } catch (err) {
157
- throw new Error('npm install failed');
158
- }
159
- }
160
- }
161
-
162
- /**
163
- * Start the Next.js dev server
164
- */
165
- async function startDevServer(port) {
166
- return new Promise((resolve, reject) => {
167
- console.log(chalk.dim(`Starting Next.js on port ${port}...`));
168
-
169
- const nextBin = getNextBinPath();
170
- const nodePath = getNodePath();
171
- const nodeEnv = getNodeEnv();
172
-
173
- // Use bundled Node.js to run Next.js dev server
174
- devServerProcess = spawn(nodePath, [nextBin, 'dev', '-p', String(port)], {
175
- stdio: ['pipe', 'pipe', 'pipe'],
176
- env: { ...nodeEnv, FORCE_COLOR: '1' },
177
- });
178
-
179
- let started = false;
180
-
181
- devServerProcess.stdout.on('data', (data) => {
182
- const output = data.toString();
183
- process.stdout.write(chalk.dim(output));
184
-
185
- // Detect when server is ready
186
- if (!started && (output.includes('Ready') || output.includes('started'))) {
187
- started = true;
188
- console.log(chalk.green(`\nāœ“ Dev server running on http://localhost:${port}\n`));
189
- resolve();
190
- }
191
- });
192
-
193
- devServerProcess.stderr.on('data', (data) => {
194
- const output = data.toString();
195
- // Filter out noisy warnings
196
- if (!output.includes('ExperimentalWarning')) {
197
- process.stderr.write(chalk.yellow(output));
198
- }
199
- });
200
-
201
- devServerProcess.on('error', (err) => {
202
- reject(new Error(`Failed to start dev server: ${err.message}`));
203
- });
204
-
205
- devServerProcess.on('exit', (code) => {
206
- if (!isShuttingDown && code !== 0) {
207
- console.error(chalk.red(`\nDev server exited with code ${code}`));
208
- }
209
- });
210
-
211
- // Timeout for server start
212
- setTimeout(() => {
213
- if (!started) {
214
- started = true;
215
- console.log(chalk.yellow('\nāš ļø Server may still be starting...\n'));
216
- resolve();
217
- }
218
- }, 30000);
219
- });
220
- }
221
-
222
- /**
223
- * Download cloudflared if not present
224
- */
225
- async function ensureCloudflared() {
226
- if (fs.existsSync(CLOUDFLARED_PATH)) {
227
- return;
228
- }
229
-
230
- console.log(chalk.dim('Downloading cloudflared tunnel client...'));
231
-
232
- // Create bin directory
233
- if (!fs.existsSync(CLOUDFLARED_DIR)) {
234
- fs.mkdirSync(CLOUDFLARED_DIR, { recursive: true });
235
- }
236
-
237
- const platform = process.platform;
238
- const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
239
-
240
- const urls = CLOUDFLARED_URLS[platform];
241
- if (!urls || !urls[arch]) {
242
- throw new Error(
243
- `Unsupported platform: ${platform}/${arch}. Please install cloudflared manually.`
244
- );
245
- }
246
-
247
- const url = urls[arch];
248
-
249
- return new Promise((resolve, reject) => {
250
- const downloadFile = (downloadUrl, destPath, callback) => {
251
- const file = fs.createWriteStream(destPath);
252
-
253
- https
254
- .get(downloadUrl, (response) => {
255
- // Handle redirects
256
- if (response.statusCode === 302 || response.statusCode === 301) {
257
- downloadFile(response.headers.location, destPath, callback);
258
- return;
259
- }
260
-
261
- response.pipe(file);
262
- file.on('finish', () => {
263
- file.close(callback);
264
- });
265
- })
266
- .on('error', (err) => {
267
- fs.unlink(destPath, () => {});
268
- reject(err);
269
- });
270
- };
271
-
272
- const isTarball = url.endsWith('.tgz');
273
- const downloadPath = isTarball
274
- ? path.join(CLOUDFLARED_DIR, 'cloudflared.tgz')
275
- : CLOUDFLARED_PATH;
276
-
277
- downloadFile(url, downloadPath, async () => {
278
- try {
279
- if (isTarball) {
280
- // Extract tarball (macOS)
281
- execSync(`tar -xzf "${downloadPath}" -C "${CLOUDFLARED_DIR}"`, {
282
- stdio: 'ignore',
283
- });
284
- fs.unlinkSync(downloadPath);
285
- }
286
-
287
- // Make executable
288
- if (platform !== 'win32') {
289
- fs.chmodSync(CLOUDFLARED_PATH, '755');
290
- }
291
-
292
- console.log(chalk.green('āœ“ cloudflared installed\n'));
293
- resolve();
294
- } catch (err) {
295
- reject(new Error(`Failed to setup cloudflared: ${err.message}`));
296
- }
297
- });
298
- });
299
- }
300
-
301
- /**
302
- * Start cloudflare tunnel
303
- */
304
- async function startTunnel(port) {
305
- await ensureCloudflared();
306
-
307
- return new Promise((resolve, reject) => {
308
- console.log(chalk.dim('Starting tunnel...'));
309
-
310
- tunnelProcess = spawn(CLOUDFLARED_PATH, [
311
- 'tunnel',
312
- '--url',
313
- `http://localhost:${port}`,
314
- '--no-autoupdate',
315
- ]);
316
-
317
- let resolved = false;
318
-
319
- tunnelProcess.stderr.on('data', (data) => {
320
- const output = data.toString();
321
-
322
- // Extract tunnel URL from output
323
- const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
324
- if (urlMatch && !resolved) {
325
- tunnelUrl = urlMatch[0];
326
- resolved = true;
327
- console.log(chalk.green(`āœ“ Tunnel active: ${chalk.cyan(tunnelUrl)}\n`));
328
- resolve();
329
- }
330
-
331
- // Debug output (only show errors)
332
- if (output.includes('ERR') || output.includes('error')) {
333
- console.error(chalk.red(output));
334
- }
335
- });
336
-
337
- tunnelProcess.on('error', (err) => {
338
- reject(new Error(`Failed to start tunnel: ${err.message}`));
339
- });
340
-
341
- tunnelProcess.on('exit', (code) => {
342
- if (!isShuttingDown && code !== 0) {
343
- console.error(chalk.red(`\nTunnel exited with code ${code}`));
344
- }
345
- });
346
-
347
- // Timeout
348
- setTimeout(() => {
349
- if (!resolved) {
350
- resolved = true;
351
- console.log(chalk.yellow('\nāš ļø Tunnel may still be connecting...\n'));
352
- resolve();
353
- }
354
- }, 30000);
355
- });
356
- }
357
-
358
- /**
359
- * Register tunnel URL with Lux cloud (dashboard)
360
- */
361
- async function registerTunnel(interfaceId, url) {
362
- try {
363
- const dashboardUrl = getDashboardUrl();
364
- const headers = getAuthHeaders();
365
-
366
- await axios.post(
367
- `${dashboardUrl}/api/tunnel/register`,
368
- {
369
- interface_id: interfaceId,
370
- tunnel_url: url,
371
- },
372
- { headers }
373
- );
374
-
375
- console.log(chalk.green('āœ“ Tunnel registered with Lux cloud\n'));
376
- } catch (err) {
377
- console.log(
378
- chalk.yellow('āš ļø Could not register tunnel with cloud:'),
379
- err.response?.data?.error || err.message
380
- );
381
- console.log(chalk.dim('Preview in dashboard may not work.\n'));
382
- }
383
- }
384
-
385
- /**
386
- * Unregister tunnel when shutting down
387
- */
388
- async function unregisterTunnel(interfaceId) {
389
- try {
390
- const dashboardUrl = getDashboardUrl();
391
- const headers = getAuthHeaders();
392
-
393
- await axios.post(
394
- `${dashboardUrl}/api/tunnel/unregister`,
395
- { interface_id: interfaceId },
396
- { headers, timeout: 3000 }
397
- );
398
- } catch (err) {
399
- // Ignore errors during shutdown
400
- }
401
- }
402
-
403
- /**
404
- * Start heartbeat to keep tunnel alive
405
- */
406
- function startHeartbeat(interfaceId) {
407
- const dashboardUrl = getDashboardUrl();
408
- const headers = getAuthHeaders();
409
-
410
- // Send heartbeat every 30 seconds
411
- heartbeatInterval = setInterval(async () => {
412
- try {
413
- await axios.post(
414
- `${dashboardUrl}/api/tunnel/heartbeat`,
415
- { interface_id: interfaceId },
416
- { headers, timeout: 5000 }
417
- );
418
- } catch (err) {
419
- // Silently ignore heartbeat failures
420
- // The tunnel will be marked stale after 2 minutes without heartbeat
421
- }
422
- }, 30000);
423
-
424
- // Send initial heartbeat
425
- axios.post(
426
- `${dashboardUrl}/api/tunnel/heartbeat`,
427
- { interface_id: interfaceId },
428
- { headers, timeout: 5000 }
429
- ).catch(() => {});
430
- }
431
-
432
- /**
433
- * Start file watcher for GitHub sync
434
- */
435
- function startFileSync() {
436
- console.log(chalk.dim('Starting file sync...\n'));
437
-
438
- // Debounce sync operations
439
- let syncTimeout = null;
440
- const pendingChanges = new Set();
441
-
442
- const doSync = () => {
443
- if (pendingChanges.size === 0) return;
444
-
445
- const changes = Array.from(pendingChanges);
446
- pendingChanges.clear();
447
-
448
- console.log(chalk.dim(`Syncing ${changes.length} file(s) to GitHub...`));
449
-
450
- try {
451
- // Add all changes
452
- execSync('git add -A', { stdio: 'ignore' });
453
-
454
- // Check if there are changes to commit
455
- const status = execSync('git status --porcelain').toString();
456
- if (status.trim()) {
457
- // Commit with auto message
458
- const message = `Auto-sync: ${changes.slice(0, 3).join(', ')}${changes.length > 3 ? ` +${changes.length - 3} more` : ''}`;
459
- execSync(`git commit -m "${message}"`, { stdio: 'ignore' });
460
-
461
- // Push to dev branch
462
- execSync('git push origin dev', { stdio: 'ignore' });
463
-
464
- console.log(chalk.green('āœ“ Synced to GitHub\n'));
465
- }
466
- } catch (err) {
467
- console.log(chalk.yellow('āš ļø Sync failed:'), err.message);
468
- }
469
- };
470
-
471
- // Watch for file changes
472
- syncWatcher = chokidar.watch('.', {
473
- ignored: [
474
- /node_modules/,
475
- /\.next/,
476
- /\.git/,
477
- /\.lux/,
478
- /\.DS_Store/,
479
- ],
480
- ignoreInitial: true,
481
- persistent: true,
482
- });
483
-
484
- syncWatcher.on('all', (event, filePath) => {
485
- pendingChanges.add(filePath);
486
-
487
- // Debounce: sync after 5 seconds of no changes
488
- clearTimeout(syncTimeout);
489
- syncTimeout = setTimeout(doSync, 5000);
490
- });
491
-
492
- console.log(chalk.green('āœ“ File sync active (auto-commits to GitHub)\n'));
493
- }
494
-
495
- /**
496
- * Print final status
497
- */
498
- function printStatus(port, noTunnel, noSync, interfaceConfig) {
499
- console.log(chalk.cyan('─'.repeat(50)));
500
- console.log(chalk.cyan.bold('\nšŸ“ Dev Server Status\n'));
501
-
502
- console.log(chalk.white(' Local:'), `http://localhost:${port}`);
503
-
504
- if (!noTunnel && tunnelUrl) {
505
- console.log(chalk.white(' Tunnel:'), chalk.cyan(tunnelUrl));
506
- console.log(chalk.white(' Preview:'), chalk.dim('Available in Lux dashboard'));
507
- }
508
-
509
- if (!noSync) {
510
- console.log(chalk.white(' Sync:'), chalk.green('Active'), chalk.dim('(auto-push to GitHub)'));
511
- }
512
-
513
- console.log(chalk.white(' Interface:'), interfaceConfig.name || interfaceConfig.id);
514
-
515
- console.log(chalk.cyan('\n─'.repeat(50)));
516
- console.log(chalk.dim('\nPress Ctrl+C to stop\n'));
517
- }
518
-
519
- /**
520
- * Setup graceful shutdown handlers
521
- */
522
- function setupShutdownHandlers() {
523
- const shutdown = async (signal) => {
524
- if (isShuttingDown) return;
525
- isShuttingDown = true;
526
-
527
- console.log(chalk.yellow(`\n\nReceived ${signal}, shutting down...\n`));
528
-
529
- await cleanup();
530
- process.exit(0);
531
- };
532
-
533
- process.on('SIGINT', () => shutdown('SIGINT'));
534
- process.on('SIGTERM', () => shutdown('SIGTERM'));
535
- process.on('SIGHUP', () => shutdown('SIGHUP'));
536
- }
537
-
538
- /**
539
- * Cleanup all processes
540
- */
541
- async function cleanup() {
542
- const interfaceConfig = loadInterfaceConfig();
543
-
544
- // Stop heartbeat
545
- if (heartbeatInterval) {
546
- clearInterval(heartbeatInterval);
547
- console.log(chalk.dim('Stopped heartbeat'));
548
- }
549
-
550
- // Unregister tunnel
551
- if (interfaceConfig?.id) {
552
- await unregisterTunnel(interfaceConfig.id);
553
- }
554
-
555
- // Stop file watcher
556
- if (syncWatcher) {
557
- await syncWatcher.close();
558
- console.log(chalk.dim('Stopped file sync'));
559
- }
560
-
561
- // Stop tunnel
562
- if (tunnelProcess) {
563
- tunnelProcess.kill();
564
- console.log(chalk.dim('Stopped tunnel'));
565
- }
566
-
567
- // Stop dev server
568
- if (devServerProcess) {
569
- devServerProcess.kill();
570
- console.log(chalk.dim('Stopped dev server'));
571
- }
572
-
573
- console.log(chalk.green('\nāœ“ Shutdown complete\n'));
574
- }
575
-
576
- module.exports = {
577
- dev,
578
- };
package/commands/init.js DELETED
@@ -1,126 +0,0 @@
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
- };