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.
- package/commands/{workflows.js → flows.js} +426 -150
- package/commands/servers.js +57 -0
- package/commands/spec.js +137 -0
- package/lib/config.js +16 -1
- package/lux.js +22 -11
- package/package.json +2 -2
|
@@ -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
|
|
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
|
|
92
|
+
${chalk.bold('Usage:')} lux flows <command> [args]
|
|
92
93
|
|
|
93
94
|
${chalk.bold('Commands:')}
|
|
94
|
-
list List all
|
|
95
|
-
get <id> Get
|
|
96
|
-
status <id> Show sync status for a
|
|
97
|
-
create <name> [desc] [--publish] Create
|
|
98
|
-
init <name> [desc] [--publish] Initialize
|
|
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
|
|
101
|
-
publish <id> Publish
|
|
102
|
-
delete <id> Delete a local
|
|
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
|
|
125
|
-
lux
|
|
126
|
-
lux
|
|
127
|
-
lux
|
|
128
|
-
lux
|
|
129
|
-
lux
|
|
130
|
-
lux
|
|
131
|
-
lux
|
|
132
|
-
lux
|
|
133
|
-
lux
|
|
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
|
|
137
|
-
lux
|
|
138
|
-
lux
|
|
139
|
-
lux
|
|
140
|
-
|
|
141
|
-
${chalk.bold('Webhook
|
|
142
|
-
lux
|
|
143
|
-
lux
|
|
144
|
-
lux
|
|
145
|
-
lux
|
|
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
|
|
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
|
|
161
|
-
console.log(chalk.gray('Run "lux
|
|
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}
|
|
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
|
|
195
|
-
const
|
|
206
|
+
requireArgs(args.slice(1), 1, 'lux flows status <id>');
|
|
207
|
+
const flowId = args[1];
|
|
196
208
|
|
|
197
|
-
const flow = loadLocalFlow(
|
|
209
|
+
const flow = loadLocalFlow(flowId);
|
|
198
210
|
if (!flow) {
|
|
199
|
-
error(`
|
|
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📝
|
|
223
|
-
console.log(` ID: ${
|
|
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
|
|
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
|
|
238
|
-
console.log(chalk.gray(' Run "lux
|
|
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
|
|
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
|
|
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
|
|
355
|
-
const
|
|
366
|
+
requireArgs(args.slice(1), 1, 'lux flows delete <id>');
|
|
367
|
+
const flowId = args[1];
|
|
356
368
|
|
|
357
|
-
const flow = loadLocalFlow(
|
|
369
|
+
const flow = loadLocalFlow(flowId);
|
|
358
370
|
if (!flow) {
|
|
359
|
-
error(`
|
|
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(
|
|
369
|
-
success(`Deleted local
|
|
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
|
|
375
|
-
const
|
|
386
|
+
requireArgs(args.slice(1), 1, 'lux flows get <id>');
|
|
387
|
+
const flowId = args[1];
|
|
376
388
|
|
|
377
|
-
info(`Loading
|
|
389
|
+
info(`Loading flow: ${flowId}`);
|
|
378
390
|
const { data } = await axios.get(
|
|
379
|
-
`${apiUrl}/api/workflows/${
|
|
391
|
+
`${apiUrl}/api/workflows/${flowId}`,
|
|
380
392
|
{ headers: getAuthHeaders() }
|
|
381
393
|
);
|
|
382
394
|
|
|
383
395
|
if (!data.workflow) {
|
|
384
|
-
error(`
|
|
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
|
|
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
|
|
457
|
+
info(`Creating local flow: ${name}`);
|
|
446
458
|
|
|
447
459
|
if (saveLocalFlow(flowId, newFlow)) {
|
|
448
|
-
success(`
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
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
|
|
530
|
-
const
|
|
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(
|
|
546
|
+
const existingFlow = loadLocalFlow(flowId);
|
|
535
547
|
if (!existingFlow) {
|
|
536
|
-
error(`
|
|
537
|
-
console.log(chalk.gray('Run "lux
|
|
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, '
|
|
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: ${
|
|
555
|
-
if (saveLocalFlow(
|
|
556
|
-
success('
|
|
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
|
|
579
|
+
console.log(chalk.gray(`\nRun "lux flows publish ${flowId}" to publish to cloud.`));
|
|
568
580
|
} else {
|
|
569
|
-
error('Failed to save
|
|
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
|
|
576
|
-
const
|
|
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(
|
|
591
|
+
const localFlow = loadLocalFlow(flowId);
|
|
580
592
|
if (!localFlow) {
|
|
581
|
-
error(`
|
|
582
|
-
console.log(chalk.gray('Run "lux
|
|
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:
|
|
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
|
|
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/${
|
|
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(
|
|
646
|
+
saveLocalFlow(flowId, updatedFlow);
|
|
635
647
|
|
|
636
|
-
success('
|
|
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
|
|
645
|
-
const
|
|
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(
|
|
660
|
+
const localFlow = loadLocalFlow(flowId);
|
|
649
661
|
if (!localFlow) {
|
|
650
|
-
error(`
|
|
662
|
+
error(`Flow not found locally: ${flowId}`);
|
|
651
663
|
break;
|
|
652
664
|
}
|
|
653
665
|
|
|
654
|
-
info(`Comparing local vs published for: ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
707
|
-
const
|
|
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: ${
|
|
710
|
-
const tokenData = await getWebhookToken(
|
|
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
|
|
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
|
|
725
|
-
const
|
|
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: ${
|
|
728
|
-
const tokenData = await getWebhookToken(
|
|
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
|
|
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
|
|
743
|
-
const
|
|
754
|
+
requireArgs(args.slice(1), 1, 'lux flows webhook-poll <flow-id>');
|
|
755
|
+
const flowId = args[1];
|
|
744
756
|
|
|
745
|
-
const tokenData = await getWebhookToken(
|
|
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
|
|
755
|
-
const
|
|
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: ${
|
|
769
|
+
info(`Accepting webhook format for: ${flowId}`);
|
|
758
770
|
|
|
759
771
|
// Step 1: Get webhook token
|
|
760
|
-
const tokenData = await getWebhookToken(
|
|
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
|
|
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
|
|
843
|
-
const
|
|
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: ${
|
|
846
|
-
const tokenData = await getWebhookToken(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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 (
|
|
883
|
-
console.log(
|
|
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
|
-
|
|
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 =
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
933
|
-
`${apiUrl}/api/flows/${flowId}/executions/${executionId}`,
|
|
934
|
-
{ headers: getAuthHeaders() }
|
|
935
|
-
);
|
|
973
|
+
info(`Loading execution: ${executionId}`);
|
|
936
974
|
|
|
937
|
-
|
|
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
|
-
|
|
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: ${
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
1014
|
-
|
|
1015
|
-
|
|
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.
|
|
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
|
|
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 = {
|
|
1376
|
+
module.exports = { handleFlows };
|
package/commands/servers.js
CHANGED
|
@@ -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
|
};
|
package/commands/spec.js
ADDED
|
@@ -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(
|
|
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 {
|
|
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
|
-
//
|
|
99
|
+
// Flows commands (with short alias 'f')
|
|
99
100
|
program
|
|
100
|
-
.command('
|
|
101
|
-
.
|
|
102
|
-
.description('
|
|
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
|
-
|
|
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('
|
|
199
|
-
.
|
|
200
|
-
|
|
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.
|
|
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
|
-
"
|
|
27
|
+
"flows",
|
|
28
28
|
"agents"
|
|
29
29
|
],
|
|
30
30
|
"repository": {
|