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,704 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const axios = require('axios');
5
+ const ora = require('ora');
6
+ const chalk = require('chalk');
7
+ const {
8
+ getApiUrl,
9
+ getAuthHeaders,
10
+ isAuthenticated,
11
+ getOrgId,
12
+ getProjectId,
13
+ LUX_STUDIO_DIR,
14
+ } = require('../lib/config');
15
+
16
+ /**
17
+ * Get the project directory path
18
+ * ~/.lux-studio/{orgId}/projects/{projectId}/
19
+ */
20
+ function getProjectDir(orgId, projectId) {
21
+ return path.join(LUX_STUDIO_DIR, orgId, 'projects', projectId);
22
+ }
23
+
24
+ /**
25
+ * Create .gitignore for the project
26
+ */
27
+ function createGitignore(projectDir) {
28
+ const gitignorePath = path.join(projectDir, '.gitignore');
29
+ const gitignoreContent = `# Dependencies
30
+ **/node_modules/
31
+
32
+ # Build outputs
33
+ **/.next/
34
+ **/.vercel/
35
+ **/dist/
36
+ **/build/
37
+ **/.turbo/
38
+
39
+ # Local KV cache (synced from cloud)
40
+ data/kv/
41
+
42
+ # OS files
43
+ .DS_Store
44
+ Thumbs.db
45
+
46
+ # Logs
47
+ *.log
48
+ npm-debug.log*
49
+
50
+ # Environment files (secrets)
51
+ .env
52
+ .env.local
53
+ .env.*.local
54
+
55
+ # IDE
56
+ .idea/
57
+ .vscode/
58
+ *.swp
59
+ *.swo
60
+ `;
61
+
62
+ fs.writeFileSync(gitignorePath, gitignoreContent);
63
+ }
64
+
65
+ /**
66
+ * Initialize git in project directory if needed
67
+ */
68
+ function initGitIfNeeded(projectDir) {
69
+ const gitDir = path.join(projectDir, '.git');
70
+
71
+ if (!fs.existsSync(gitDir)) {
72
+ execSync('git init', { cwd: projectDir, stdio: 'pipe' });
73
+ return true;
74
+ }
75
+
76
+ return false;
77
+ }
78
+
79
+ /**
80
+ * Deploy project to GitHub
81
+ * Force pushes the entire project state to the remote repository
82
+ */
83
+ async function deployProject(projectId) {
84
+ // Check authentication
85
+ if (!isAuthenticated()) {
86
+ console.log(
87
+ chalk.red('Not authenticated. Run'),
88
+ chalk.white('lux login'),
89
+ chalk.red('first.')
90
+ );
91
+ process.exit(1);
92
+ }
93
+
94
+ const orgId = getOrgId();
95
+ if (!orgId) {
96
+ console.log(chalk.red('No organization found. Please login first.'));
97
+ process.exit(1);
98
+ }
99
+
100
+ // Use provided projectId or get current project
101
+ const targetProjectId = projectId || getProjectId();
102
+ const projectDir = getProjectDir(orgId, targetProjectId);
103
+
104
+ // Check project exists
105
+ if (!fs.existsSync(projectDir)) {
106
+ console.log(chalk.red(`Project directory not found: ${projectDir}`));
107
+ process.exit(1);
108
+ }
109
+
110
+ const repoName = `${orgId}_${targetProjectId}`;
111
+ const apiUrl = getApiUrl();
112
+
113
+ console.log(chalk.dim(`\nProject: ${targetProjectId}`));
114
+ console.log(chalk.dim(`Directory: ${projectDir}`));
115
+ console.log(chalk.dim(`Repository: LuxUserProjects/${repoName}\n`));
116
+
117
+ // Step 0: Build check - verify all interfaces build successfully (if any exist)
118
+ const interfacesDir = path.join(projectDir, 'interfaces');
119
+ let interfaceIds = [];
120
+
121
+ if (fs.existsSync(interfacesDir)) {
122
+ interfaceIds = fs.readdirSync(interfacesDir).filter(f => {
123
+ const stat = fs.statSync(path.join(interfacesDir, f));
124
+ return stat.isDirectory();
125
+ });
126
+
127
+ if (interfaceIds.length > 0) {
128
+ console.log(chalk.dim(`Found ${interfaceIds.length} interface(s) to build\n`));
129
+
130
+ for (const interfaceId of interfaceIds) {
131
+ const repoDir = path.join(interfacesDir, interfaceId, 'repo');
132
+ const packageJsonPath = path.join(repoDir, 'package.json');
133
+
134
+ if (fs.existsSync(packageJsonPath)) {
135
+ const buildSpinner = ora(`Building interface ${interfaceId.substring(0, 8)}...`).start();
136
+
137
+ try {
138
+ // Install dependencies if node_modules doesn't exist
139
+ const nodeModulesPath = path.join(repoDir, 'node_modules');
140
+ if (!fs.existsSync(nodeModulesPath)) {
141
+ buildSpinner.text = `Installing dependencies for ${interfaceId.substring(0, 8)}...`;
142
+ execSync('npm install', { cwd: repoDir, stdio: 'pipe', timeout: 300000 });
143
+ }
144
+
145
+ // Run build
146
+ buildSpinner.text = `Building interface ${interfaceId.substring(0, 8)}...`;
147
+ execSync('npm run build', { cwd: repoDir, stdio: 'pipe', timeout: 300000 });
148
+ buildSpinner.succeed(`Interface ${interfaceId.substring(0, 8)} built successfully`);
149
+ } catch (error) {
150
+ buildSpinner.fail(`Interface ${interfaceId.substring(0, 8)} build failed`);
151
+
152
+ console.log(chalk.red('\n═══════════════════════════════════════════════════════════════'));
153
+ console.log(chalk.red(' BUILD FAILED'));
154
+ console.log(chalk.red('═══════════════════════════════════════════════════════════════\n'));
155
+
156
+ // Try to extract useful error info
157
+ if (error.stdout) {
158
+ console.log(chalk.dim('--- Build Output ---'));
159
+ console.log(chalk.dim(error.stdout.toString().slice(-3000)));
160
+ }
161
+ if (error.stderr) {
162
+ console.log(chalk.dim('\n--- Error Output ---'));
163
+ console.log(chalk.dim(error.stderr.toString().slice(-3000)));
164
+ }
165
+
166
+ console.log(chalk.dim('\n───────────────────────────────────────────────────────────────'));
167
+ console.log(chalk.cyan('\n💡 To fix this issue, please copy the build error above and'));
168
+ console.log(chalk.cyan(' share it with your coding agent (Claude) for assistance.\n'));
169
+ console.log(chalk.dim(' Interface directory:'));
170
+ console.log(chalk.dim(` ${repoDir}\n`));
171
+ console.log(chalk.dim('───────────────────────────────────────────────────────────────\n'));
172
+
173
+ process.exit(1);
174
+ }
175
+ }
176
+ }
177
+ } else {
178
+ console.log(chalk.dim('No interfaces found. Skipping build step.\n'));
179
+ }
180
+ } else {
181
+ console.log(chalk.dim('No interfaces directory. Skipping build step.\n'));
182
+ }
183
+
184
+ // Step 1: Get deploy credentials (GitHub token, and Vercel projects if interfaces exist)
185
+ const tokenSpinner = ora('Getting deploy credentials...').start();
186
+
187
+ let repoUrl, token, interfaces;
188
+ try {
189
+ const { data } = await axios.post(
190
+ `${apiUrl}/api/projects/${targetProjectId}/deploy/init`,
191
+ { orgId, interfaceIds },
192
+ { headers: getAuthHeaders() }
193
+ );
194
+
195
+ repoUrl = data.repoUrl;
196
+ token = data.token;
197
+ interfaces = data.interfaces || [];
198
+
199
+ if (interfaces.length > 0) {
200
+ tokenSpinner.succeed(`Got deploy credentials for ${interfaces.length} interface(s)`);
201
+ } else {
202
+ tokenSpinner.succeed('Got deploy credentials (no interfaces to deploy to Vercel)');
203
+ }
204
+ } catch (error) {
205
+ tokenSpinner.fail('Failed to get deploy credentials');
206
+ console.error(
207
+ chalk.red('\nError:'),
208
+ error.response?.data?.error || error.message
209
+ );
210
+
211
+ if (error.response?.status === 401) {
212
+ console.log(
213
+ chalk.yellow('\nYour session may have expired. Try running:'),
214
+ chalk.white('lux login')
215
+ );
216
+ }
217
+
218
+ process.exit(1);
219
+ }
220
+
221
+ // Step 2: Initialize git if needed
222
+ const gitSpinner = ora('Preparing git repository...').start();
223
+
224
+ try {
225
+ const wasInitialized = initGitIfNeeded(projectDir);
226
+ if (wasInitialized) {
227
+ gitSpinner.text = 'Initialized git repository';
228
+ }
229
+
230
+ // Create/update .gitignore
231
+ createGitignore(projectDir);
232
+
233
+ // Always configure git user (required for commits)
234
+ execSync('git config user.email "deploy@uselux.ai"', { cwd: projectDir, stdio: 'pipe' });
235
+ execSync('git config user.name "Lux Deploy"', { cwd: projectDir, stdio: 'pipe' });
236
+
237
+ gitSpinner.succeed('Git repository ready');
238
+ } catch (error) {
239
+ gitSpinner.fail('Failed to prepare git repository');
240
+ console.error(chalk.red('\nError:'), error.message);
241
+ process.exit(1);
242
+ }
243
+
244
+ // Step 3: Stage all files
245
+ const stageSpinner = ora('Staging files...').start();
246
+
247
+ try {
248
+ execSync('git add -A', { cwd: projectDir, stdio: 'pipe' });
249
+ stageSpinner.succeed('Files staged');
250
+ } catch (error) {
251
+ stageSpinner.fail('Failed to stage files');
252
+ console.error(chalk.red('\nError:'), error.message);
253
+ process.exit(1);
254
+ }
255
+
256
+ // Step 4: Commit
257
+ const commitSpinner = ora('Creating commit...').start();
258
+
259
+ try {
260
+ const timestamp = new Date().toISOString();
261
+ const commitMessage = `Deploy ${timestamp}`;
262
+
263
+ // Check if we have any commits at all
264
+ let hasCommits = false;
265
+ try {
266
+ execSync('git rev-parse HEAD', { cwd: projectDir, stdio: 'pipe' });
267
+ hasCommits = true;
268
+ } catch {
269
+ hasCommits = false;
270
+ }
271
+
272
+ // Check if there are staged changes
273
+ const stagedDiff = execSync('git diff --cached --name-only', { cwd: projectDir, encoding: 'utf-8' });
274
+
275
+ if (stagedDiff.trim() === '' && hasCommits) {
276
+ // No staged changes and already has commits
277
+ commitSpinner.succeed('No changes to commit');
278
+ } else if (!hasCommits) {
279
+ // First commit - use --allow-empty if needed
280
+ try {
281
+ execSync(`git commit -m "${commitMessage}"`, { cwd: projectDir, stdio: 'pipe' });
282
+ commitSpinner.succeed('Created initial commit');
283
+ } catch {
284
+ execSync(`git commit --allow-empty -m "${commitMessage}"`, { cwd: projectDir, stdio: 'pipe' });
285
+ commitSpinner.succeed('Created initial commit');
286
+ }
287
+ } else {
288
+ // Has staged changes
289
+ execSync(`git commit -m "${commitMessage}"`, { cwd: projectDir, stdio: 'pipe' });
290
+ commitSpinner.succeed('Commit created');
291
+ }
292
+ } catch (error) {
293
+ // If commit fails because nothing to commit, that's OK
294
+ const errorStr = error.message + (error.stderr || '');
295
+ if (errorStr.includes('nothing to commit') || errorStr.includes('nothing added to commit')) {
296
+ commitSpinner.succeed('No changes to commit');
297
+ } else {
298
+ commitSpinner.fail('Failed to create commit');
299
+ console.error(chalk.red('\nError:'), error.message);
300
+ if (error.stderr) console.error(chalk.dim(error.stderr));
301
+ process.exit(1);
302
+ }
303
+ }
304
+
305
+ // Step 5: Push to GitHub (force push)
306
+ const pushSpinner = ora('Pushing to GitHub...').start();
307
+
308
+ try {
309
+ // Construct authenticated URL
310
+ const authUrl = repoUrl.replace('https://github.com/', `https://x-access-token:${token}@github.com/`);
311
+
312
+ // Set remote (or update if exists)
313
+ try {
314
+ execSync(`git remote add origin ${authUrl}`, { cwd: projectDir, stdio: 'pipe' });
315
+ } catch {
316
+ // Remote might already exist, update it
317
+ execSync(`git remote set-url origin ${authUrl}`, { cwd: projectDir, stdio: 'pipe' });
318
+ }
319
+
320
+ // Always ensure we're on main branch before pushing
321
+ execSync('git branch -M main', { cwd: projectDir, stdio: 'pipe' });
322
+
323
+ // Force push to main
324
+ execSync('git push -f origin main', { cwd: projectDir, stdio: 'pipe', timeout: 120000 });
325
+
326
+ // Clear the token from remote URL after push (security)
327
+ execSync(`git remote set-url origin ${repoUrl}`, { cwd: projectDir, stdio: 'pipe' });
328
+
329
+ pushSpinner.succeed('Pushed to GitHub');
330
+ } catch (error) {
331
+ pushSpinner.fail('Failed to push to GitHub');
332
+ console.error(chalk.red('\nError:'), error.message);
333
+
334
+ // Clear token from URL even on failure
335
+ try {
336
+ execSync(`git remote set-url origin ${repoUrl}`, { cwd: projectDir, stdio: 'pipe' });
337
+ } catch {
338
+ // Ignore
339
+ }
340
+
341
+ process.exit(1);
342
+ }
343
+
344
+ // Track overall deployment errors
345
+ let interfaceHasErrors = false;
346
+
347
+ // Step 6: Trigger Vercel deployment for each interface (if any)
348
+ if (interfaces.length > 0) {
349
+ console.log(chalk.dim(`\nDeploying ${interfaces.length} interface(s) to Vercel...\n`));
350
+
351
+ const deploymentResults = [];
352
+
353
+ for (const iface of interfaces) {
354
+ const vercelSpinner = ora(`Deploying interface ${iface.interfaceId.substring(0, 8)}...`).start();
355
+
356
+ try {
357
+ const { data: deployData } = await axios.post(
358
+ `${apiUrl}/api/projects/${targetProjectId}/deploy/trigger`,
359
+ {
360
+ vercelProjectId: iface.vercelProjectId,
361
+ vercelProjectName: iface.vercelProjectName,
362
+ repoName: repoName.toLowerCase(),
363
+ interfaceId: iface.interfaceId
364
+ },
365
+ { headers: getAuthHeaders() }
366
+ );
367
+
368
+ vercelSpinner.succeed(`Interface ${iface.interfaceId.substring(0, 8)} deployment triggered`);
369
+
370
+ // Sync local custom domains to Vercel after successful deployment
371
+ const interfaceMetadataPath = path.join(interfacesDir, iface.interfaceId, 'metadata.json');
372
+ if (fs.existsSync(interfaceMetadataPath)) {
373
+ try {
374
+ const metadata = JSON.parse(fs.readFileSync(interfaceMetadataPath, 'utf-8'));
375
+ const localDomains = metadata.customDomains || [];
376
+ const pendingDomains = localDomains.filter(d => !d.synced);
377
+
378
+ if (pendingDomains.length > 0) {
379
+ const domainSpinner = ora(`Syncing ${pendingDomains.length} custom domain(s)...`).start();
380
+
381
+ for (const domainInfo of pendingDomains) {
382
+ try {
383
+ // Add domain to Vercel via backend API
384
+ await axios.post(
385
+ `${apiUrl}/api/interfaces/${iface.interfaceId}/domains`,
386
+ { domain: domainInfo.domain },
387
+ { headers: getAuthHeaders() }
388
+ );
389
+
390
+ // Mark as synced in local metadata
391
+ domainInfo.synced = true;
392
+ domainInfo.syncedAt = new Date().toISOString();
393
+ } catch (domainError) {
394
+ console.log(chalk.dim(`\n Warning: Failed to add domain ${domainInfo.domain}: ${domainError.response?.data?.error || domainError.message}`));
395
+ }
396
+ }
397
+
398
+ // Save updated metadata
399
+ fs.writeFileSync(interfaceMetadataPath, JSON.stringify(metadata, null, 2));
400
+
401
+ const syncedCount = pendingDomains.filter(d => d.synced).length;
402
+ if (syncedCount === pendingDomains.length) {
403
+ domainSpinner.succeed(`${syncedCount} custom domain(s) synced`);
404
+ } else {
405
+ domainSpinner.warn(`${syncedCount}/${pendingDomains.length} custom domain(s) synced`);
406
+ }
407
+ }
408
+ } catch (metadataError) {
409
+ // Ignore metadata errors - domain sync is optional
410
+ }
411
+ }
412
+
413
+ deploymentResults.push({
414
+ interfaceId: iface.interfaceId,
415
+ name: iface.interfaceName || iface.interfaceId.substring(0, 8),
416
+ success: true,
417
+ url: deployData.deployment_url,
418
+ subdomainUrl: iface.subdomainUrl,
419
+ deploymentId: deployData.deployment_id
420
+ });
421
+ } catch (error) {
422
+ interfaceHasErrors = true;
423
+ vercelSpinner.fail(`Interface ${iface.interfaceId.substring(0, 8)} deployment failed`);
424
+ const errorData = error.response?.data;
425
+ deploymentResults.push({
426
+ interfaceId: iface.interfaceId,
427
+ name: iface.interfaceName || iface.interfaceId.substring(0, 8),
428
+ success: false,
429
+ error: errorData?.error || error.message
430
+ });
431
+ }
432
+ }
433
+
434
+ // Summary
435
+ console.log('');
436
+ if (interfaceHasErrors) {
437
+ console.log(chalk.yellow('═══════════════════════════════════════════════════════════════'));
438
+ console.log(chalk.yellow(' DEPLOYMENT COMPLETED WITH ERRORS'));
439
+ console.log(chalk.yellow('═══════════════════════════════════════════════════════════════\n'));
440
+ } else {
441
+ console.log(chalk.green('═══════════════════════════════════════════════════════════════'));
442
+ console.log(chalk.green(' ALL DEPLOYMENTS SUCCESSFUL'));
443
+ console.log(chalk.green('═══════════════════════════════════════════════════════════════\n'));
444
+ }
445
+
446
+ console.log(chalk.dim(`GitHub: ${repoUrl}\n`));
447
+
448
+ for (const result of deploymentResults) {
449
+ if (result.success) {
450
+ console.log(chalk.green(`✓ ${result.interfaceId.substring(0, 8)}`));
451
+ console.log(chalk.dim(` URL: ${result.url}`));
452
+ console.log(chalk.dim(` Deployment ID: ${result.deploymentId}`));
453
+ } else {
454
+ console.log(chalk.red(`✗ ${result.interfaceId.substring(0, 8)}`));
455
+ console.log(chalk.dim(` Error: ${result.error}`));
456
+ }
457
+ console.log('');
458
+ }
459
+
460
+ if (interfaceHasErrors) {
461
+ console.log(chalk.dim('───────────────────────────────────────────────────────────────'));
462
+ console.log(chalk.cyan('\n💡 To debug failed deployments, check the Vercel dashboard:'));
463
+ console.log(chalk.dim(' https://vercel.com/dashboard\n'));
464
+ console.log(chalk.dim('───────────────────────────────────────────────────────────────\n'));
465
+ }
466
+ } else {
467
+ // No interfaces - just show GitHub success
468
+ console.log('');
469
+ console.log(chalk.green('═══════════════════════════════════════════════════════════════'));
470
+ console.log(chalk.green(' PROJECT PUSHED TO GITHUB'));
471
+ console.log(chalk.green('═══════════════════════════════════════════════════════════════\n'));
472
+ console.log(chalk.dim(`GitHub: ${repoUrl}\n`));
473
+ console.log(chalk.dim('No interfaces to deploy to Vercel.\n'));
474
+ }
475
+
476
+ // Step 7: Deploy flows (if any exist)
477
+ const flowsDir = path.join(projectDir, 'flows');
478
+ if (fs.existsSync(flowsDir)) {
479
+ const flowFiles = fs.readdirSync(flowsDir).filter(f => f.endsWith('.json'));
480
+
481
+ if (flowFiles.length > 0) {
482
+ console.log(chalk.dim(`\nDeploying ${flowFiles.length} flow(s)...\n`));
483
+
484
+ const flowResults = [];
485
+ let flowHasErrors = false;
486
+
487
+ for (const flowFile of flowFiles) {
488
+ const flowId = flowFile.replace('.json', '');
489
+ const flowSpinner = ora(`Deploying flow ${flowId.substring(0, 8)}...`).start();
490
+
491
+ try {
492
+ // Read flow data
493
+ const flowPath = path.join(flowsDir, flowFile);
494
+ const flowData = JSON.parse(fs.readFileSync(flowPath, 'utf-8'));
495
+
496
+ // Deploy to cloud API using /publish endpoint
497
+ // The publish endpoint supports CLI auth and will create the flow if it doesn't exist
498
+ const { data: deployData } = await axios.post(
499
+ `${apiUrl}/api/workflows/${flowId}/publish`,
500
+ {
501
+ // Wrap flow data in config object as expected by /publish
502
+ config: {
503
+ nodes: flowData.nodes,
504
+ edges: flowData.edges,
505
+ variables: flowData.variables || {},
506
+ metadata: {
507
+ name: flowData.name,
508
+ description: flowData.description,
509
+ ...flowData.metadata,
510
+ },
511
+ },
512
+ },
513
+ { headers: getAuthHeaders() }
514
+ );
515
+
516
+ flowSpinner.succeed(`Flow ${flowId.substring(0, 8)} deployed`);
517
+ flowResults.push({
518
+ flowId,
519
+ name: flowData.name,
520
+ success: true,
521
+ webhookUrl: deployData.workflow?.webhook_token ? `${apiUrl}/api/webhooks/${deployData.workflow.webhook_token}` : null,
522
+ });
523
+
524
+ // Update local flow with webhook URL
525
+ flowData.deployedAt = new Date().toISOString();
526
+ const webhookToken = deployData.workflow?.webhook_token;
527
+ flowData.webhookUrl = webhookToken ? `${apiUrl}/api/webhooks/${webhookToken}` : null;
528
+ fs.writeFileSync(flowPath, JSON.stringify(flowData, null, 2));
529
+ } catch (error) {
530
+ flowHasErrors = true;
531
+ flowSpinner.fail(`Flow ${flowId.substring(0, 8)} deployment failed`);
532
+ const errorData = error.response?.data;
533
+ flowResults.push({
534
+ flowId,
535
+ success: false,
536
+ error: errorData?.error || error.message,
537
+ });
538
+ }
539
+ }
540
+
541
+ // Flow deployment summary
542
+ console.log('');
543
+ if (flowHasErrors) {
544
+ console.log(chalk.yellow('Some flow deployments failed:\n'));
545
+ } else {
546
+ console.log(chalk.green('All flows deployed successfully:\n'));
547
+ }
548
+
549
+ for (const result of flowResults) {
550
+ if (result.success) {
551
+ console.log(chalk.green(`✓ ${result.name || result.flowId.substring(0, 8)}`));
552
+ if (result.webhookUrl) {
553
+ console.log(chalk.dim(` Webhook: ${result.webhookUrl}`));
554
+ }
555
+ } else {
556
+ console.log(chalk.red(`✗ ${result.flowId.substring(0, 8)}`));
557
+ console.log(chalk.dim(` Error: ${result.error}`));
558
+ }
559
+ }
560
+ console.log('');
561
+
562
+ // Exit with error if any flows failed
563
+ if (flowHasErrors) {
564
+ process.exit(1);
565
+ }
566
+ }
567
+ }
568
+
569
+ // Output JSON summary for programmatic parsing (Lux Studio)
570
+ // Format: __LUX_DEPLOY_RESULT__{json}__END_LUX_DEPLOY_RESULT__
571
+ if (interfaces.length > 0) {
572
+ const deploymentSummary = {
573
+ success: !interfaceHasErrors,
574
+ interfaces: deploymentResults.map(r => ({
575
+ id: r.interfaceId,
576
+ name: r.name,
577
+ success: r.success,
578
+ subdomainUrl: r.subdomainUrl,
579
+ deploymentUrl: r.url,
580
+ error: r.error
581
+ }))
582
+ };
583
+ console.log(`__LUX_DEPLOY_RESULT__${JSON.stringify(deploymentSummary)}__END_LUX_DEPLOY_RESULT__`);
584
+ }
585
+
586
+ // Exit with error if any interface deployments failed (and no flows were deployed)
587
+ if (interfaceHasErrors) {
588
+ process.exit(1);
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Show project status
594
+ */
595
+ async function projectStatus(projectId) {
596
+ if (!isAuthenticated()) {
597
+ console.log(
598
+ chalk.red('Not authenticated. Run'),
599
+ chalk.white('lux login'),
600
+ chalk.red('first.')
601
+ );
602
+ process.exit(1);
603
+ }
604
+
605
+ const orgId = getOrgId();
606
+ if (!orgId) {
607
+ console.log(chalk.red('No organization found. Please login first.'));
608
+ process.exit(1);
609
+ }
610
+
611
+ const targetProjectId = projectId || getProjectId();
612
+ const projectDir = getProjectDir(orgId, targetProjectId);
613
+ const repoName = `${orgId}_${targetProjectId}`;
614
+
615
+ console.log(chalk.bold('\nProject Status\n'));
616
+ console.log(` Project ID: ${chalk.cyan(targetProjectId)}`);
617
+ console.log(` Directory: ${chalk.dim(projectDir)}`);
618
+ console.log(` Repository: ${chalk.dim(`LuxOrg/${repoName}`)}`);
619
+
620
+ // Check if project exists locally
621
+ if (!fs.existsSync(projectDir)) {
622
+ console.log(chalk.yellow('\n Local project directory not found.'));
623
+ return;
624
+ }
625
+
626
+ // Check git status
627
+ const gitDir = path.join(projectDir, '.git');
628
+ if (fs.existsSync(gitDir)) {
629
+ try {
630
+ const status = execSync('git status --porcelain', { cwd: projectDir, encoding: 'utf-8' });
631
+ const lines = status.trim().split('\n').filter(l => l.trim());
632
+
633
+ if (lines.length === 0) {
634
+ console.log(chalk.green('\n No uncommitted changes.'));
635
+ } else {
636
+ console.log(chalk.yellow(`\n ${lines.length} uncommitted changes:`));
637
+ lines.slice(0, 10).forEach(line => {
638
+ console.log(chalk.dim(` ${line}`));
639
+ });
640
+ if (lines.length > 10) {
641
+ console.log(chalk.dim(` ... and ${lines.length - 10} more`));
642
+ }
643
+ }
644
+
645
+ // Check if remote exists
646
+ try {
647
+ const remote = execSync('git remote get-url origin', { cwd: projectDir, encoding: 'utf-8' }).trim();
648
+ console.log(chalk.dim(`\n Remote: ${remote}`));
649
+ } catch {
650
+ console.log(chalk.yellow('\n No remote configured. Run `lux project deploy` to push to GitHub.'));
651
+ }
652
+ } catch (error) {
653
+ console.log(chalk.dim('\n Unable to get git status.'));
654
+ }
655
+ } else {
656
+ console.log(chalk.yellow('\n Git not initialized. Run `lux project deploy` to initialize and push.'));
657
+ }
658
+
659
+ console.log('');
660
+ }
661
+
662
+ /**
663
+ * Handle project commands
664
+ */
665
+ async function handleProject(args) {
666
+ const subcommand = args[0];
667
+ const subArgs = args.slice(1);
668
+
669
+ switch (subcommand) {
670
+ case 'deploy':
671
+ case 'push': {
672
+ const projectId = subArgs[0]; // Optional: specific project ID
673
+ await deployProject(projectId);
674
+ break;
675
+ }
676
+
677
+ case 'status': {
678
+ const projectId = subArgs[0];
679
+ await projectStatus(projectId);
680
+ break;
681
+ }
682
+
683
+ case 'help':
684
+ default:
685
+ console.log(chalk.bold('\nLux Project Commands\n'));
686
+ console.log('Usage: lux project <command> [options]\n');
687
+ console.log('Commands:');
688
+ console.log(' deploy [projectId] Push project to GitHub (force push entire state)');
689
+ console.log(' push [projectId] Alias for deploy');
690
+ console.log(' status [projectId] Show project status');
691
+ console.log('');
692
+ console.log('Examples:');
693
+ console.log(' lux project deploy Deploy current project');
694
+ console.log(' lux project deploy my-project-id Deploy specific project');
695
+ console.log(' lux project status Show current project status');
696
+ console.log('');
697
+ break;
698
+ }
699
+ }
700
+
701
+ module.exports = {
702
+ handleProject,
703
+ deployProject,
704
+ };