luxlabs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +37 -0
- package/README.md +161 -0
- package/commands/ab-tests.js +437 -0
- package/commands/agents.js +226 -0
- package/commands/data.js +966 -0
- package/commands/deploy.js +166 -0
- package/commands/dev.js +569 -0
- package/commands/init.js +126 -0
- package/commands/interface/boilerplate.js +52 -0
- package/commands/interface/git-utils.js +85 -0
- package/commands/interface/index.js +7 -0
- package/commands/interface/init.js +375 -0
- package/commands/interface/path.js +74 -0
- package/commands/interface.js +125 -0
- package/commands/knowledge.js +339 -0
- package/commands/link.js +127 -0
- package/commands/list.js +97 -0
- package/commands/login.js +247 -0
- package/commands/logout.js +19 -0
- package/commands/logs.js +182 -0
- package/commands/pricing.js +328 -0
- package/commands/project.js +704 -0
- package/commands/secrets.js +129 -0
- package/commands/servers.js +411 -0
- package/commands/storage.js +177 -0
- package/commands/up.js +211 -0
- package/commands/validate-data-lux.js +502 -0
- package/commands/voice-agents.js +1055 -0
- package/commands/webview.js +393 -0
- package/commands/workflows.js +836 -0
- package/lib/config.js +403 -0
- package/lib/helpers.js +189 -0
- package/lib/node-helper.js +120 -0
- package/lux.js +268 -0
- package/package.json +56 -0
- package/templates/next-env.d.ts +6 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const {
|
|
4
|
+
getApiUrl,
|
|
5
|
+
getStudioApiUrl,
|
|
6
|
+
getAuthHeaders,
|
|
7
|
+
isAuthenticated,
|
|
8
|
+
getOrgId,
|
|
9
|
+
getFlowsDir,
|
|
10
|
+
loadLocalFlow,
|
|
11
|
+
saveLocalFlow,
|
|
12
|
+
listLocalFlows,
|
|
13
|
+
deleteLocalFlow,
|
|
14
|
+
} = require('../lib/config');
|
|
15
|
+
const {
|
|
16
|
+
error,
|
|
17
|
+
success,
|
|
18
|
+
info,
|
|
19
|
+
formatJson,
|
|
20
|
+
formatTable,
|
|
21
|
+
requireArgs,
|
|
22
|
+
readFile,
|
|
23
|
+
parseJson,
|
|
24
|
+
} = require('../lib/helpers');
|
|
25
|
+
|
|
26
|
+
// Webhook helper functions
|
|
27
|
+
const WEBHOOK_WORKER_URL = 'https://webhook-trigger-worker.jason-a5d.workers.dev';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get webhook token for a workflow
|
|
31
|
+
*/
|
|
32
|
+
async function getWebhookToken(workflowId) {
|
|
33
|
+
// Use container API URL for lux-api-2 routes
|
|
34
|
+
const containerUrl = process.env.CONTAINER_API_URL || 'http://localhost:8000';
|
|
35
|
+
const { data } = await axios.get(
|
|
36
|
+
`${containerUrl}/lux-api-2/workflows/${workflowId}/webhook-token`,
|
|
37
|
+
{ headers: getAuthHeaders() }
|
|
38
|
+
);
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get captured webhook payload from worker
|
|
44
|
+
*/
|
|
45
|
+
async function getCapturedPayload(token) {
|
|
46
|
+
const { data } = await axios.get(
|
|
47
|
+
`${WEBHOOK_WORKER_URL}/listening/poll/${token}`
|
|
48
|
+
);
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get workflow draft config
|
|
54
|
+
*/
|
|
55
|
+
async function getWorkflowDraft(workflowId) {
|
|
56
|
+
const apiUrl = getApiUrl();
|
|
57
|
+
const { data } = await axios.get(
|
|
58
|
+
`${apiUrl}/api/workflows/${workflowId}/draft`,
|
|
59
|
+
{ headers: getAuthHeaders() }
|
|
60
|
+
);
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Save workflow draft config
|
|
66
|
+
*/
|
|
67
|
+
async function saveWorkflowDraft(workflowId, config) {
|
|
68
|
+
const apiUrl = getApiUrl();
|
|
69
|
+
await axios.put(
|
|
70
|
+
`${apiUrl}/api/workflows/${workflowId}/draft`,
|
|
71
|
+
{ config },
|
|
72
|
+
{ headers: getAuthHeaders() }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handleWorkflows(args) {
|
|
77
|
+
// Check authentication
|
|
78
|
+
if (!isAuthenticated()) {
|
|
79
|
+
console.log(
|
|
80
|
+
chalk.red('ā Not authenticated. Run'),
|
|
81
|
+
chalk.white('lux login'),
|
|
82
|
+
chalk.red('first.')
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const command = args[0];
|
|
88
|
+
|
|
89
|
+
if (!command) {
|
|
90
|
+
console.log(`
|
|
91
|
+
${chalk.bold('Usage:')} lux workflows <command> [args]
|
|
92
|
+
|
|
93
|
+
${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)
|
|
99
|
+
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
|
|
103
|
+
diff <id> Show local vs published differences
|
|
104
|
+
|
|
105
|
+
${chalk.bold('Sync Status:')}
|
|
106
|
+
draft - Never published, local only
|
|
107
|
+
synced - Matches published version
|
|
108
|
+
dirty - Has local changes since publish
|
|
109
|
+
conflict - Local changes + cloud has newer version
|
|
110
|
+
|
|
111
|
+
${chalk.bold('Webhook Commands:')}
|
|
112
|
+
webhook-url <id> Get webhook URL and status
|
|
113
|
+
webhook-listen <id> Start listening session for webhook capture
|
|
114
|
+
webhook-poll <id> Check for captured webhook payload
|
|
115
|
+
webhook-accept <id> Accept webhook format and save variables
|
|
116
|
+
webhook-decline <id> Decline webhook and reset session
|
|
117
|
+
|
|
118
|
+
${chalk.bold('Examples:')}
|
|
119
|
+
lux workflows list
|
|
120
|
+
lux workflows get flow_123
|
|
121
|
+
lux workflows status flow_123
|
|
122
|
+
lux workflows create "My Workflow" "Description"
|
|
123
|
+
lux workflows create "My Workflow" --publish # Create and publish immediately
|
|
124
|
+
lux flow init "My Workflow" --publish
|
|
125
|
+
lux workflows save flow_123 ./config.json
|
|
126
|
+
lux workflows sync
|
|
127
|
+
lux workflows publish flow_123
|
|
128
|
+
lux workflows diff flow_123
|
|
129
|
+
|
|
130
|
+
${chalk.bold('Webhook workflow:')}
|
|
131
|
+
lux flow webhook-url flow_123
|
|
132
|
+
lux flow webhook-listen flow_123
|
|
133
|
+
lux flow webhook-poll flow_123
|
|
134
|
+
lux flow webhook-accept flow_123
|
|
135
|
+
`);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Use studio API (v2.uselux.ai) for workflow operations
|
|
140
|
+
const apiUrl = getStudioApiUrl();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
switch (command) {
|
|
144
|
+
case 'list': {
|
|
145
|
+
info('Loading workflows from local storage...');
|
|
146
|
+
const localFlows = listLocalFlows();
|
|
147
|
+
|
|
148
|
+
if (localFlows.length === 0) {
|
|
149
|
+
console.log('\n(No local workflows found)');
|
|
150
|
+
console.log(chalk.gray('Run "lux workflows sync" to sync from cloud, or "lux workflows create" to create one.\n'));
|
|
151
|
+
} else {
|
|
152
|
+
console.log(`\nFound ${localFlows.length} workflow(s):\n`);
|
|
153
|
+
|
|
154
|
+
// Color-code sync status
|
|
155
|
+
const statusColors = {
|
|
156
|
+
draft: chalk.gray,
|
|
157
|
+
synced: chalk.green,
|
|
158
|
+
dirty: chalk.yellow,
|
|
159
|
+
conflict: chalk.red,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const formatted = localFlows.map((w) => ({
|
|
163
|
+
id: w.id,
|
|
164
|
+
name: w.name,
|
|
165
|
+
sync: statusColors[w.syncStatus]?.(w.syncStatus) || w.syncStatus,
|
|
166
|
+
local_v: w.localVersion || 1,
|
|
167
|
+
published_v: w.publishedVersion || '-',
|
|
168
|
+
updated: w.updatedAt ? new Date(w.updatedAt).toLocaleDateString() : '-',
|
|
169
|
+
}));
|
|
170
|
+
formatTable(formatted);
|
|
171
|
+
|
|
172
|
+
// Show legend
|
|
173
|
+
console.log(chalk.gray('\nSync status: ') +
|
|
174
|
+
chalk.gray('draft') + ' | ' +
|
|
175
|
+
chalk.green('synced') + ' | ' +
|
|
176
|
+
chalk.yellow('dirty') + ' | ' +
|
|
177
|
+
chalk.red('conflict'));
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'status': {
|
|
183
|
+
requireArgs(args.slice(1), 1, 'lux workflows status <id>');
|
|
184
|
+
const workflowId = args[1];
|
|
185
|
+
|
|
186
|
+
const flow = loadLocalFlow(workflowId);
|
|
187
|
+
if (!flow) {
|
|
188
|
+
error(`Workflow not found locally: ${workflowId}`);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Determine sync status
|
|
193
|
+
let syncStatus = 'draft';
|
|
194
|
+
if (flow.publishedVersion) {
|
|
195
|
+
if (flow.cloudVersion && flow.cloudVersion > flow.publishedVersion) {
|
|
196
|
+
syncStatus = flow.localVersion > flow.publishedVersion ? 'conflict' : 'synced';
|
|
197
|
+
} else if (flow.localVersion > flow.publishedVersion) {
|
|
198
|
+
syncStatus = 'dirty';
|
|
199
|
+
} else {
|
|
200
|
+
syncStatus = 'synced';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const statusColors = {
|
|
205
|
+
draft: chalk.gray,
|
|
206
|
+
synced: chalk.green,
|
|
207
|
+
dirty: chalk.yellow,
|
|
208
|
+
conflict: chalk.red,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
console.log(`\nš Workflow: ${flow.name}`);
|
|
212
|
+
console.log(` ID: ${workflowId}`);
|
|
213
|
+
console.log(` Sync Status: ${statusColors[syncStatus](syncStatus)}`);
|
|
214
|
+
console.log(` Local Version: ${flow.localVersion || 1}`);
|
|
215
|
+
console.log(` Published Version: ${flow.publishedVersion || '(never published)'}`);
|
|
216
|
+
console.log(` Cloud Version: ${flow.cloudVersion || '(unknown)'}`);
|
|
217
|
+
console.log(` Last Updated: ${flow.updatedAt || '(unknown)'}`);
|
|
218
|
+
console.log(` Last Published: ${flow.lastPublishedAt || '(never)'}`);
|
|
219
|
+
console.log(` Nodes: ${flow.nodes?.length || 0}`);
|
|
220
|
+
console.log(` Edges: ${flow.edges?.length || 0}`);
|
|
221
|
+
|
|
222
|
+
if (syncStatus === 'conflict') {
|
|
223
|
+
console.log(chalk.red('\nā ļø This workflow has conflicts with the cloud version.'));
|
|
224
|
+
console.log(chalk.gray(' Use the Lux Studio app to resolve conflicts.\n'));
|
|
225
|
+
} else if (syncStatus === 'dirty') {
|
|
226
|
+
console.log(chalk.yellow('\nš¤ This workflow has unpublished local changes.'));
|
|
227
|
+
console.log(chalk.gray(' Run "lux workflows publish ' + workflowId + '" to publish.\n'));
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case 'sync': {
|
|
233
|
+
info('Syncing workflows from cloud...');
|
|
234
|
+
|
|
235
|
+
const { data } = await axios.get(`${apiUrl}/api/workflows?include_config=true`, {
|
|
236
|
+
headers: getAuthHeaders(),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const cloudFlows = data.workflows || [];
|
|
240
|
+
let synced = 0;
|
|
241
|
+
let newFromCloud = 0;
|
|
242
|
+
let conflicts = 0;
|
|
243
|
+
|
|
244
|
+
for (const cloudFlow of cloudFlows) {
|
|
245
|
+
// Only sync published flows
|
|
246
|
+
if (cloudFlow.status !== 'published') continue;
|
|
247
|
+
|
|
248
|
+
const localFlow = loadLocalFlow(cloudFlow.id);
|
|
249
|
+
const now = new Date().toISOString();
|
|
250
|
+
|
|
251
|
+
if (!localFlow) {
|
|
252
|
+
// New from cloud
|
|
253
|
+
const newFlow = {
|
|
254
|
+
id: cloudFlow.id,
|
|
255
|
+
name: cloudFlow.name,
|
|
256
|
+
description: cloudFlow.description,
|
|
257
|
+
nodes: cloudFlow.config?.nodes || [],
|
|
258
|
+
edges: cloudFlow.config?.edges || [],
|
|
259
|
+
localVersion: cloudFlow.version || 1,
|
|
260
|
+
publishedVersion: cloudFlow.version || 1,
|
|
261
|
+
cloudVersion: cloudFlow.version || 1,
|
|
262
|
+
lastSyncedAt: now,
|
|
263
|
+
createdAt: cloudFlow.updated_at || cloudFlow.created_at,
|
|
264
|
+
updatedAt: cloudFlow.updated_at,
|
|
265
|
+
};
|
|
266
|
+
saveLocalFlow(cloudFlow.id, newFlow);
|
|
267
|
+
newFromCloud++;
|
|
268
|
+
} else {
|
|
269
|
+
const hasLocalChanges = localFlow.publishedVersion
|
|
270
|
+
? localFlow.localVersion > localFlow.publishedVersion
|
|
271
|
+
: true;
|
|
272
|
+
const cloudVersion = cloudFlow.version || 1;
|
|
273
|
+
const cloudHasNewerVersion = cloudVersion > (localFlow.cloudVersion || 0);
|
|
274
|
+
|
|
275
|
+
if (hasLocalChanges && cloudHasNewerVersion) {
|
|
276
|
+
// Conflict
|
|
277
|
+
const updatedFlow = {
|
|
278
|
+
...localFlow,
|
|
279
|
+
cloudVersion,
|
|
280
|
+
lastSyncedAt: now,
|
|
281
|
+
cloudData: {
|
|
282
|
+
nodes: cloudFlow.config?.nodes || [],
|
|
283
|
+
edges: cloudFlow.config?.edges || [],
|
|
284
|
+
version: cloudVersion,
|
|
285
|
+
updatedAt: cloudFlow.updated_at,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
saveLocalFlow(cloudFlow.id, updatedFlow);
|
|
289
|
+
conflicts++;
|
|
290
|
+
} else if (cloudHasNewerVersion && !hasLocalChanges) {
|
|
291
|
+
// Update from cloud
|
|
292
|
+
const updatedFlow = {
|
|
293
|
+
...localFlow,
|
|
294
|
+
name: cloudFlow.name,
|
|
295
|
+
description: cloudFlow.description,
|
|
296
|
+
nodes: cloudFlow.config?.nodes || [],
|
|
297
|
+
edges: cloudFlow.config?.edges || [],
|
|
298
|
+
localVersion: cloudVersion,
|
|
299
|
+
publishedVersion: cloudVersion,
|
|
300
|
+
cloudVersion,
|
|
301
|
+
lastSyncedAt: now,
|
|
302
|
+
updatedAt: cloudFlow.updated_at,
|
|
303
|
+
};
|
|
304
|
+
saveLocalFlow(cloudFlow.id, updatedFlow);
|
|
305
|
+
synced++;
|
|
306
|
+
} else {
|
|
307
|
+
// Up to date
|
|
308
|
+
const updatedFlow = {
|
|
309
|
+
...localFlow,
|
|
310
|
+
cloudVersion,
|
|
311
|
+
lastSyncedAt: now,
|
|
312
|
+
};
|
|
313
|
+
saveLocalFlow(cloudFlow.id, updatedFlow);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
success('Sync complete!');
|
|
319
|
+
console.log(` Synced: ${synced}`);
|
|
320
|
+
console.log(` New from cloud: ${newFromCloud}`);
|
|
321
|
+
if (conflicts > 0) {
|
|
322
|
+
console.log(chalk.red(` Conflicts: ${conflicts}`));
|
|
323
|
+
console.log(chalk.gray('\n Use "lux workflows list" to see workflows with conflicts.'));
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case 'delete': {
|
|
329
|
+
requireArgs(args.slice(1), 1, 'lux workflows delete <id>');
|
|
330
|
+
const workflowId = args[1];
|
|
331
|
+
|
|
332
|
+
const flow = loadLocalFlow(workflowId);
|
|
333
|
+
if (!flow) {
|
|
334
|
+
error(`Workflow not found locally: ${workflowId}`);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (flow.publishedVersion) {
|
|
339
|
+
console.log(chalk.yellow(`\nā ļø Warning: "${flow.name}" has been published.`));
|
|
340
|
+
console.log(chalk.gray(' This only deletes the local copy. The published version remains in the cloud.\n'));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
deleteLocalFlow(workflowId);
|
|
344
|
+
success(`Deleted local workflow: ${flow.name}`);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
case 'get': {
|
|
349
|
+
requireArgs(args.slice(1), 1, 'lux workflows get <id>');
|
|
350
|
+
const workflowId = args[1];
|
|
351
|
+
|
|
352
|
+
info(`Loading workflow: ${workflowId}`);
|
|
353
|
+
const { data } = await axios.get(
|
|
354
|
+
`${apiUrl}/api/workflows/${workflowId}`,
|
|
355
|
+
{ headers: getAuthHeaders() }
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (!data.workflow) {
|
|
359
|
+
error(`Workflow not found: ${workflowId}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const workflow = data.workflow;
|
|
363
|
+
|
|
364
|
+
console.log(`\nš Name: ${workflow.name}`);
|
|
365
|
+
console.log(`š Status: ${workflow.status}`);
|
|
366
|
+
console.log(`š¢ Version: ${workflow.version}`);
|
|
367
|
+
console.log(`š
Created: ${workflow.created_at}`);
|
|
368
|
+
console.log(`š
Updated: ${workflow.updated_at}`);
|
|
369
|
+
|
|
370
|
+
if (workflow.config) {
|
|
371
|
+
console.log(`\nš Current Config:\n`);
|
|
372
|
+
console.log(formatJson(workflow.config));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
case 'create':
|
|
379
|
+
case 'init': {
|
|
380
|
+
// Check for --publish flag
|
|
381
|
+
const publishFlagIndex = args.indexOf('--publish');
|
|
382
|
+
const shouldPublish = publishFlagIndex !== -1;
|
|
383
|
+
if (shouldPublish) {
|
|
384
|
+
args.splice(publishFlagIndex, 1); // Remove flag from args
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
requireArgs(args.slice(1), 1, 'lux workflows create <name> [description] [--publish]');
|
|
388
|
+
const name = args[1];
|
|
389
|
+
const description = args[2] || '';
|
|
390
|
+
|
|
391
|
+
// Slugify the name for the flow ID
|
|
392
|
+
const slug = name
|
|
393
|
+
.toLowerCase()
|
|
394
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
395
|
+
.replace(/^-|-$/g, '');
|
|
396
|
+
const flowId = `${slug}-flow`;
|
|
397
|
+
|
|
398
|
+
// Check if flow already exists
|
|
399
|
+
const existingFlow = loadLocalFlow(flowId);
|
|
400
|
+
if (existingFlow) {
|
|
401
|
+
error(`Flow already exists: ${flowId}`);
|
|
402
|
+
console.log(chalk.gray(`Use a different name or delete the existing flow first.`));
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const now = new Date().toISOString();
|
|
407
|
+
|
|
408
|
+
// Create locally only - no API call
|
|
409
|
+
const newFlow = {
|
|
410
|
+
id: flowId,
|
|
411
|
+
name,
|
|
412
|
+
description,
|
|
413
|
+
nodes: [],
|
|
414
|
+
edges: [],
|
|
415
|
+
localVersion: 1,
|
|
416
|
+
createdAt: now,
|
|
417
|
+
updatedAt: now,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
info(`Creating local workflow: ${name}`);
|
|
421
|
+
|
|
422
|
+
if (saveLocalFlow(flowId, newFlow)) {
|
|
423
|
+
success(`Workflow created locally!`);
|
|
424
|
+
console.log(` ID: ${flowId}`);
|
|
425
|
+
|
|
426
|
+
// If --publish flag was provided, publish immediately
|
|
427
|
+
if (shouldPublish) {
|
|
428
|
+
info(`Publishing workflow to cloud...`);
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const publishConfig = {
|
|
432
|
+
nodes: newFlow.nodes || [],
|
|
433
|
+
edges: newFlow.edges || [],
|
|
434
|
+
variables: newFlow.variables || {},
|
|
435
|
+
metadata: newFlow.metadata || {},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Step 1: Create workflow in cloud database (this generates a cloud UUID)
|
|
439
|
+
const createResponse = await axios.post(
|
|
440
|
+
`${apiUrl}/api/workflows`,
|
|
441
|
+
{
|
|
442
|
+
name: newFlow.name,
|
|
443
|
+
description: newFlow.description,
|
|
444
|
+
config: publishConfig,
|
|
445
|
+
},
|
|
446
|
+
{ headers: getAuthHeaders() }
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const cloudId = createResponse.data.workflow?.id || createResponse.data.workflow?.workflow_id;
|
|
450
|
+
if (!cloudId) {
|
|
451
|
+
throw new Error('No workflow ID returned from cloud');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
info(`Created in cloud with ID: ${cloudId}`);
|
|
455
|
+
|
|
456
|
+
// Step 2: Publish the workflow (copies draft config to published)
|
|
457
|
+
const publishResponse = await axios.post(
|
|
458
|
+
`${apiUrl}/api/workflows/${cloudId}/publish`,
|
|
459
|
+
{},
|
|
460
|
+
{ headers: getAuthHeaders() }
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const newVersion = publishResponse.data.version || 1;
|
|
464
|
+
|
|
465
|
+
// Update local storage with published version and cloud ID
|
|
466
|
+
const updatedFlow = {
|
|
467
|
+
...newFlow,
|
|
468
|
+
cloudId: cloudId,
|
|
469
|
+
publishedVersion: newVersion,
|
|
470
|
+
cloudVersion: newVersion,
|
|
471
|
+
lastPublishedAt: new Date().toISOString(),
|
|
472
|
+
lastSyncedAt: new Date().toISOString(),
|
|
473
|
+
};
|
|
474
|
+
saveLocalFlow(flowId, updatedFlow);
|
|
475
|
+
|
|
476
|
+
success('Workflow published!');
|
|
477
|
+
console.log(` Cloud ID: ${cloudId}`);
|
|
478
|
+
console.log(` Status: ${chalk.green('published')}`);
|
|
479
|
+
console.log(` Version: ${newVersion}`);
|
|
480
|
+
} catch (publishError) {
|
|
481
|
+
error(`Failed to publish: ${publishError.response?.data?.error || publishError.message}`);
|
|
482
|
+
console.log(` Status: ${chalk.gray('draft')}`);
|
|
483
|
+
console.log(chalk.gray(`\nRun "lux workflows publish ${flowId}" to try again.`));
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
console.log(` Status: ${chalk.gray('draft')}`);
|
|
487
|
+
console.log(chalk.gray(`\n This workflow is local only. Run "lux workflows publish ${flowId}" to publish to cloud.`));
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
error('Failed to create workflow. Make sure you are authenticated.');
|
|
491
|
+
}
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
case 'save': {
|
|
496
|
+
requireArgs(args.slice(1), 2, 'lux workflows save <id> <config-file>');
|
|
497
|
+
const workflowId = args[1];
|
|
498
|
+
const configFile = args[2];
|
|
499
|
+
|
|
500
|
+
// Load existing local flow
|
|
501
|
+
const existingFlow = loadLocalFlow(workflowId);
|
|
502
|
+
if (!existingFlow) {
|
|
503
|
+
error(`Workflow not found locally: ${workflowId}`);
|
|
504
|
+
console.log(chalk.gray('Run "lux workflows sync" to sync from cloud first.'));
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
info(`Reading config from: ${configFile}`);
|
|
509
|
+
const configJson = readFile(configFile);
|
|
510
|
+
const config = parseJson(configJson, 'workflow config');
|
|
511
|
+
|
|
512
|
+
// Update local flow with new config
|
|
513
|
+
const updatedFlow = {
|
|
514
|
+
...existingFlow,
|
|
515
|
+
nodes: config.nodes || existingFlow.nodes,
|
|
516
|
+
edges: config.edges || existingFlow.edges,
|
|
517
|
+
localVersion: (existingFlow.localVersion || 1) + 1,
|
|
518
|
+
updatedAt: new Date().toISOString(),
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
info(`Saving to local storage: ${workflowId}`);
|
|
522
|
+
if (saveLocalFlow(workflowId, updatedFlow)) {
|
|
523
|
+
success('Workflow saved locally!');
|
|
524
|
+
console.log(` Local Version: ${updatedFlow.localVersion}`);
|
|
525
|
+
|
|
526
|
+
// Show sync status
|
|
527
|
+
const hasPublished = existingFlow.publishedVersion;
|
|
528
|
+
if (!hasPublished) {
|
|
529
|
+
console.log(` Status: ${chalk.gray('draft')}`);
|
|
530
|
+
} else if (updatedFlow.localVersion > existingFlow.publishedVersion) {
|
|
531
|
+
console.log(` Status: ${chalk.yellow('dirty')} (has unpublished changes)`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
console.log(chalk.gray(`\nRun "lux workflows publish ${workflowId}" to publish to cloud.`));
|
|
535
|
+
} else {
|
|
536
|
+
error('Failed to save workflow locally.');
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
case 'publish': {
|
|
542
|
+
requireArgs(args.slice(1), 1, 'lux workflows publish <id>');
|
|
543
|
+
const workflowId = args[1];
|
|
544
|
+
|
|
545
|
+
// Load local flow
|
|
546
|
+
const localFlow = loadLocalFlow(workflowId);
|
|
547
|
+
if (!localFlow) {
|
|
548
|
+
error(`Workflow not found locally: ${workflowId}`);
|
|
549
|
+
console.log(chalk.gray('Run "lux workflows sync" to sync from cloud first.'));
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Check for conflicts
|
|
554
|
+
if (localFlow.cloudVersion && localFlow.publishedVersion &&
|
|
555
|
+
localFlow.cloudVersion > localFlow.publishedVersion &&
|
|
556
|
+
localFlow.localVersion > localFlow.publishedVersion) {
|
|
557
|
+
error('Cannot publish: workflow has conflicts with cloud version.');
|
|
558
|
+
console.log(chalk.gray('Resolve conflicts in Lux Studio first.'));
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
info(`Publishing workflow: ${workflowId}`);
|
|
563
|
+
|
|
564
|
+
// Publish directly to cloud with config in request body
|
|
565
|
+
const publishConfig = {
|
|
566
|
+
nodes: localFlow.nodes || [],
|
|
567
|
+
edges: localFlow.edges || [],
|
|
568
|
+
variables: localFlow.variables || {},
|
|
569
|
+
metadata: localFlow.metadata || {},
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const { data } = await axios.post(
|
|
573
|
+
`${apiUrl}/api/workflows/${workflowId}/publish`,
|
|
574
|
+
{
|
|
575
|
+
name: localFlow.name,
|
|
576
|
+
description: localFlow.description,
|
|
577
|
+
config: publishConfig,
|
|
578
|
+
},
|
|
579
|
+
{ headers: getAuthHeaders() }
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const newVersion = data.version || (localFlow.publishedVersion || 0) + 1;
|
|
583
|
+
|
|
584
|
+
// Update local storage with new published version
|
|
585
|
+
const updatedFlow = {
|
|
586
|
+
...localFlow,
|
|
587
|
+
publishedVersion: newVersion,
|
|
588
|
+
cloudVersion: newVersion,
|
|
589
|
+
lastPublishedAt: new Date().toISOString(),
|
|
590
|
+
lastSyncedAt: new Date().toISOString(),
|
|
591
|
+
cloudData: undefined, // Clear any stored conflict data
|
|
592
|
+
};
|
|
593
|
+
saveLocalFlow(workflowId, updatedFlow);
|
|
594
|
+
|
|
595
|
+
success('Workflow published!');
|
|
596
|
+
console.log(` Name: ${localFlow.name}`);
|
|
597
|
+
console.log(` Version: ${newVersion}`);
|
|
598
|
+
console.log(` Status: ${chalk.green('synced')}`);
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
case 'diff': {
|
|
603
|
+
requireArgs(args.slice(1), 1, 'lux workflows diff <id>');
|
|
604
|
+
const workflowId = args[1];
|
|
605
|
+
|
|
606
|
+
// Load local flow
|
|
607
|
+
const localFlow = loadLocalFlow(workflowId);
|
|
608
|
+
if (!localFlow) {
|
|
609
|
+
error(`Workflow not found locally: ${workflowId}`);
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
info(`Comparing local vs published for: ${workflowId}`);
|
|
614
|
+
|
|
615
|
+
const localConfig = {
|
|
616
|
+
nodes: localFlow.nodes || [],
|
|
617
|
+
edges: localFlow.edges || [],
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// Check if never published
|
|
621
|
+
if (!localFlow.publishedVersion) {
|
|
622
|
+
console.log('\nš NEW WORKFLOW (not yet published)\n');
|
|
623
|
+
console.log(` Name: ${localFlow.name}`);
|
|
624
|
+
console.log(` Local Version: ${localFlow.localVersion || 1}`);
|
|
625
|
+
console.log(` Nodes: ${localConfig.nodes.length}`);
|
|
626
|
+
console.log(` Edges: ${localConfig.edges.length}`);
|
|
627
|
+
console.log(chalk.gray(`\nRun "lux workflows publish ${workflowId}" to publish.\n`));
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Check for conflicts
|
|
632
|
+
if (localFlow.cloudData) {
|
|
633
|
+
console.log('\nā ļø CONFLICT DETECTED\n');
|
|
634
|
+
console.log('Local version:');
|
|
635
|
+
console.log(` Version: ${localFlow.localVersion}`);
|
|
636
|
+
console.log(` Nodes: ${localConfig.nodes.length}`);
|
|
637
|
+
console.log(` Edges: ${localConfig.edges.length}`);
|
|
638
|
+
console.log('\nCloud version:');
|
|
639
|
+
console.log(` Version: ${localFlow.cloudData.version}`);
|
|
640
|
+
console.log(` Nodes: ${localFlow.cloudData.nodes?.length || 0}`);
|
|
641
|
+
console.log(` Edges: ${localFlow.cloudData.edges?.length || 0}`);
|
|
642
|
+
console.log(` Updated: ${localFlow.cloudData.updatedAt}`);
|
|
643
|
+
console.log(chalk.gray('\nResolve this conflict in Lux Studio.\n'));
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Check if has local changes
|
|
648
|
+
const hasLocalChanges = localFlow.localVersion > localFlow.publishedVersion;
|
|
649
|
+
|
|
650
|
+
if (!hasLocalChanges) {
|
|
651
|
+
success('No changes - local matches published version');
|
|
652
|
+
console.log(` Published Version: ${localFlow.publishedVersion}`);
|
|
653
|
+
} else {
|
|
654
|
+
console.log('\nš LOCAL CHANGES (unpublished)\n');
|
|
655
|
+
console.log(` Local Version: ${localFlow.localVersion}`);
|
|
656
|
+
console.log(` Published Version: ${localFlow.publishedVersion}`);
|
|
657
|
+
console.log(` Nodes: ${localConfig.nodes.length}`);
|
|
658
|
+
console.log(` Edges: ${localConfig.edges.length}`);
|
|
659
|
+
console.log(chalk.gray(`\nRun "lux workflows publish ${workflowId}" to publish changes.\n`));
|
|
660
|
+
}
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
case 'webhook-url': {
|
|
665
|
+
requireArgs(args.slice(1), 1, 'lux flow webhook-url <flow-id>');
|
|
666
|
+
const workflowId = args[1];
|
|
667
|
+
|
|
668
|
+
info(`Getting webhook URL for: ${workflowId}`);
|
|
669
|
+
const tokenData = await getWebhookToken(workflowId);
|
|
670
|
+
|
|
671
|
+
console.log(`\nš Webhook URL:\n${tokenData.webhookUrl}\n`);
|
|
672
|
+
console.log(`š Status:`);
|
|
673
|
+
console.log(` Format Confirmed: ${tokenData.formatConfirmed ? 'ā
Yes' : 'ā No'}`);
|
|
674
|
+
console.log(` Webhook Trigger Enabled: ${tokenData.webhookTrigger ? 'ā
Yes' : 'ā No'}\n`);
|
|
675
|
+
|
|
676
|
+
if (!tokenData.formatConfirmed) {
|
|
677
|
+
console.log(chalk.yellow('ā¹ļø Run "lux flow webhook-listen" to capture a sample webhook'));
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
case 'webhook-listen': {
|
|
683
|
+
requireArgs(args.slice(1), 1, 'lux flow webhook-listen <flow-id>');
|
|
684
|
+
const workflowId = args[1];
|
|
685
|
+
|
|
686
|
+
info(`Starting webhook listening session for: ${workflowId}`);
|
|
687
|
+
const tokenData = await getWebhookToken(workflowId);
|
|
688
|
+
|
|
689
|
+
// Start listening session
|
|
690
|
+
await axios.post(`${WEBHOOK_WORKER_URL}/listening/start/${tokenData.token}`);
|
|
691
|
+
|
|
692
|
+
success('Listening session started!');
|
|
693
|
+
console.log(`\nš” Now listening for webhooks at:\n${tokenData.webhookUrl}\n`);
|
|
694
|
+
console.log('Session expires in 5 minutes.');
|
|
695
|
+
console.log(`\nSend a webhook request to the URL above, then run:`);
|
|
696
|
+
console.log(chalk.white(` lux flow webhook-poll ${workflowId}\n`));
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
case 'webhook-poll': {
|
|
701
|
+
requireArgs(args.slice(1), 1, 'lux flow webhook-poll <flow-id>');
|
|
702
|
+
const workflowId = args[1];
|
|
703
|
+
|
|
704
|
+
const tokenData = await getWebhookToken(workflowId);
|
|
705
|
+
const pollData = await getCapturedPayload(tokenData.token);
|
|
706
|
+
|
|
707
|
+
// Return raw JSON response
|
|
708
|
+
console.log(formatJson(pollData));
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
case 'webhook-accept': {
|
|
713
|
+
requireArgs(args.slice(1), 1, 'lux flow webhook-accept <flow-id>');
|
|
714
|
+
const workflowId = args[1];
|
|
715
|
+
|
|
716
|
+
info(`Accepting webhook format for: ${workflowId}`);
|
|
717
|
+
|
|
718
|
+
// Step 1: Get webhook token
|
|
719
|
+
const tokenData = await getWebhookToken(workflowId);
|
|
720
|
+
|
|
721
|
+
// Step 2: Poll for captured payload
|
|
722
|
+
const pollData = await getCapturedPayload(tokenData.token);
|
|
723
|
+
|
|
724
|
+
// Check if payload was captured
|
|
725
|
+
if (pollData.waiting || !pollData.payload) {
|
|
726
|
+
error('No webhook captured. Run "lux flow webhook-listen" first and send a test webhook.');
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const capturedPayload = pollData.payload;
|
|
731
|
+
|
|
732
|
+
// Step 3: Transform payload into schema
|
|
733
|
+
info('Creating webhook schema from captured payload...');
|
|
734
|
+
const schema = {};
|
|
735
|
+
|
|
736
|
+
Object.entries(capturedPayload).forEach(([key, value]) => {
|
|
737
|
+
const varType =
|
|
738
|
+
typeof value === 'number' ? 'number' :
|
|
739
|
+
typeof value === 'boolean' ? 'boolean' : 'string';
|
|
740
|
+
|
|
741
|
+
schema[key] = {
|
|
742
|
+
type: varType,
|
|
743
|
+
description: `Webhook parameter: ${key}`,
|
|
744
|
+
example: value,
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// Step 4: Save schema to database (does NOT set format_confirmed yet)
|
|
749
|
+
info('Saving webhook schema to database...');
|
|
750
|
+
const containerUrl = process.env.CONTAINER_API_URL || 'http://localhost:8000';
|
|
751
|
+
await axios.put(`${containerUrl}/lux-api-2/webhooks/${tokenData.token}/schema`, {
|
|
752
|
+
schema,
|
|
753
|
+
}, {
|
|
754
|
+
headers: getAuthHeaders(),
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Step 5: Call worker accept endpoint (if not already confirmed)
|
|
758
|
+
info('Confirming webhook format with worker...');
|
|
759
|
+
let alreadyConfirmed = false;
|
|
760
|
+
try {
|
|
761
|
+
await axios.post(`${WEBHOOK_WORKER_URL}/listening/accept/${tokenData.token}`);
|
|
762
|
+
} catch (workerError) {
|
|
763
|
+
const errorMessage = workerError.response?.data?.error || '';
|
|
764
|
+
if (errorMessage.includes('already confirmed')) {
|
|
765
|
+
alreadyConfirmed = true;
|
|
766
|
+
info('Webhook already confirmed, updating schema only...');
|
|
767
|
+
} else {
|
|
768
|
+
throw workerError;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Step 6: ONLY if worker accepted (first time), set format_confirmed to 1
|
|
773
|
+
if (!alreadyConfirmed) {
|
|
774
|
+
info('Setting format_confirmed flag...');
|
|
775
|
+
await axios.post(`${containerUrl}/lux-api-2/webhooks/${tokenData.token}/confirm`, {}, {
|
|
776
|
+
headers: getAuthHeaders(),
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
success('Webhook format updated!');
|
|
781
|
+
console.log(`\nā
Captured schema with ${Object.keys(schema).length} parameters`);
|
|
782
|
+
console.log(`ā
Schema saved to database`);
|
|
783
|
+
if (!alreadyConfirmed) {
|
|
784
|
+
console.log(`ā
Worker accepted format`);
|
|
785
|
+
console.log(`ā
Format confirmed`);
|
|
786
|
+
} else {
|
|
787
|
+
console.log(`ā
Schema updated (already confirmed)`);
|
|
788
|
+
}
|
|
789
|
+
console.log('');
|
|
790
|
+
|
|
791
|
+
// Show the schema
|
|
792
|
+
console.log('Captured schema:');
|
|
793
|
+
Object.entries(schema).forEach(([key, value]) => {
|
|
794
|
+
console.log(` ${key}: ${value.type}`);
|
|
795
|
+
});
|
|
796
|
+
console.log('');
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
case 'webhook-decline': {
|
|
801
|
+
requireArgs(args.slice(1), 1, 'lux flow webhook-decline <flow-id>');
|
|
802
|
+
const workflowId = args[1];
|
|
803
|
+
|
|
804
|
+
info(`Declining webhook format for: ${workflowId}`);
|
|
805
|
+
const tokenData = await getWebhookToken(workflowId);
|
|
806
|
+
|
|
807
|
+
// Clear webhook schema from database
|
|
808
|
+
info('Clearing webhook schema...');
|
|
809
|
+
const containerUrl = process.env.CONTAINER_API_URL || 'http://localhost:8000';
|
|
810
|
+
await axios.delete(`${containerUrl}/lux-api-2/webhooks/${tokenData.token}/schema`, {
|
|
811
|
+
headers: getAuthHeaders(),
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// Call worker decline endpoint (clears listening session)
|
|
815
|
+
await axios.delete(`${WEBHOOK_WORKER_URL}/listening/decline/${tokenData.token}`);
|
|
816
|
+
|
|
817
|
+
success('Webhook format declined!');
|
|
818
|
+
console.log('\nā
Schema cleared from database');
|
|
819
|
+
console.log('ā
Listening session has been reset');
|
|
820
|
+
console.log(`\nRun "lux flow webhook-listen ${workflowId}" to try again.\n`);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
default:
|
|
825
|
+
error(
|
|
826
|
+
`Unknown command: ${command}\n\nRun 'lux workflows' to see available commands`
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
} catch (err) {
|
|
830
|
+
const errorMessage =
|
|
831
|
+
err.response?.data?.error || err.message || 'Unknown error';
|
|
832
|
+
error(`${errorMessage}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
module.exports = { handleWorkflows };
|