luxlabs 1.0.17 → 1.0.19

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.
@@ -6,6 +6,7 @@ const {
6
6
  getAuthHeaders,
7
7
  isAuthenticated,
8
8
  getOrgId,
9
+ getProjectId,
9
10
  getFlowsDir,
10
11
  loadLocalFlow,
11
12
  saveLocalFlow,
@@ -73,7 +74,7 @@ async function saveWorkflowDraft(workflowId, config) {
73
74
  );
74
75
  }
75
76
 
76
- async function handleWorkflows(args) {
77
+ async function handleFlows(args) {
77
78
  // Check authentication
78
79
  if (!isAuthenticated()) {
79
80
  console.log(
@@ -88,18 +89,18 @@ async function handleWorkflows(args) {
88
89
 
89
90
  if (!command) {
90
91
  console.log(`
91
- ${chalk.bold('Usage:')} lux workflows <command> [args]
92
+ ${chalk.bold('Usage:')} lux flows <command> [args]
92
93
 
93
94
  ${chalk.bold('Commands:')}
94
- list List all workflows (from local storage)
95
- get <id> Get workflow details with config
96
- status <id> Show sync status for a workflow
97
- create <name> [desc] [--publish] Create workflow (use --publish to publish immediately)
98
- init <name> [desc] [--publish] Initialize workflow (alias for create)
95
+ list List all flows (from local storage)
96
+ get <id> Get flow details with config
97
+ status <id> Show sync status for a flow
98
+ create <name> [desc] [--publish] Create flow (use --publish to publish immediately)
99
+ init <name> [desc] [--publish] Initialize flow (alias for create)
99
100
  save <id> <config-file> Save draft config from file (local)
100
- sync Sync published workflows from cloud
101
- publish <id> Publish workflow to cloud
102
- delete <id> Delete a local workflow
101
+ sync Sync published flows from cloud
102
+ publish <id> Publish flow to cloud
103
+ delete <id> Delete a local flow
103
104
  diff <id> Show local vs published differences
104
105
 
105
106
  ${chalk.bold('Execution History:')}
@@ -120,29 +121,40 @@ ${chalk.bold('Webhook Commands:')}
120
121
  webhook-accept <id> Accept webhook format and save variables
121
122
  webhook-decline <id> Decline webhook and reset session
122
123
 
124
+ ${chalk.bold('Execution Commands:')}
125
+ executions <id> List recent executions for a flow
126
+ execution <id> <exec-id> Get details of a specific execution
127
+ test <id> [--data] Test a flow with sample data
128
+ last <id> Show the last execution result
129
+
123
130
  ${chalk.bold('Examples:')}
124
- lux workflows list
125
- lux workflows get flow_123
126
- lux workflows status flow_123
127
- lux workflows create "My Workflow" "Description"
128
- lux workflows create "My Workflow" --publish # Create and publish immediately
129
- lux flow init "My Workflow" --publish
130
- lux workflows save flow_123 ./config.json
131
- lux workflows sync
132
- lux workflows publish flow_123
133
- lux workflows diff flow_123
131
+ lux flows list
132
+ lux flows get flow_123
133
+ lux flows status flow_123
134
+ lux flows create "My Flow" "Description"
135
+ lux flows create "My Flow" --publish # Create and publish immediately
136
+ lux flows init "My Flow" --publish
137
+ lux flows save flow_123 ./config.json
138
+ lux flows sync
139
+ lux flows publish flow_123
140
+ lux flows diff flow_123
134
141
 
135
142
  ${chalk.bold('Execution history:')}
136
- lux flow executions my-flow-id
137
- lux flow executions my-flow-id --limit 50
138
- lux flow execution my-flow-id exec_abc123
139
- lux flow node my-flow-id exec_abc123 node-1
140
-
141
- ${chalk.bold('Webhook workflow:')}
142
- lux flow webhook-url flow_123
143
- lux flow webhook-listen flow_123
144
- lux flow webhook-poll flow_123
145
- lux flow webhook-accept flow_123
143
+ lux flows executions my-flow-id
144
+ lux flows executions my-flow-id --limit 50
145
+ lux flows execution my-flow-id exec_abc123
146
+ lux flows node my-flow-id exec_abc123 node-1
147
+
148
+ ${chalk.bold('Webhook:')}
149
+ lux flows webhook-url flow_123
150
+ lux flows webhook-listen flow_123
151
+ lux flows webhook-poll flow_123
152
+ lux flows webhook-accept flow_123
153
+
154
+ ${chalk.bold('Execution debugging:')}
155
+ lux flows executions my-flow # List recent executions
156
+ lux flows last my-flow # Show last execution result
157
+ lux flows test my-flow --data '{"name":"test"}' # Test with data
146
158
  `);
147
159
  process.exit(0);
148
160
  }
@@ -153,14 +165,14 @@ ${chalk.bold('Examples:')}
153
165
  try {
154
166
  switch (command) {
155
167
  case 'list': {
156
- info('Loading workflows from local storage...');
168
+ info('Loading flows from local storage...');
157
169
  const localFlows = listLocalFlows();
158
170
 
159
171
  if (localFlows.length === 0) {
160
- console.log('\n(No local workflows found)');
161
- console.log(chalk.gray('Run "lux workflows sync" to sync from cloud, or "lux workflows create" to create one.\n'));
172
+ console.log('\n(No local flows found)');
173
+ console.log(chalk.gray('Run "lux flows sync" to sync from cloud, or "lux flows create" to create one.\n'));
162
174
  } else {
163
- console.log(`\nFound ${localFlows.length} workflow(s):\n`);
175
+ console.log(`\nFound ${localFlows.length} flow(s):\n`);
164
176
 
165
177
  // Color-code sync status
166
178
  const statusColors = {
@@ -191,12 +203,12 @@ ${chalk.bold('Examples:')}
191
203
  }
192
204
 
193
205
  case 'status': {
194
- requireArgs(args.slice(1), 1, 'lux workflows status <id>');
195
- const workflowId = args[1];
206
+ requireArgs(args.slice(1), 1, 'lux flows status <id>');
207
+ const flowId = args[1];
196
208
 
197
- const flow = loadLocalFlow(workflowId);
209
+ const flow = loadLocalFlow(flowId);
198
210
  if (!flow) {
199
- error(`Workflow not found locally: ${workflowId}`);
211
+ error(`Flow not found locally: ${flowId}`);
200
212
  break;
201
213
  }
202
214
 
@@ -219,8 +231,8 @@ ${chalk.bold('Examples:')}
219
231
  conflict: chalk.red,
220
232
  };
221
233
 
222
- console.log(`\n📝 Workflow: ${flow.name}`);
223
- console.log(` ID: ${workflowId}`);
234
+ console.log(`\n📝 Flow: ${flow.name}`);
235
+ console.log(` ID: ${flowId}`);
224
236
  console.log(` Sync Status: ${statusColors[syncStatus](syncStatus)}`);
225
237
  console.log(` Local Version: ${flow.localVersion || 1}`);
226
238
  console.log(` Published Version: ${flow.publishedVersion || '(never published)'}`);
@@ -231,17 +243,17 @@ ${chalk.bold('Examples:')}
231
243
  console.log(` Edges: ${flow.edges?.length || 0}`);
232
244
 
233
245
  if (syncStatus === 'conflict') {
234
- console.log(chalk.red('\n⚠️ This workflow has conflicts with the cloud version.'));
246
+ console.log(chalk.red('\n⚠️ This flow has conflicts with the cloud version.'));
235
247
  console.log(chalk.gray(' Use the Lux Studio app to resolve conflicts.\n'));
236
248
  } else if (syncStatus === 'dirty') {
237
- console.log(chalk.yellow('\n📤 This workflow has unpublished local changes.'));
238
- console.log(chalk.gray(' Run "lux workflows publish ' + workflowId + '" to publish.\n'));
249
+ console.log(chalk.yellow('\n📤 This flow has unpublished local changes.'));
250
+ console.log(chalk.gray(' Run "lux flows publish ' + flowId + '" to publish.\n'));
239
251
  }
240
252
  break;
241
253
  }
242
254
 
243
255
  case 'sync': {
244
- info('Syncing workflows from cloud...');
256
+ info('Syncing flows from cloud...');
245
257
 
246
258
  const { data } = await axios.get(`${apiUrl}/api/workflows?include_config=true`, {
247
259
  headers: getAuthHeaders(),
@@ -345,18 +357,18 @@ ${chalk.bold('Examples:')}
345
357
  console.log(` New from cloud: ${newFromCloud}`);
346
358
  if (conflicts > 0) {
347
359
  console.log(chalk.red(` Conflicts: ${conflicts}`));
348
- console.log(chalk.gray('\n Use "lux workflows list" to see workflows with conflicts.'));
360
+ console.log(chalk.gray('\n Use "lux flows list" to see flows with conflicts.'));
349
361
  }
350
362
  break;
351
363
  }
352
364
 
353
365
  case 'delete': {
354
- requireArgs(args.slice(1), 1, 'lux workflows delete <id>');
355
- const workflowId = args[1];
366
+ requireArgs(args.slice(1), 1, 'lux flows delete <id>');
367
+ const flowId = args[1];
356
368
 
357
- const flow = loadLocalFlow(workflowId);
369
+ const flow = loadLocalFlow(flowId);
358
370
  if (!flow) {
359
- error(`Workflow not found locally: ${workflowId}`);
371
+ error(`Flow not found locally: ${flowId}`);
360
372
  break;
361
373
  }
362
374
 
@@ -365,23 +377,23 @@ ${chalk.bold('Examples:')}
365
377
  console.log(chalk.gray(' This only deletes the local copy. The published version remains in the cloud.\n'));
366
378
  }
367
379
 
368
- deleteLocalFlow(workflowId);
369
- success(`Deleted local workflow: ${flow.name}`);
380
+ deleteLocalFlow(flowId);
381
+ success(`Deleted local flow: ${flow.name}`);
370
382
  break;
371
383
  }
372
384
 
373
385
  case 'get': {
374
- requireArgs(args.slice(1), 1, 'lux workflows get <id>');
375
- const workflowId = args[1];
386
+ requireArgs(args.slice(1), 1, 'lux flows get <id>');
387
+ const flowId = args[1];
376
388
 
377
- info(`Loading workflow: ${workflowId}`);
389
+ info(`Loading flow: ${flowId}`);
378
390
  const { data } = await axios.get(
379
- `${apiUrl}/api/workflows/${workflowId}`,
391
+ `${apiUrl}/api/workflows/${flowId}`,
380
392
  { headers: getAuthHeaders() }
381
393
  );
382
394
 
383
395
  if (!data.workflow) {
384
- error(`Workflow not found: ${workflowId}`);
396
+ error(`Flow not found: ${flowId}`);
385
397
  }
386
398
 
387
399
  const workflow = data.workflow;
@@ -409,7 +421,7 @@ ${chalk.bold('Examples:')}
409
421
  args.splice(publishFlagIndex, 1); // Remove flag from args
410
422
  }
411
423
 
412
- requireArgs(args.slice(1), 1, 'lux workflows create <name> [description] [--publish]');
424
+ requireArgs(args.slice(1), 1, 'lux flows create <name> [description] [--publish]');
413
425
  const name = args[1];
414
426
  const description = args[2] || '';
415
427
 
@@ -442,15 +454,15 @@ ${chalk.bold('Examples:')}
442
454
  updatedAt: now,
443
455
  };
444
456
 
445
- info(`Creating local workflow: ${name}`);
457
+ info(`Creating local flow: ${name}`);
446
458
 
447
459
  if (saveLocalFlow(flowId, newFlow)) {
448
- success(`Workflow created locally!`);
460
+ success(`Flow created locally!`);
449
461
  console.log(` ID: ${flowId}`);
450
462
 
451
463
  // If --publish flag was provided, publish immediately
452
464
  if (shouldPublish) {
453
- info(`Publishing workflow to cloud...`);
465
+ info(`Publishing flow to cloud...`);
454
466
 
455
467
  try {
456
468
  const publishConfig = {
@@ -473,7 +485,7 @@ ${chalk.bold('Examples:')}
473
485
 
474
486
  const cloudId = createResponse.data.workflow?.id || createResponse.data.workflow?.workflow_id;
475
487
  if (!cloudId) {
476
- throw new Error('No workflow ID returned from cloud');
488
+ throw new Error('No flow ID returned from cloud');
477
489
  }
478
490
 
479
491
  info(`Created in cloud with ID: ${cloudId}`);
@@ -506,41 +518,41 @@ ${chalk.bold('Examples:')}
506
518
  };
507
519
  saveLocalFlow(flowId, updatedFlow);
508
520
 
509
- success('Workflow published!');
521
+ success('Flow published!');
510
522
  console.log(` Cloud ID: ${cloudId}`);
511
523
  console.log(` Status: ${chalk.green('published')}`);
512
524
  console.log(` Version: ${newVersion}`);
513
525
  } catch (publishError) {
514
526
  error(`Failed to publish: ${publishError.response?.data?.error || publishError.message}`);
515
527
  console.log(` Status: ${chalk.gray('draft')}`);
516
- console.log(chalk.gray(`\nRun "lux workflows publish ${flowId}" to try again.`));
528
+ console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to try again.`));
517
529
  }
518
530
  } else {
519
531
  console.log(` Status: ${chalk.gray('draft')}`);
520
- console.log(chalk.gray(`\n This workflow is local only. Run "lux workflows publish ${flowId}" to publish to cloud.`));
532
+ console.log(chalk.gray(`\n This flow is local only. Run "lux flows publish ${flowId}" to publish to cloud.`));
521
533
  }
522
534
  } else {
523
- error('Failed to create workflow. Make sure you are authenticated.');
535
+ error('Failed to create flow. Make sure you are authenticated.');
524
536
  }
525
537
  break;
526
538
  }
527
539
 
528
540
  case 'save': {
529
- requireArgs(args.slice(1), 2, 'lux workflows save <id> <config-file>');
530
- const workflowId = args[1];
541
+ requireArgs(args.slice(1), 2, 'lux flows save <id> <config-file>');
542
+ const flowId = args[1];
531
543
  const configFile = args[2];
532
544
 
533
545
  // Load existing local flow
534
- const existingFlow = loadLocalFlow(workflowId);
546
+ const existingFlow = loadLocalFlow(flowId);
535
547
  if (!existingFlow) {
536
- error(`Workflow not found locally: ${workflowId}`);
537
- console.log(chalk.gray('Run "lux workflows sync" to sync from cloud first.'));
548
+ error(`Flow not found locally: ${flowId}`);
549
+ console.log(chalk.gray('Run "lux flows sync" to sync from cloud first.'));
538
550
  break;
539
551
  }
540
552
 
541
553
  info(`Reading config from: ${configFile}`);
542
554
  const configJson = readFile(configFile);
543
- const config = parseJson(configJson, 'workflow config');
555
+ const config = parseJson(configJson, 'flow config');
544
556
 
545
557
  // Update local flow with new config
546
558
  const updatedFlow = {
@@ -551,9 +563,9 @@ ${chalk.bold('Examples:')}
551
563
  updatedAt: new Date().toISOString(),
552
564
  };
553
565
 
554
- info(`Saving to local storage: ${workflowId}`);
555
- if (saveLocalFlow(workflowId, updatedFlow)) {
556
- success('Workflow saved locally!');
566
+ info(`Saving to local storage: ${flowId}`);
567
+ if (saveLocalFlow(flowId, updatedFlow)) {
568
+ success('Flow saved locally!');
557
569
  console.log(` Local Version: ${updatedFlow.localVersion}`);
558
570
 
559
571
  // Show sync status
@@ -564,22 +576,22 @@ ${chalk.bold('Examples:')}
564
576
  console.log(` Status: ${chalk.yellow('dirty')} (has unpublished changes)`);
565
577
  }
566
578
 
567
- console.log(chalk.gray(`\nRun "lux workflows publish ${workflowId}" to publish to cloud.`));
579
+ console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to publish to cloud.`));
568
580
  } else {
569
- error('Failed to save workflow locally.');
581
+ error('Failed to save flow locally.');
570
582
  }
571
583
  break;
572
584
  }
573
585
 
574
586
  case 'publish': {
575
- requireArgs(args.slice(1), 1, 'lux workflows publish <id>');
576
- const workflowId = args[1];
587
+ requireArgs(args.slice(1), 1, 'lux flows publish <id>');
588
+ const flowId = args[1];
577
589
 
578
590
  // Load local flow
579
- const localFlow = loadLocalFlow(workflowId);
591
+ const localFlow = loadLocalFlow(flowId);
580
592
  if (!localFlow) {
581
- error(`Workflow not found locally: ${workflowId}`);
582
- console.log(chalk.gray('Run "lux workflows sync" to sync from cloud first.'));
593
+ error(`Flow not found locally: ${flowId}`);
594
+ console.log(chalk.gray('Run "lux flows sync" to sync from cloud first.'));
583
595
  break;
584
596
  }
585
597
 
@@ -587,12 +599,12 @@ ${chalk.bold('Examples:')}
587
599
  if (localFlow.cloudVersion && localFlow.publishedVersion &&
588
600
  localFlow.cloudVersion > localFlow.publishedVersion &&
589
601
  localFlow.localVersion > localFlow.publishedVersion) {
590
- error('Cannot publish: workflow has conflicts with cloud version.');
602
+ error('Cannot publish: flow has conflicts with cloud version.');
591
603
  console.log(chalk.gray('Resolve conflicts in Lux Studio first.'));
592
604
  break;
593
605
  }
594
606
 
595
- info(`Publishing workflow: ${workflowId}`);
607
+ info(`Publishing flow: ${flowId}`);
596
608
 
597
609
  // Publish directly to cloud with config in request body
598
610
  const publishConfig = {
@@ -603,7 +615,7 @@ ${chalk.bold('Examples:')}
603
615
  };
604
616
 
605
617
  const { data } = await axios.post(
606
- `${apiUrl}/api/workflows/${workflowId}/publish`,
618
+ `${apiUrl}/api/workflows/${flowId}/publish`,
607
619
  {
608
620
  name: localFlow.name,
609
621
  description: localFlow.description,
@@ -631,9 +643,9 @@ ${chalk.bold('Examples:')}
631
643
  cloudPublishedAt: now,
632
644
  cloudStatus: 'published',
633
645
  };
634
- saveLocalFlow(workflowId, updatedFlow);
646
+ saveLocalFlow(flowId, updatedFlow);
635
647
 
636
- success('Workflow published!');
648
+ success('Flow published!');
637
649
  console.log(` Name: ${localFlow.name}`);
638
650
  console.log(` Version: ${newVersion}`);
639
651
  console.log(` Status: ${chalk.green('synced')}`);
@@ -641,17 +653,17 @@ ${chalk.bold('Examples:')}
641
653
  }
642
654
 
643
655
  case 'diff': {
644
- requireArgs(args.slice(1), 1, 'lux workflows diff <id>');
645
- const workflowId = args[1];
656
+ requireArgs(args.slice(1), 1, 'lux flows diff <id>');
657
+ const flowId = args[1];
646
658
 
647
659
  // Load local flow
648
- const localFlow = loadLocalFlow(workflowId);
660
+ const localFlow = loadLocalFlow(flowId);
649
661
  if (!localFlow) {
650
- error(`Workflow not found locally: ${workflowId}`);
662
+ error(`Flow not found locally: ${flowId}`);
651
663
  break;
652
664
  }
653
665
 
654
- info(`Comparing local vs published for: ${workflowId}`);
666
+ info(`Comparing local vs published for: ${flowId}`);
655
667
 
656
668
  const localConfig = {
657
669
  nodes: localFlow.nodes || [],
@@ -660,12 +672,12 @@ ${chalk.bold('Examples:')}
660
672
 
661
673
  // Check if never published
662
674
  if (!localFlow.publishedVersion) {
663
- console.log('\n📝 NEW WORKFLOW (not yet published)\n');
675
+ console.log('\n📝 NEW FLOW (not yet published)\n');
664
676
  console.log(` Name: ${localFlow.name}`);
665
677
  console.log(` Local Version: ${localFlow.localVersion || 1}`);
666
678
  console.log(` Nodes: ${localConfig.nodes.length}`);
667
679
  console.log(` Edges: ${localConfig.edges.length}`);
668
- console.log(chalk.gray(`\nRun "lux workflows publish ${workflowId}" to publish.\n`));
680
+ console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to publish.\n`));
669
681
  break;
670
682
  }
671
683
 
@@ -697,17 +709,17 @@ ${chalk.bold('Examples:')}
697
709
  console.log(` Published Version: ${localFlow.publishedVersion}`);
698
710
  console.log(` Nodes: ${localConfig.nodes.length}`);
699
711
  console.log(` Edges: ${localConfig.edges.length}`);
700
- console.log(chalk.gray(`\nRun "lux workflows publish ${workflowId}" to publish changes.\n`));
712
+ console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to publish changes.\n`));
701
713
  }
702
714
  break;
703
715
  }
704
716
 
705
717
  case 'webhook-url': {
706
- requireArgs(args.slice(1), 1, 'lux flow webhook-url <flow-id>');
707
- const workflowId = args[1];
718
+ requireArgs(args.slice(1), 1, 'lux flows webhook-url <flow-id>');
719
+ const flowId = args[1];
708
720
 
709
- info(`Getting webhook URL for: ${workflowId}`);
710
- const tokenData = await getWebhookToken(workflowId);
721
+ info(`Getting webhook URL for: ${flowId}`);
722
+ const tokenData = await getWebhookToken(flowId);
711
723
 
712
724
  console.log(`\n📍 Webhook URL:\n${tokenData.webhookUrl}\n`);
713
725
  console.log(`📊 Status:`);
@@ -715,17 +727,17 @@ ${chalk.bold('Examples:')}
715
727
  console.log(` Webhook Trigger Enabled: ${tokenData.webhookTrigger ? '✅ Yes' : '❌ No'}\n`);
716
728
 
717
729
  if (!tokenData.formatConfirmed) {
718
- console.log(chalk.yellow('ℹ️ Run "lux flow webhook-listen" to capture a sample webhook'));
730
+ console.log(chalk.yellow('ℹ️ Run "lux flows webhook-listen" to capture a sample webhook'));
719
731
  }
720
732
  break;
721
733
  }
722
734
 
723
735
  case 'webhook-listen': {
724
- requireArgs(args.slice(1), 1, 'lux flow webhook-listen <flow-id>');
725
- const workflowId = args[1];
736
+ requireArgs(args.slice(1), 1, 'lux flows webhook-listen <flow-id>');
737
+ const flowId = args[1];
726
738
 
727
- info(`Starting webhook listening session for: ${workflowId}`);
728
- const tokenData = await getWebhookToken(workflowId);
739
+ info(`Starting webhook listening session for: ${flowId}`);
740
+ const tokenData = await getWebhookToken(flowId);
729
741
 
730
742
  // Start listening session
731
743
  await axios.post(`${WEBHOOK_WORKER_URL}/listening/start/${tokenData.token}`);
@@ -734,15 +746,15 @@ ${chalk.bold('Examples:')}
734
746
  console.log(`\n📡 Now listening for webhooks at:\n${tokenData.webhookUrl}\n`);
735
747
  console.log('Session expires in 5 minutes.');
736
748
  console.log(`\nSend a webhook request to the URL above, then run:`);
737
- console.log(chalk.white(` lux flow webhook-poll ${workflowId}\n`));
749
+ console.log(chalk.white(` lux flows webhook-poll ${flowId}\n`));
738
750
  break;
739
751
  }
740
752
 
741
753
  case 'webhook-poll': {
742
- requireArgs(args.slice(1), 1, 'lux flow webhook-poll <flow-id>');
743
- const workflowId = args[1];
754
+ requireArgs(args.slice(1), 1, 'lux flows webhook-poll <flow-id>');
755
+ const flowId = args[1];
744
756
 
745
- const tokenData = await getWebhookToken(workflowId);
757
+ const tokenData = await getWebhookToken(flowId);
746
758
  const pollData = await getCapturedPayload(tokenData.token);
747
759
 
748
760
  // Return raw JSON response
@@ -751,20 +763,20 @@ ${chalk.bold('Examples:')}
751
763
  }
752
764
 
753
765
  case 'webhook-accept': {
754
- requireArgs(args.slice(1), 1, 'lux flow webhook-accept <flow-id>');
755
- const workflowId = args[1];
766
+ requireArgs(args.slice(1), 1, 'lux flows webhook-accept <flow-id>');
767
+ const flowId = args[1];
756
768
 
757
- info(`Accepting webhook format for: ${workflowId}`);
769
+ info(`Accepting webhook format for: ${flowId}`);
758
770
 
759
771
  // Step 1: Get webhook token
760
- const tokenData = await getWebhookToken(workflowId);
772
+ const tokenData = await getWebhookToken(flowId);
761
773
 
762
774
  // Step 2: Poll for captured payload
763
775
  const pollData = await getCapturedPayload(tokenData.token);
764
776
 
765
777
  // Check if payload was captured
766
778
  if (pollData.waiting || !pollData.payload) {
767
- error('No webhook captured. Run "lux flow webhook-listen" first and send a test webhook.');
779
+ error('No webhook captured. Run "lux flows webhook-listen" first and send a test webhook.');
768
780
  break;
769
781
  }
770
782
 
@@ -839,11 +851,11 @@ ${chalk.bold('Examples:')}
839
851
  }
840
852
 
841
853
  case 'webhook-decline': {
842
- requireArgs(args.slice(1), 1, 'lux flow webhook-decline <flow-id>');
843
- const workflowId = args[1];
854
+ requireArgs(args.slice(1), 1, 'lux flows webhook-decline <flow-id>');
855
+ const flowId = args[1];
844
856
 
845
- info(`Declining webhook format for: ${workflowId}`);
846
- const tokenData = await getWebhookToken(workflowId);
857
+ info(`Declining webhook format for: ${flowId}`);
858
+ const tokenData = await getWebhookToken(flowId);
847
859
 
848
860
  // Clear webhook schema from database
849
861
  info('Clearing webhook schema...');
@@ -858,33 +870,57 @@ ${chalk.bold('Examples:')}
858
870
  success('Webhook format declined!');
859
871
  console.log('\n✅ Schema cleared from database');
860
872
  console.log('✅ Listening session has been reset');
861
- console.log(`\nRun "lux flow webhook-listen ${workflowId}" to try again.\n`);
873
+ console.log(`\nRun "lux flows webhook-listen ${flowId}" to try again.\n`);
862
874
  break;
863
875
  }
864
876
 
865
877
  // ============ EXECUTION HISTORY COMMANDS ============
866
878
 
867
879
  case 'executions': {
868
- requireArgs(args.slice(1), 1, 'lux flow executions <flow-id> [--limit N]');
880
+ requireArgs(args.slice(1), 1, 'lux flows executions <flow-id> [--limit N]');
869
881
  const flowId = args[1];
870
882
 
871
883
  // Parse --limit flag
872
884
  const limitIndex = args.indexOf('--limit');
873
885
  const limit = limitIndex !== -1 && args[limitIndex + 1] ? parseInt(args[limitIndex + 1], 10) : 20;
886
+ const jsonOutput = args.includes('--json');
874
887
 
875
- info(`Fetching execution history for: ${flowId}`);
888
+ const projectId = getProjectId();
889
+ if (!projectId || projectId === 'default') {
890
+ error('No project selected. Open a project in Lux Studio first.');
891
+ break;
892
+ }
876
893
 
877
- const { data } = await axios.get(
878
- `${apiUrl}/api/flows/${flowId}/executions?limit=${limit}`,
879
- { headers: getAuthHeaders() }
880
- );
894
+ info(`Loading executions for: ${flowId}`);
895
+
896
+ const workflowsUrl = 'https://lux-workflows.jason-a5d.workers.dev';
897
+ const url = `${workflowsUrl}/api/flows/${flowId}/executions?limit=${limit}&offset=0`;
898
+
899
+ const { data } = await axios.get(url, {
900
+ headers: {
901
+ ...getAuthHeaders(),
902
+ 'X-Project-Id': projectId,
903
+ },
904
+ });
905
+
906
+ const executions = data.executions || [];
907
+
908
+ if (jsonOutput) {
909
+ console.log(formatJson(executions));
910
+ break;
911
+ }
881
912
 
882
- if (!data.executions || data.executions.length === 0) {
883
- console.log(chalk.dim('\nNo executions found for this flow.\n'));
913
+ if (executions.length === 0) {
914
+ console.log('\n(No executions found for this flow)');
915
+ console.log(chalk.gray('Run the flow from the interface or use "lux flows test" to create an execution.\n'));
884
916
  break;
885
917
  }
886
918
 
887
- console.log(`\n📊 Execution History for: ${flowId}\n`);
919
+ // Load flow name
920
+ const flow = loadLocalFlow(flowId);
921
+ const flowName = flow?.name || flowId;
922
+
923
+ console.log(`\n📊 Execution History for "${flowName}" (${executions.length})\n`);
888
924
 
889
925
  // Status colors
890
926
  const statusColors = {
@@ -894,9 +930,9 @@ ${chalk.bold('Examples:')}
894
930
  cancelled: chalk.yellow,
895
931
  };
896
932
 
897
- const formatted = data.executions.map((exec) => {
933
+ const formatted = executions.map((exec) => {
898
934
  const statusColor = statusColors[exec.status] || chalk.white;
899
- const duration = exec.durationMs ? `${exec.durationMs}ms` : '-';
935
+ const duration = exec.duration ? `${(exec.duration / 1000).toFixed(1)}s` : (exec.durationMs ? `${exec.durationMs}ms` : '-');
900
936
  const startedAt = exec.startedAt ? new Date(exec.startedAt).toLocaleString() : '-';
901
937
  const nodeCount = exec.nodeExecutions?.length || 0;
902
938
 
@@ -914,32 +950,53 @@ ${chalk.bold('Examples:')}
914
950
  formatTable(formatted);
915
951
 
916
952
  if (data.pagination?.hasMore) {
917
- console.log(chalk.dim(`\nShowing ${data.executions.length} of ${data.pagination.total} executions.`));
953
+ console.log(chalk.dim(`\nShowing ${executions.length} of ${data.pagination.total} executions.`));
918
954
  console.log(chalk.dim(`Use --limit N to see more.\n`));
919
955
  }
920
956
 
921
- console.log(chalk.gray(`\nTo see full details: lux flow execution ${flowId} <execution-id>\n`));
957
+ console.log(chalk.gray(`\nTo see full details: lux flows execution ${flowId} <execution-id>\n`));
922
958
  break;
923
959
  }
924
960
 
925
961
  case 'execution': {
926
- requireArgs(args.slice(1), 2, 'lux flow execution <flow-id> <execution-id>');
962
+ requireArgs(args.slice(1), 2, 'lux flows execution <flow-id> <execution-id>');
927
963
  const flowId = args[1];
928
964
  const executionId = args[2];
965
+ const jsonOutput = args.includes('--json');
929
966
 
930
- info(`Fetching execution: ${executionId}`);
967
+ const projectId = getProjectId();
968
+ if (!projectId || projectId === 'default') {
969
+ error('No project selected. Open a project in Lux Studio first.');
970
+ break;
971
+ }
931
972
 
932
- const { data } = await axios.get(
933
- `${apiUrl}/api/flows/${flowId}/executions/${executionId}`,
934
- { headers: getAuthHeaders() }
935
- );
973
+ info(`Loading execution: ${executionId}`);
936
974
 
937
- if (!data.execution) {
975
+ const workflowsUrl = 'https://lux-workflows.jason-a5d.workers.dev';
976
+ const url = `${workflowsUrl}/api/flows/${flowId}/executions/${executionId}`;
977
+
978
+ const { data } = await axios.get(url, {
979
+ headers: {
980
+ ...getAuthHeaders(),
981
+ 'X-Project-Id': projectId,
982
+ },
983
+ });
984
+
985
+ const exec = data.execution;
986
+ if (!exec) {
938
987
  error(`Execution not found: ${executionId}`);
939
988
  break;
940
989
  }
941
990
 
942
- const exec = data.execution;
991
+ if (jsonOutput) {
992
+ console.log(formatJson(exec));
993
+ break;
994
+ }
995
+
996
+ // Load flow name
997
+ const flow = loadLocalFlow(flowId);
998
+ const flowName = flow?.name || flowId;
999
+
943
1000
  const statusColors = {
944
1001
  completed: chalk.green,
945
1002
  running: chalk.cyan,
@@ -950,10 +1007,10 @@ ${chalk.bold('Examples:')}
950
1007
 
951
1008
  console.log(`\n📋 Execution Details\n`);
952
1009
  console.log(` ID: ${exec.id}`);
953
- console.log(` Flow: ${exec.flowId}`);
1010
+ console.log(` Flow: ${flowName}`);
954
1011
  console.log(` Status: ${statusColor(exec.status)}`);
955
1012
  console.log(` Trigger: ${exec.triggerType || '-'}`);
956
- console.log(` Duration: ${exec.durationMs ? exec.durationMs + 'ms' : '-'}`);
1013
+ console.log(` Duration: ${exec.duration ? `${(exec.duration / 1000).toFixed(1)}s` : (exec.durationMs ? `${exec.durationMs}ms` : '-')}`);
957
1014
  console.log(` Started: ${exec.startedAt ? new Date(exec.startedAt).toLocaleString() : '-'}`);
958
1015
  console.log(` Completed: ${exec.completedAt ? new Date(exec.completedAt).toLocaleString() : '-'}`);
959
1016
  console.log(` Version: ${exec.flowVersion || '-'}`);
@@ -974,6 +1031,9 @@ ${chalk.bold('Examples:')}
974
1031
  if (exec.outputData && Object.keys(exec.outputData).length > 0) {
975
1032
  console.log(`\n${chalk.cyan('📤 Output Data:')}`);
976
1033
  console.log(formatJson(exec.outputData));
1034
+ } else if (exec.output) {
1035
+ console.log(`\n${chalk.cyan('📤 Output:')}`);
1036
+ console.log(formatJson(exec.output));
977
1037
  }
978
1038
 
979
1039
  // Show node executions summary
@@ -983,7 +1043,7 @@ ${chalk.bold('Examples:')}
983
1043
  const nodeFormatted = exec.nodeExecutions.map((node) => {
984
1044
  const nodeStatus = node.status || 'unknown';
985
1045
  const nodeStatusColor = statusColors[nodeStatus] || chalk.white;
986
- const nodeDuration = node.durationMs ? `${node.durationMs}ms` : '-';
1046
+ const nodeDuration = node.duration ? `${node.duration}ms` : (node.durationMs ? `${node.durationMs}ms` : '-');
987
1047
 
988
1048
  return {
989
1049
  node_id: node.nodeId || node.id,
@@ -996,24 +1056,36 @@ ${chalk.bold('Examples:')}
996
1056
 
997
1057
  formatTable(nodeFormatted);
998
1058
 
999
- console.log(chalk.gray(`\nTo see node details: lux flow node ${flowId} ${executionId} <node-id>\n`));
1059
+ console.log(chalk.gray(`\nTo see node details: lux flows node ${flowId} ${executionId} <node-id>\n`));
1000
1060
  }
1001
1061
 
1002
1062
  break;
1003
1063
  }
1004
1064
 
1005
1065
  case 'node': {
1006
- requireArgs(args.slice(1), 3, 'lux flow node <flow-id> <execution-id> <node-id>');
1066
+ requireArgs(args.slice(1), 3, 'lux flows node <flow-id> <execution-id> <node-id>');
1007
1067
  const flowId = args[1];
1008
1068
  const executionId = args[2];
1009
1069
  const nodeId = args[3];
1070
+ const jsonOutput = args.includes('--json');
1071
+
1072
+ const projectId = getProjectId();
1073
+ if (!projectId || projectId === 'default') {
1074
+ error('No project selected. Open a project in Lux Studio first.');
1075
+ break;
1076
+ }
1010
1077
 
1011
1078
  info(`Fetching node execution: ${nodeId}`);
1012
1079
 
1013
- const { data } = await axios.get(
1014
- `${apiUrl}/api/flows/${flowId}/executions/${executionId}`,
1015
- { headers: getAuthHeaders() }
1016
- );
1080
+ const workflowsUrl = 'https://lux-workflows.jason-a5d.workers.dev';
1081
+ const url = `${workflowsUrl}/api/flows/${flowId}/executions/${executionId}`;
1082
+
1083
+ const { data } = await axios.get(url, {
1084
+ headers: {
1085
+ ...getAuthHeaders(),
1086
+ 'X-Project-Id': projectId,
1087
+ },
1088
+ });
1017
1089
 
1018
1090
  if (!data.execution) {
1019
1091
  error(`Execution not found: ${executionId}`);
@@ -1034,6 +1106,11 @@ ${chalk.bold('Examples:')}
1034
1106
  break;
1035
1107
  }
1036
1108
 
1109
+ if (jsonOutput) {
1110
+ console.log(formatJson(nodeExec));
1111
+ break;
1112
+ }
1113
+
1037
1114
  const statusColors = {
1038
1115
  completed: chalk.green,
1039
1116
  running: chalk.cyan,
@@ -1048,7 +1125,7 @@ ${chalk.bold('Examples:')}
1048
1125
  console.log(` Type: ${nodeExec.nodeType || '-'}`);
1049
1126
  console.log(` Label: ${nodeExec.label || '-'}`);
1050
1127
  console.log(` Status: ${statusColor(nodeExec.status || 'unknown')}`);
1051
- console.log(` Duration: ${nodeExec.durationMs ? nodeExec.durationMs + 'ms' : '-'}`);
1128
+ console.log(` Duration: ${nodeExec.duration ? `${nodeExec.duration}ms` : (nodeExec.durationMs ? `${nodeExec.durationMs}ms` : '-')}`);
1052
1129
  console.log(` Started: ${nodeExec.startedAt ? new Date(nodeExec.startedAt).toLocaleString() : '-'}`);
1053
1130
  console.log(` Completed: ${nodeExec.completedAt ? new Date(nodeExec.completedAt).toLocaleString() : '-'}`);
1054
1131
 
@@ -1085,9 +1162,208 @@ ${chalk.bold('Examples:')}
1085
1162
  break;
1086
1163
  }
1087
1164
 
1165
+ case 'last': {
1166
+ requireArgs(args.slice(1), 1, 'lux flows last <flow-id>');
1167
+ const flowId = args[1];
1168
+ const jsonOutput = args.includes('--json');
1169
+
1170
+ const projectId = getProjectId();
1171
+ if (!projectId || projectId === 'default') {
1172
+ error('No project selected. Open a project in Lux Studio first.');
1173
+ break;
1174
+ }
1175
+
1176
+ info(`Loading last execution for: ${flowId}`);
1177
+
1178
+ const workflowsUrl = 'https://lux-workflows.jason-a5d.workers.dev';
1179
+ const listUrl = `${workflowsUrl}/api/flows/${flowId}/executions?limit=1&offset=0`;
1180
+
1181
+ const { data: listData } = await axios.get(listUrl, {
1182
+ headers: {
1183
+ ...getAuthHeaders(),
1184
+ 'X-Project-Id': projectId,
1185
+ },
1186
+ });
1187
+
1188
+ const executions = listData.executions || [];
1189
+ if (executions.length === 0) {
1190
+ console.log('\n(No executions found for this flow)');
1191
+ console.log(chalk.gray('Run the flow from the interface or use "lux flows test" to create an execution.\n'));
1192
+ break;
1193
+ }
1194
+
1195
+ const lastExecId = executions[0].id;
1196
+ const execUrl = `${workflowsUrl}/api/flows/${flowId}/executions/${lastExecId}`;
1197
+
1198
+ const { data: execData } = await axios.get(execUrl, {
1199
+ headers: {
1200
+ ...getAuthHeaders(),
1201
+ 'X-Project-Id': projectId,
1202
+ },
1203
+ });
1204
+
1205
+ const exec = execData.execution;
1206
+
1207
+ if (jsonOutput) {
1208
+ console.log(formatJson(exec));
1209
+ break;
1210
+ }
1211
+
1212
+ // Load flow name
1213
+ const flow = loadLocalFlow(flowId);
1214
+ const flowName = flow?.name || flowId;
1215
+
1216
+ const status = exec.status === 'completed' ? chalk.green('✓ Success') :
1217
+ exec.status === 'failed' ? chalk.red('✗ Failed') :
1218
+ chalk.yellow('⏳ ' + exec.status);
1219
+ const timestamp = new Date(exec.startedAt).toLocaleString();
1220
+ const duration = exec.duration ? `${(exec.duration / 1000).toFixed(1)}s` : '-';
1221
+
1222
+ console.log(`\nLast Execution for "${flowName}"\n`);
1223
+ console.log(` Status: ${status}`);
1224
+ console.log(` Started: ${timestamp}`);
1225
+ console.log(` Duration: ${duration}`);
1226
+ console.log(` ID: ${exec.id}`);
1227
+
1228
+ if (exec.nodeExecutions && exec.nodeExecutions.length > 0) {
1229
+ console.log(`\n Node Results:`);
1230
+ exec.nodeExecutions.forEach((node, i) => {
1231
+ const nodeStatus = node.status === 'completed' ? chalk.green('✓') :
1232
+ node.status === 'failed' ? chalk.red('✗') :
1233
+ chalk.yellow('⏳');
1234
+ const nodeDuration = node.duration ? `${node.duration}ms` : '-';
1235
+ let outputPreview = '';
1236
+ if (node.output) {
1237
+ const outputStr = JSON.stringify(node.output);
1238
+ outputPreview = outputStr.length > 60 ? outputStr.substring(0, 60) + '...' : outputStr;
1239
+ outputPreview = chalk.gray(` → ${outputPreview}`);
1240
+ }
1241
+ if (node.error) {
1242
+ outputPreview = chalk.red(` → Error: ${node.error}`);
1243
+ }
1244
+ console.log(` ${i + 1}. ${node.nodeId} ${nodeStatus} ${nodeDuration}${outputPreview}`);
1245
+ });
1246
+ }
1247
+
1248
+ if (exec.output) {
1249
+ console.log(`\n Final Output:`);
1250
+ console.log(formatJson(exec.output).split('\n').map(l => ' ' + l).join('\n'));
1251
+ }
1252
+
1253
+ if (exec.error) {
1254
+ console.log(chalk.red(`\n Error: ${exec.error}`));
1255
+ }
1256
+
1257
+ console.log('');
1258
+ break;
1259
+ }
1260
+
1261
+ case 'test': {
1262
+ requireArgs(args.slice(1), 1, 'lux flows test <flow-id> [--data <json>] [--data-file <path>]');
1263
+ const flowId = args[1];
1264
+ const jsonOutput = args.includes('--json');
1265
+
1266
+ // Parse --data or --data-file arguments
1267
+ let testData = {};
1268
+ const dataIndex = args.indexOf('--data');
1269
+ const dataFileIndex = args.indexOf('--data-file');
1270
+
1271
+ if (dataIndex !== -1 && args[dataIndex + 1]) {
1272
+ testData = parseJson(args[dataIndex + 1], 'test data');
1273
+ } else if (dataFileIndex !== -1 && args[dataFileIndex + 1]) {
1274
+ const fileContent = readFile(args[dataFileIndex + 1]);
1275
+ testData = parseJson(fileContent, 'test data file');
1276
+ }
1277
+
1278
+ const projectId = getProjectId();
1279
+ if (!projectId || projectId === 'default') {
1280
+ error('No project selected. Open a project in Lux Studio first.');
1281
+ break;
1282
+ }
1283
+
1284
+ // Load flow from local storage
1285
+ const flowData = loadLocalFlow(flowId);
1286
+ if (!flowData) {
1287
+ error(`Flow not found locally: ${flowId}`);
1288
+ console.log(chalk.gray('Run "lux flows sync" to sync from cloud first.'));
1289
+ break;
1290
+ }
1291
+
1292
+ info(`Testing flow: ${flowData.name}`);
1293
+
1294
+ const executeUrl = `${apiUrl}/api/flows/execute`;
1295
+
1296
+ const startTime = Date.now();
1297
+ const { data: result } = await axios.post(executeUrl, {
1298
+ flowId,
1299
+ nodes: flowData.nodes || [],
1300
+ edges: flowData.edges || [],
1301
+ context: testData,
1302
+ projectId,
1303
+ }, {
1304
+ headers: {
1305
+ ...getAuthHeaders(),
1306
+ 'Content-Type': 'application/json',
1307
+ 'X-Project-Id': projectId,
1308
+ },
1309
+ });
1310
+
1311
+ const executionTime = Date.now() - startTime;
1312
+
1313
+ if (jsonOutput) {
1314
+ console.log(formatJson({
1315
+ success: result.success,
1316
+ data: result.data,
1317
+ statusCode: result.statusCode,
1318
+ nodeExecutions: result.nodeExecutions,
1319
+ executionTime,
1320
+ }));
1321
+ break;
1322
+ }
1323
+
1324
+ console.log(`\nTesting flow: ${flowData.name}\n`);
1325
+
1326
+ if (result.nodeExecutions && result.nodeExecutions.length > 0) {
1327
+ result.nodeExecutions.forEach((node) => {
1328
+ const nodeStatus = node.status === 'completed' ? chalk.green('✓') :
1329
+ node.status === 'failed' ? chalk.red('✗') :
1330
+ chalk.yellow('⏳');
1331
+ const nodeDuration = node.duration ? `${node.duration}ms` : '-';
1332
+ let outputPreview = '';
1333
+ if (node.output) {
1334
+ const outputStr = JSON.stringify(node.output);
1335
+ if (outputStr.length > 40) {
1336
+ outputPreview = chalk.gray(` → ${outputStr.substring(0, 40)}...`);
1337
+ } else {
1338
+ outputPreview = chalk.gray(` → ${outputStr}`);
1339
+ }
1340
+ }
1341
+ if (node.error) {
1342
+ outputPreview = chalk.red(` → ${node.error}`);
1343
+ }
1344
+ console.log(` ${nodeStatus} ${node.nodeId} ${nodeDuration}${outputPreview}`);
1345
+ });
1346
+ }
1347
+
1348
+ const resultStatus = result.success ? chalk.green('Success') : chalk.red('Failed');
1349
+ console.log(`\n Result: ${resultStatus} (${(executionTime / 1000).toFixed(1)}s)`);
1350
+
1351
+ if (result.data !== undefined) {
1352
+ console.log(`\n Output:`);
1353
+ console.log(formatJson(result.data).split('\n').map(l => ' ' + l).join('\n'));
1354
+ }
1355
+
1356
+ if (!result.success && result.error) {
1357
+ console.log(chalk.red(`\n Error: ${result.error}`));
1358
+ }
1359
+
1360
+ console.log('');
1361
+ break;
1362
+ }
1363
+
1088
1364
  default:
1089
1365
  error(
1090
- `Unknown command: ${command}\n\nRun 'lux workflows' to see available commands`
1366
+ `Unknown command: ${command}\n\nRun 'lux flows' to see available commands`
1091
1367
  );
1092
1368
  }
1093
1369
  } catch (err) {
@@ -1097,4 +1373,4 @@ ${chalk.bold('Examples:')}
1097
1373
  }
1098
1374
  }
1099
1375
 
1100
- module.exports = { handleWorkflows };
1376
+ module.exports = { handleFlows };
@@ -238,6 +238,56 @@ function formatTime(isoString) {
238
238
  return date.toLocaleString();
239
239
  }
240
240
 
241
+ /**
242
+ * Restart a specific server by sending a signal file
243
+ * The Lux Studio Electron app watches for these signal files
244
+ */
245
+ async function restartServer(appNameOrId) {
246
+ if (!appNameOrId) {
247
+ console.log(chalk.red('\nPlease specify an app name or ID to restart.'));
248
+ console.log(chalk.dim('Usage: lux servers restart <app-name-or-id>\n'));
249
+ return;
250
+ }
251
+
252
+ // Verify ports to clean up stale entries
253
+ const registry = await loadRegistry(true);
254
+
255
+ if (!registry.servers || registry.servers.length === 0) {
256
+ console.log(chalk.yellow('\nNo dev servers are currently running.'));
257
+ console.log(chalk.dim('Start a server in Lux Studio to restart it.\n'));
258
+ return;
259
+ }
260
+
261
+ // Find the server by name or ID (case-insensitive partial match)
262
+ const searchTerm = appNameOrId.toLowerCase();
263
+ const server = registry.servers.find(s =>
264
+ s.appId.toLowerCase().includes(searchTerm) ||
265
+ (s.appName && s.appName.toLowerCase().includes(searchTerm))
266
+ );
267
+
268
+ if (!server) {
269
+ console.log(chalk.red(`\nNo running server found matching "${appNameOrId}".`));
270
+ console.log(chalk.dim('\nAvailable servers:'));
271
+ registry.servers.forEach(s => {
272
+ console.log(chalk.dim(` - ${s.appName || s.appId}`));
273
+ });
274
+ console.log('');
275
+ return;
276
+ }
277
+
278
+ // Write signal file to trigger restart
279
+ const signalDir = path.join(LUX_STUDIO_DIR, 'restart-signals');
280
+ if (!fs.existsSync(signalDir)) {
281
+ fs.mkdirSync(signalDir, { recursive: true });
282
+ }
283
+
284
+ const signalFile = path.join(signalDir, `${server.appId}.signal`);
285
+ fs.writeFileSync(signalFile, Date.now().toString());
286
+
287
+ console.log(chalk.green(`\n✓ Restart signal sent for ${server.appName || server.appId}`));
288
+ console.log(chalk.dim(` The server will restart momentarily.\n`));
289
+ }
290
+
241
291
  /**
242
292
  * Handle the servers command
243
293
  */
@@ -249,10 +299,16 @@ async function handleServers(args = []) {
249
299
  return;
250
300
  }
251
301
 
302
+ if (subcommand === 'restart') {
303
+ await restartServer(args[1]);
304
+ return;
305
+ }
306
+
252
307
  // Show help
253
308
  console.log(chalk.cyan('\nUsage:'));
254
309
  console.log(' lux servers List all running dev servers');
255
310
  console.log(' lux servers list List all running dev servers');
311
+ console.log(' lux servers restart <app> Restart a specific dev server');
256
312
  console.log('');
257
313
  }
258
314
 
@@ -408,4 +464,5 @@ module.exports = {
408
464
  listServers,
409
465
  getLogs,
410
466
  getBrowserConsoleLogs,
467
+ restartServer,
411
468
  };
@@ -0,0 +1,137 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const {
5
+ error,
6
+ success,
7
+ info,
8
+ } = require('../lib/helpers');
9
+
10
+ // Paths to the pro-docs files (relative to project root)
11
+ const PRO_DOCS_DIR = 'knowledge/pro-docs';
12
+ const SPEC_FILE = 'project-spec.md';
13
+
14
+ function getProjectRoot() {
15
+ // Walk up from cwd to find .lux folder (project root)
16
+ let dir = process.cwd();
17
+ while (dir !== path.dirname(dir)) {
18
+ if (fs.existsSync(path.join(dir, '.lux'))) {
19
+ return dir;
20
+ }
21
+ dir = path.dirname(dir);
22
+ }
23
+ return process.cwd();
24
+ }
25
+
26
+ function getSpecPath() {
27
+ return path.join(getProjectRoot(), PRO_DOCS_DIR, SPEC_FILE);
28
+ }
29
+
30
+ function ensureProDocsDir() {
31
+ const dir = path.join(getProjectRoot(), PRO_DOCS_DIR);
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ }
35
+ return dir;
36
+ }
37
+
38
+ function readFile(filePath) {
39
+ if (!fs.existsSync(filePath)) {
40
+ return null;
41
+ }
42
+ return fs.readFileSync(filePath, 'utf-8');
43
+ }
44
+
45
+ function writeFile(filePath, content) {
46
+ ensureProDocsDir();
47
+ fs.writeFileSync(filePath, content, 'utf-8');
48
+ }
49
+
50
+ async function handleSpec(args) {
51
+ const command = args[0];
52
+
53
+ if (!command || command === 'show') {
54
+ // Show project spec
55
+ const content = readFile(getSpecPath());
56
+ if (!content) {
57
+ console.log(chalk.yellow('\nNo project-spec.md found.'));
58
+ console.log(`Run ${chalk.cyan('lux spec init')} to create one.\n`);
59
+ return;
60
+ }
61
+ console.log('\n' + content);
62
+ return;
63
+ }
64
+
65
+ if (command === 'init') {
66
+ const specPath = getSpecPath();
67
+ if (fs.existsSync(specPath)) {
68
+ console.log(chalk.yellow('\nproject-spec.md already exists.'));
69
+ console.log(`Run ${chalk.cyan('lux spec edit')} to modify it.\n`);
70
+ return;
71
+ }
72
+
73
+ const template = `# Project Spec
74
+
75
+ ## Overview
76
+ <!-- What is this project? Why does it exist? -->
77
+
78
+ ## Core Features
79
+
80
+ ### Planned
81
+ - [ ] Feature 1
82
+ - [ ] Feature 2
83
+ - [ ] Feature 3
84
+
85
+ ## Technical Stack
86
+ <!-- Key technologies, frameworks, architecture decisions -->
87
+
88
+ ## Key Decisions
89
+ <!-- Important decisions made and why -->
90
+
91
+ ## Out of Scope
92
+ <!-- What we're NOT building (helps keep focus) -->
93
+ `;
94
+ writeFile(specPath, template);
95
+ success('Created project-spec.md');
96
+ console.log(` Path: ${specPath}\n`);
97
+ return;
98
+ }
99
+
100
+ if (command === 'edit') {
101
+ const specPath = getSpecPath();
102
+ if (!fs.existsSync(specPath)) {
103
+ error('No project-spec.md found. Run "lux spec init" first.');
104
+ return;
105
+ }
106
+
107
+ // Open in default editor
108
+ const editor = process.env.EDITOR || 'code';
109
+ const { spawn } = require('child_process');
110
+ spawn(editor, [specPath], { stdio: 'inherit', detached: true });
111
+ info(`Opening ${specPath} in ${editor}`);
112
+ return;
113
+ }
114
+
115
+ if (command === 'path') {
116
+ console.log(getSpecPath());
117
+ return;
118
+ }
119
+
120
+ // Show help
121
+ console.log(`
122
+ ${chalk.bold('Usage:')} lux spec <command>
123
+
124
+ ${chalk.bold('Commands:')}
125
+ show Show project spec (default)
126
+ init Create a new project-spec.md
127
+ edit Open project-spec.md in editor
128
+ path Print path to project-spec.md
129
+
130
+ ${chalk.bold('Examples:')}
131
+ lux spec
132
+ lux spec init
133
+ lux spec edit
134
+ `);
135
+ }
136
+
137
+ module.exports = { handleSpec };
package/lib/config.js CHANGED
@@ -201,6 +201,7 @@ function loadLocalFlow(flowId) {
201
201
 
202
202
  /**
203
203
  * Save a flow to local storage
204
+ * Matches Electron storage service pattern: main file + separate .deployed.json
204
205
  */
205
206
  function saveLocalFlow(flowId, flowData) {
206
207
  const flowsDir = getFlowsDir();
@@ -211,8 +212,22 @@ function saveLocalFlow(flowId, flowData) {
211
212
  fs.mkdirSync(flowsDir, { recursive: true });
212
213
  }
213
214
 
215
+ // Extract deployed data (same pattern as Electron storage service)
216
+ const { deployedNodes, deployedEdges, ...flowWithoutDeployed } = flowData;
217
+
218
+ // Write main flow file (without deployed data)
214
219
  const flowPath = path.join(flowsDir, `${flowId}.json`);
215
- fs.writeFileSync(flowPath, JSON.stringify(flowData, null, 2));
220
+ fs.writeFileSync(flowPath, JSON.stringify(flowWithoutDeployed, null, 2));
221
+
222
+ // Write deployed data to separate file (if present)
223
+ if (deployedNodes || deployedEdges) {
224
+ const deployedPath = path.join(flowsDir, `${flowId}.deployed.json`);
225
+ fs.writeFileSync(deployedPath, JSON.stringify({
226
+ deployedNodes: deployedNodes || [],
227
+ deployedEdges: deployedEdges || [],
228
+ }, null, 2));
229
+ }
230
+
216
231
  return true;
217
232
  }
218
233
 
package/lux.js CHANGED
@@ -11,7 +11,7 @@ const { dev } = require('./commands/dev');
11
11
  const { handleInterface } = require('./commands/interface');
12
12
  const { handleData } = require('./commands/data');
13
13
  const { handleStorage } = require('./commands/storage');
14
- const { handleWorkflows } = require('./commands/workflows');
14
+ const { handleFlows } = require('./commands/flows');
15
15
  const { handleSecrets } = require('./commands/secrets');
16
16
  const { handleAgents } = require('./commands/agents');
17
17
  const { handleKnowledge } = require('./commands/knowledge');
@@ -23,6 +23,7 @@ const { handleServers, handleLogs } = require('./commands/servers');
23
23
  const { handleTest } = require('./commands/webview');
24
24
  const { handleABTests } = require('./commands/ab-tests');
25
25
  const { handleTools } = require('./commands/tools');
26
+ const { handleSpec } = require('./commands/spec');
26
27
 
27
28
  program
28
29
  .name('lux')
@@ -95,14 +96,14 @@ program
95
96
  handleStorage(subcommand ? [subcommand, ...(args || [])] : []);
96
97
  });
97
98
 
98
- // Workflows commands (with short aliases 'flow' and 'f')
99
+ // Flows commands (with short alias 'f')
99
100
  program
100
- .command('workflows [subcommand] [args...]')
101
- .aliases(['flow', 'f'])
102
- .description('Workflow management (list, get, create/init, save, publish, diff)')
101
+ .command('flows [subcommand] [args...]')
102
+ .alias('f')
103
+ .description('Flow management (list, get, create/init, save, publish, diff)')
103
104
  .allowUnknownOption()
104
105
  .action((subcommand, args) => {
105
- handleWorkflows(subcommand ? [subcommand, ...(args || [])] : []);
106
+ handleFlows(subcommand ? [subcommand, ...(args || [])] : []);
106
107
  });
107
108
 
108
109
  // Secrets commands
@@ -192,12 +193,13 @@ program
192
193
  handleProject(subcommand ? [subcommand, ...(args || [])] : []);
193
194
  });
194
195
 
195
- // Servers command - list running dev servers
196
+ // Servers command - list/restart running dev servers
196
197
  program
197
- .command('servers [subcommand]')
198
- .description('List running dev servers started from Lux Studio')
199
- .action((subcommand) => {
200
- handleServers(subcommand ? [subcommand] : []);
198
+ .command('servers [subcommand] [args...]')
199
+ .description('Manage dev servers (list, restart)')
200
+ .allowUnknownOption()
201
+ .action((subcommand, args) => {
202
+ handleServers(subcommand ? [subcommand, ...(args || [])] : []);
201
203
  });
202
204
 
203
205
  // Logs command - view dev server logs
@@ -275,4 +277,13 @@ program
275
277
  await startPreview(interfaceId);
276
278
  });
277
279
 
280
+ // Project spec command
281
+ program
282
+ .command('spec [subcommand] [args...]')
283
+ .description('Project spec management (show, init, edit)')
284
+ .allowUnknownOption()
285
+ .action((subcommand, args) => {
286
+ handleSpec(subcommand ? [subcommand, ...(args || [])] : []);
287
+ });
288
+
278
289
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luxlabs",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "CLI tool for Lux - Upload and deploy interfaces from your terminal",
5
5
  "author": "Jason Henkel <jason@uselux.ai>",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -24,7 +24,7 @@
24
24
  "deploy",
25
25
  "interfaces",
26
26
  "ai",
27
- "workflows",
27
+ "flows",
28
28
  "agents"
29
29
  ],
30
30
  "repository": {