luxlabs 1.0.21 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -21
- package/commands/ab-tests.js +14 -11
- package/commands/agents.js +11 -11
- package/commands/data.js +19 -17
- package/commands/deploy.js +145 -82
- package/commands/flows.js +152 -133
- package/commands/interface/init.js +36 -35
- package/commands/interface.js +135 -10
- package/commands/knowledge.js +3 -3
- package/commands/list.js +6 -24
- package/commands/login.js +31 -26
- package/commands/logout.js +13 -4
- package/commands/logs.js +17 -66
- package/commands/project.js +74 -47
- package/commands/secrets.js +1 -1
- package/commands/servers.js +9 -113
- package/commands/storage.js +1 -1
- package/commands/tools.js +4 -4
- package/commands/validate-data-lux.js +5 -2
- package/commands/voice-agents.js +22 -18
- package/lib/config.js +235 -83
- package/lib/helpers.js +6 -4
- package/lux.js +4 -94
- package/package.json +6 -1
- package/templates/interface-boilerplate/components/auth/sign-in-form.tsx +41 -34
- package/templates/interface-boilerplate/components/auth/sign-up-form.tsx +41 -34
- package/templates/interface-boilerplate/components/providers/posthog-provider.tsx +41 -26
- package/templates/interface-boilerplate/gitignore.template +4 -0
- package/templates/interface-boilerplate/lib/auth.config.ts +3 -2
- package/templates/interface-boilerplate/lib/knowledge.ts +2 -2
- package/templates/interface-boilerplate/middleware.ts +14 -3
- package/templates/interface-boilerplate/next-env.d.ts +6 -0
- package/templates/interface-boilerplate/package-lock.json +432 -8
- package/commands/dev.js +0 -578
- package/commands/init.js +0 -126
- package/commands/link.js +0 -127
- package/commands/up.js +0 -211
package/lib/config.js
CHANGED
|
@@ -3,79 +3,141 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
|
|
5
5
|
// Shared storage directory (used by both Electron app and CLI)
|
|
6
|
+
// Must match Electron ConfigService path: ~/.lux-studio on all platforms
|
|
6
7
|
const LUX_STUDIO_DIR = path.join(os.homedir(), '.lux-studio');
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
// Unified config file path
|
|
10
|
+
const CONFIG_FILE = path.join(LUX_STUDIO_DIR, 'config.json');
|
|
8
11
|
const INTERFACE_FILE = '.lux/interface.json';
|
|
9
12
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// Short folder names (to reduce path length for Windows 260 char limit)
|
|
14
|
+
const FOLDER_NAMES = {
|
|
15
|
+
projects: 'p',
|
|
16
|
+
interfaces: 'i',
|
|
17
|
+
flows: 'f',
|
|
18
|
+
'voice-agents': 'v',
|
|
19
|
+
data: 'd',
|
|
20
|
+
knowledge: 'k',
|
|
21
|
+
};
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
|
-
* Load the unified config
|
|
16
|
-
*
|
|
24
|
+
* Load the unified config from ~/.lux-studio/config.json
|
|
25
|
+
* Returns the full config object or default structure
|
|
17
26
|
*/
|
|
18
27
|
function loadUnifiedConfig() {
|
|
19
|
-
if (!fs.existsSync(
|
|
20
|
-
return
|
|
28
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
29
|
+
return {
|
|
30
|
+
currentOrg: null,
|
|
31
|
+
currentProject: null,
|
|
32
|
+
orgs: {},
|
|
33
|
+
settings: {},
|
|
34
|
+
};
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (_unifiedConfigCache && _unifiedConfigMtime && stats.mtimeMs === _unifiedConfigMtime) {
|
|
27
|
-
return _unifiedConfigCache;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const content = fs.readFileSync(UNIFIED_CONFIG_FILE, 'utf8');
|
|
31
|
-
_unifiedConfigCache = JSON.parse(content);
|
|
32
|
-
_unifiedConfigMtime = stats.mtimeMs;
|
|
33
|
-
return _unifiedConfigCache;
|
|
38
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
39
|
+
return JSON.parse(content);
|
|
34
40
|
} catch (error) {
|
|
35
|
-
return
|
|
41
|
+
return {
|
|
42
|
+
currentOrg: null,
|
|
43
|
+
currentProject: null,
|
|
44
|
+
orgs: {},
|
|
45
|
+
settings: {},
|
|
46
|
+
};
|
|
36
47
|
}
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
40
|
-
*
|
|
41
|
-
* Returns { orgId, orgName } or null
|
|
51
|
+
* Save the unified config to ~/.lux-studio/config.json
|
|
42
52
|
*/
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
46
|
-
|
|
53
|
+
function saveUnifiedConfig(config) {
|
|
54
|
+
const dir = path.dirname(CONFIG_FILE);
|
|
55
|
+
if (!fs.existsSync(dir)) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
57
|
}
|
|
58
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
59
|
+
}
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Register a new short ID for a full UUID with collision detection.
|
|
63
|
+
* This must match the Electron ConfigService.registerShortId logic exactly.
|
|
64
|
+
*/
|
|
65
|
+
function registerShortId(fullId) {
|
|
66
|
+
if (!fullId) return fullId;
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
const config = loadUnifiedConfig();
|
|
69
|
+
const mappings = config.idMappings || {};
|
|
70
|
+
|
|
71
|
+
// If already registered, return existing
|
|
72
|
+
if (mappings[fullId]) {
|
|
73
|
+
return mappings[fullId];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Remove hyphens from UUID
|
|
77
|
+
const cleanId = fullId.replace(/-/g, '');
|
|
78
|
+
|
|
79
|
+
// Get all existing short IDs for collision detection
|
|
80
|
+
const existingShortIds = new Set(Object.values(mappings));
|
|
81
|
+
|
|
82
|
+
// Try last 8 chars, then 9, 10, etc. until no collision
|
|
83
|
+
let length = 8;
|
|
84
|
+
let shortId = cleanId.slice(-length);
|
|
85
|
+
|
|
86
|
+
while (existingShortIds.has(shortId) && length < cleanId.length) {
|
|
87
|
+
length++;
|
|
88
|
+
shortId = cleanId.slice(-length);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Store the mapping
|
|
92
|
+
config.idMappings = config.idMappings || {};
|
|
93
|
+
config.idMappings[fullId] = shortId;
|
|
94
|
+
saveUnifiedConfig(config);
|
|
95
|
+
|
|
96
|
+
return shortId;
|
|
56
97
|
}
|
|
57
98
|
|
|
58
99
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
100
|
+
* Get the short ID for a full UUID from the config mappings.
|
|
101
|
+
* If no mapping exists, registers a new one with collision detection.
|
|
61
102
|
*/
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
function getShortId(fullId) {
|
|
104
|
+
if (!fullId) return fullId;
|
|
105
|
+
|
|
106
|
+
const config = loadUnifiedConfig();
|
|
107
|
+
const mappings = config.idMappings || {};
|
|
108
|
+
|
|
109
|
+
// Return existing mapping if found
|
|
110
|
+
if (mappings[fullId]) {
|
|
111
|
+
return mappings[fullId];
|
|
66
112
|
}
|
|
67
113
|
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
114
|
+
// Register a new short ID with collision detection
|
|
115
|
+
return registerShortId(fullId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the full UUID from a short ID.
|
|
120
|
+
* Returns null if not found.
|
|
121
|
+
*/
|
|
122
|
+
function getFullId(shortId) {
|
|
123
|
+
if (!shortId) return null;
|
|
124
|
+
|
|
125
|
+
const config = loadUnifiedConfig();
|
|
126
|
+
const mappings = config.idMappings || {};
|
|
127
|
+
|
|
128
|
+
// Reverse lookup
|
|
129
|
+
for (const [fullId, mappedShortId] of Object.entries(mappings)) {
|
|
130
|
+
if (mappedShortId === shortId) {
|
|
131
|
+
return fullId;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
74
136
|
}
|
|
75
137
|
|
|
76
138
|
/**
|
|
77
139
|
* Load config for the currently active org
|
|
78
|
-
*
|
|
140
|
+
* Returns { apiKey, orgId, orgName } or null
|
|
79
141
|
*/
|
|
80
142
|
function loadConfig() {
|
|
81
143
|
// Check for env vars first (for CI/automation)
|
|
@@ -86,22 +148,52 @@ function loadConfig() {
|
|
|
86
148
|
};
|
|
87
149
|
}
|
|
88
150
|
|
|
89
|
-
|
|
90
|
-
|
|
151
|
+
// Load from unified config
|
|
152
|
+
const config = loadUnifiedConfig();
|
|
153
|
+
|
|
154
|
+
if (!config.currentOrg) {
|
|
91
155
|
return null;
|
|
92
156
|
}
|
|
93
157
|
|
|
94
|
-
const
|
|
95
|
-
|
|
158
|
+
const orgConfig = config.orgs[config.currentOrg];
|
|
159
|
+
if (!orgConfig || !orgConfig.apiKey) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
96
162
|
|
|
97
|
-
|
|
163
|
+
return {
|
|
164
|
+
apiKey: orgConfig.apiKey,
|
|
165
|
+
orgId: config.currentOrg,
|
|
166
|
+
orgName: orgConfig.name,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @deprecated Use loadConfig() instead - reads from unified config.json
|
|
172
|
+
*/
|
|
173
|
+
function loadActiveOrg() {
|
|
174
|
+
const config = loadUnifiedConfig();
|
|
175
|
+
if (!config.currentOrg) {
|
|
98
176
|
return null;
|
|
99
177
|
}
|
|
178
|
+
return {
|
|
179
|
+
orgId: config.currentOrg,
|
|
180
|
+
orgName: config.orgs[config.currentOrg]?.name,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
100
183
|
|
|
184
|
+
/**
|
|
185
|
+
* @deprecated Use loadConfig() instead - reads from unified config.json
|
|
186
|
+
*/
|
|
187
|
+
function loadOrgCredentials(orgId) {
|
|
188
|
+
const config = loadUnifiedConfig();
|
|
189
|
+
const orgConfig = config.orgs[orgId];
|
|
190
|
+
if (!orgConfig) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
101
193
|
return {
|
|
102
|
-
apiKey:
|
|
194
|
+
apiKey: orgConfig.apiKey,
|
|
103
195
|
orgId: orgId,
|
|
104
|
-
orgName:
|
|
196
|
+
orgName: orgConfig.name,
|
|
105
197
|
};
|
|
106
198
|
}
|
|
107
199
|
|
|
@@ -203,23 +295,28 @@ function getOrgId() {
|
|
|
203
295
|
|
|
204
296
|
/**
|
|
205
297
|
* Get the flows directory for the current org/project
|
|
206
|
-
* Returns: ~/.lux-studio/{
|
|
298
|
+
* Returns: ~/.lux-studio/{shortOrgId}/p/{shortProjectId}/f/
|
|
207
299
|
*/
|
|
208
300
|
function getFlowsDir() {
|
|
209
301
|
const orgId = getOrgId();
|
|
210
302
|
const projectId = getProjectId();
|
|
211
303
|
if (!orgId || !projectId) return null;
|
|
212
|
-
|
|
304
|
+
const shortOrgId = getShortId(orgId);
|
|
305
|
+
const shortProjectId = getShortId(projectId);
|
|
306
|
+
return path.join(LUX_STUDIO_DIR, shortOrgId, FOLDER_NAMES.projects, shortProjectId, FOLDER_NAMES.flows);
|
|
213
307
|
}
|
|
214
308
|
|
|
215
309
|
/**
|
|
216
310
|
* Load a flow from local storage
|
|
311
|
+
* New folder structure: flows/{shortFlowId}/draft.json
|
|
217
312
|
*/
|
|
218
313
|
function loadLocalFlow(flowId) {
|
|
219
314
|
const flowsDir = getFlowsDir();
|
|
220
315
|
if (!flowsDir) return null;
|
|
221
316
|
|
|
222
|
-
|
|
317
|
+
// Use short ID for folder path
|
|
318
|
+
const shortFlowId = getShortId(flowId);
|
|
319
|
+
const flowPath = path.join(flowsDir, shortFlowId, 'draft.json');
|
|
223
320
|
if (!fs.existsSync(flowPath)) return null;
|
|
224
321
|
|
|
225
322
|
try {
|
|
@@ -231,27 +328,31 @@ function loadLocalFlow(flowId) {
|
|
|
231
328
|
|
|
232
329
|
/**
|
|
233
330
|
* Save a flow to local storage
|
|
234
|
-
*
|
|
331
|
+
* New folder structure: flows/{shortFlowId}/draft.json, deployed.json
|
|
235
332
|
*/
|
|
236
333
|
function saveLocalFlow(flowId, flowData) {
|
|
237
334
|
const flowsDir = getFlowsDir();
|
|
238
335
|
if (!flowsDir) return false;
|
|
239
336
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
337
|
+
// Use short ID for folder path
|
|
338
|
+
const shortFlowId = getShortId(flowId);
|
|
339
|
+
const flowDir = path.join(flowsDir, shortFlowId);
|
|
340
|
+
|
|
341
|
+
// Ensure flow directory exists
|
|
342
|
+
if (!fs.existsSync(flowDir)) {
|
|
343
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
243
344
|
}
|
|
244
345
|
|
|
245
346
|
// Extract deployed data (same pattern as Electron storage service)
|
|
246
347
|
const { deployedNodes, deployedEdges, ...flowWithoutDeployed } = flowData;
|
|
247
348
|
|
|
248
|
-
// Write
|
|
249
|
-
const
|
|
250
|
-
fs.writeFileSync(
|
|
349
|
+
// Write draft file (without deployed data)
|
|
350
|
+
const draftPath = path.join(flowDir, 'draft.json');
|
|
351
|
+
fs.writeFileSync(draftPath, JSON.stringify(flowWithoutDeployed, null, 2));
|
|
251
352
|
|
|
252
353
|
// Write deployed data to separate file (if present)
|
|
253
354
|
if (deployedNodes || deployedEdges) {
|
|
254
|
-
const deployedPath = path.join(
|
|
355
|
+
const deployedPath = path.join(flowDir, 'deployed.json');
|
|
255
356
|
fs.writeFileSync(deployedPath, JSON.stringify({
|
|
256
357
|
deployedNodes: deployedNodes || [],
|
|
257
358
|
deployedEdges: deployedEdges || [],
|
|
@@ -263,17 +364,22 @@ function saveLocalFlow(flowId, flowData) {
|
|
|
263
364
|
|
|
264
365
|
/**
|
|
265
366
|
* List all local flows with sync status
|
|
367
|
+
* New folder structure: flows/{shortFlowId}/draft.json
|
|
266
368
|
*/
|
|
267
369
|
function listLocalFlows() {
|
|
268
370
|
const flowsDir = getFlowsDir();
|
|
269
371
|
if (!flowsDir || !fs.existsSync(flowsDir)) return [];
|
|
270
372
|
|
|
271
373
|
const flows = [];
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
374
|
+
// Now we look for directories, not .json files
|
|
375
|
+
const entries = fs.readdirSync(flowsDir, { withFileTypes: true });
|
|
376
|
+
const flowDirs = entries.filter(e => e.isDirectory());
|
|
377
|
+
|
|
378
|
+
for (const entry of flowDirs) {
|
|
379
|
+
const shortFlowId = entry.name;
|
|
380
|
+
// Get full flow ID from mapping, or use short ID as fallback
|
|
381
|
+
const fullFlowId = getFullId(shortFlowId) || shortFlowId;
|
|
382
|
+
const flow = loadLocalFlow(fullFlowId);
|
|
277
383
|
if (flow) {
|
|
278
384
|
// Determine sync status
|
|
279
385
|
let syncStatus = 'draft';
|
|
@@ -299,14 +405,18 @@ function listLocalFlows() {
|
|
|
299
405
|
|
|
300
406
|
/**
|
|
301
407
|
* Delete a local flow
|
|
408
|
+
* New folder structure: flows/{shortFlowId}/
|
|
302
409
|
*/
|
|
303
410
|
function deleteLocalFlow(flowId) {
|
|
304
411
|
const flowsDir = getFlowsDir();
|
|
305
412
|
if (!flowsDir) return false;
|
|
306
413
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
414
|
+
// Use short ID for folder path
|
|
415
|
+
const shortFlowId = getShortId(flowId);
|
|
416
|
+
const flowDir = path.join(flowsDir, shortFlowId);
|
|
417
|
+
if (fs.existsSync(flowDir)) {
|
|
418
|
+
// Remove the entire flow directory
|
|
419
|
+
fs.rmSync(flowDir, { recursive: true, force: true });
|
|
310
420
|
return true;
|
|
311
421
|
}
|
|
312
422
|
return false;
|
|
@@ -314,13 +424,25 @@ function deleteLocalFlow(flowId) {
|
|
|
314
424
|
|
|
315
425
|
/**
|
|
316
426
|
* Get the current project ID from unified config
|
|
317
|
-
* Defaults to 'default' if not set
|
|
318
427
|
*/
|
|
319
428
|
function getProjectId() {
|
|
320
|
-
|
|
321
|
-
if (
|
|
429
|
+
// Check env var first
|
|
430
|
+
if (process.env.LUX_PROJECT_ID) {
|
|
431
|
+
return process.env.LUX_PROJECT_ID;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const config = loadUnifiedConfig();
|
|
322
435
|
|
|
323
|
-
|
|
436
|
+
if (config.currentProject) {
|
|
437
|
+
return config.currentProject;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Fallback: use the first project in the current org
|
|
441
|
+
if (config.currentOrg && config.orgs?.[config.currentOrg]?.projects?.length) {
|
|
442
|
+
return config.orgs[config.currentOrg].projects[0];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return 'default';
|
|
324
446
|
}
|
|
325
447
|
|
|
326
448
|
/**
|
|
@@ -336,33 +458,37 @@ function slugify(name) {
|
|
|
336
458
|
|
|
337
459
|
/**
|
|
338
460
|
* Get the interfaces directory for the current org/project
|
|
339
|
-
* Returns: ~/.lux-studio/{
|
|
461
|
+
* Returns: ~/.lux-studio/{shortOrgId}/p/{shortProjectId}/i/
|
|
340
462
|
*/
|
|
341
463
|
function getInterfacesDir() {
|
|
342
464
|
const orgId = getOrgId();
|
|
343
465
|
if (!orgId) return null;
|
|
344
466
|
|
|
345
467
|
const projectId = getProjectId();
|
|
346
|
-
|
|
468
|
+
const shortOrgId = getShortId(orgId);
|
|
469
|
+
const shortProjectId = getShortId(projectId);
|
|
470
|
+
return path.join(LUX_STUDIO_DIR, shortOrgId, FOLDER_NAMES.projects, shortProjectId, FOLDER_NAMES.interfaces);
|
|
347
471
|
}
|
|
348
472
|
|
|
349
473
|
/**
|
|
350
|
-
* Get the directory for a specific interface by slug
|
|
351
|
-
* Returns: ~/.lux-studio/{
|
|
474
|
+
* Get the directory for a specific interface by slug or ID
|
|
475
|
+
* Returns: ~/.lux-studio/{shortOrgId}/p/{shortProjectId}/i/{shortInterfaceId}/
|
|
352
476
|
*/
|
|
353
|
-
function getInterfaceDir(
|
|
477
|
+
function getInterfaceDir(slugOrId) {
|
|
354
478
|
const interfacesDir = getInterfacesDir();
|
|
355
479
|
if (!interfacesDir) return null;
|
|
356
480
|
|
|
357
|
-
|
|
481
|
+
// If it looks like a UUID, use short ID
|
|
482
|
+
const shortId = getShortId(slugOrId);
|
|
483
|
+
return path.join(interfacesDir, shortId);
|
|
358
484
|
}
|
|
359
485
|
|
|
360
486
|
/**
|
|
361
487
|
* Get the repo directory for a specific interface
|
|
362
|
-
* Returns: ~/.lux-studio/{
|
|
488
|
+
* Returns: ~/.lux-studio/{shortOrgId}/p/{shortProjectId}/i/{shortInterfaceId}/repo/
|
|
363
489
|
*/
|
|
364
|
-
function getInterfaceRepoDir(
|
|
365
|
-
const interfaceDir = getInterfaceDir(
|
|
490
|
+
function getInterfaceRepoDir(slugOrId) {
|
|
491
|
+
const interfaceDir = getInterfaceDir(slugOrId);
|
|
366
492
|
if (!interfaceDir) return null;
|
|
367
493
|
|
|
368
494
|
return path.join(interfaceDir, 'repo');
|
|
@@ -391,6 +517,26 @@ function interfaceExists(slug) {
|
|
|
391
517
|
return interfaceDir && fs.existsSync(interfaceDir);
|
|
392
518
|
}
|
|
393
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Load interface metadata by ID
|
|
522
|
+
* @param {string} id - The interface UUID
|
|
523
|
+
* @returns {object|null} The metadata object, or null if not found
|
|
524
|
+
*/
|
|
525
|
+
function loadInterfaceMetadata(id) {
|
|
526
|
+
const interfaceDir = getInterfaceDir(id);
|
|
527
|
+
if (!interfaceDir) return null;
|
|
528
|
+
|
|
529
|
+
const metadataPath = path.join(interfaceDir, 'metadata.json');
|
|
530
|
+
if (!fs.existsSync(metadataPath)) return null;
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const content = fs.readFileSync(metadataPath, 'utf8');
|
|
534
|
+
return JSON.parse(content);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
394
540
|
/**
|
|
395
541
|
* Save interface metadata
|
|
396
542
|
* @param {string} slug - The slugified interface name (folder name)
|
|
@@ -415,6 +561,7 @@ module.exports = {
|
|
|
415
561
|
loadActiveOrg,
|
|
416
562
|
loadOrgCredentials,
|
|
417
563
|
loadUnifiedConfig,
|
|
564
|
+
saveUnifiedConfig,
|
|
418
565
|
loadInterfaceConfig,
|
|
419
566
|
saveInterfaceConfig,
|
|
420
567
|
getApiUrl,
|
|
@@ -434,8 +581,13 @@ module.exports = {
|
|
|
434
581
|
getInterfaceRepoDir,
|
|
435
582
|
ensureInterfaceDir,
|
|
436
583
|
interfaceExists,
|
|
584
|
+
loadInterfaceMetadata,
|
|
437
585
|
saveInterfaceMetadata,
|
|
438
586
|
slugify,
|
|
587
|
+
getShortId,
|
|
588
|
+
getFullId,
|
|
589
|
+
registerShortId,
|
|
439
590
|
INTERFACE_FILE,
|
|
440
591
|
LUX_STUDIO_DIR,
|
|
592
|
+
FOLDER_NAMES,
|
|
441
593
|
};
|
package/lib/helpers.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Common functions for formatting, validation, and error handling
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Format output as table
|
|
9
11
|
*/
|
|
@@ -25,14 +27,14 @@ function formatJson(data, pretty = true) {
|
|
|
25
27
|
* Print success message
|
|
26
28
|
*/
|
|
27
29
|
function success(message) {
|
|
28
|
-
console.log(
|
|
30
|
+
console.log(chalk.green(`[OK] ${message}`));
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Print error message and exit
|
|
33
35
|
*/
|
|
34
36
|
function error(message, exitCode = 1) {
|
|
35
|
-
console.error(
|
|
37
|
+
console.error(chalk.red(`[ERROR] ${message}`));
|
|
36
38
|
process.exit(exitCode);
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -40,14 +42,14 @@ function error(message, exitCode = 1) {
|
|
|
40
42
|
* Print warning message
|
|
41
43
|
*/
|
|
42
44
|
function warn(message) {
|
|
43
|
-
console.warn(
|
|
45
|
+
console.warn(chalk.yellow(`[WARN] ${message}`));
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* Print info message
|
|
48
50
|
*/
|
|
49
51
|
function info(message) {
|
|
50
|
-
console.log(
|
|
52
|
+
console.log(chalk.blue(`[INFO] ${message}`));
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
package/lux.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
require('dotenv').config({ quiet: true });
|
|
4
|
+
|
|
3
5
|
const { program } = require('commander');
|
|
4
6
|
const chalk = require('chalk');
|
|
5
7
|
const packageJson = require('./package.json');
|
|
@@ -7,7 +9,6 @@ const packageJson = require('./package.json');
|
|
|
7
9
|
// Import commands
|
|
8
10
|
const { login } = require('./commands/login');
|
|
9
11
|
const { logout } = require('./commands/logout');
|
|
10
|
-
const { dev } = require('./commands/dev');
|
|
11
12
|
const { handleInterface } = require('./commands/interface');
|
|
12
13
|
const { handleData } = require('./commands/data');
|
|
13
14
|
const { handleStorage } = require('./commands/storage');
|
|
@@ -19,8 +20,7 @@ const { handleVoiceAgents } = require('./commands/voice-agents');
|
|
|
19
20
|
const { handleValidateDataLux } = require('./commands/validate-data-lux');
|
|
20
21
|
const { handlePricing } = require('./commands/pricing');
|
|
21
22
|
const { handleProject } = require('./commands/project');
|
|
22
|
-
const { handleServers
|
|
23
|
-
const { handleTest } = require('./commands/webview');
|
|
23
|
+
const { handleServers } = require('./commands/servers');
|
|
24
24
|
const { handleABTests } = require('./commands/ab-tests');
|
|
25
25
|
const { handleTools } = require('./commands/tools');
|
|
26
26
|
const { handleSpec } = require('./commands/spec');
|
|
@@ -43,26 +43,11 @@ program
|
|
|
43
43
|
.description('Log out from Lux')
|
|
44
44
|
.action(logout);
|
|
45
45
|
|
|
46
|
-
// Dev command - local development with tunnel
|
|
47
|
-
program
|
|
48
|
-
.command('dev')
|
|
49
|
-
.description('Start local dev server with tunnel to Lux cloud')
|
|
50
|
-
.option('-p, --port <port>', 'Port to run dev server on', '3000')
|
|
51
|
-
.option('--no-tunnel', 'Disable tunnel (local only)')
|
|
52
|
-
.option('--no-sync', 'Disable auto-sync to GitHub')
|
|
53
|
-
.action((options) => {
|
|
54
|
-
dev({
|
|
55
|
-
port: parseInt(options.port, 10),
|
|
56
|
-
noTunnel: !options.tunnel,
|
|
57
|
-
noSync: !options.sync,
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
46
|
// Interface commands (with short alias 'i')
|
|
62
47
|
program
|
|
63
48
|
.command('interface [subcommand] [args...]')
|
|
64
49
|
.alias('i')
|
|
65
|
-
.description('Interface management (
|
|
50
|
+
.description('Interface management (create, deploy, list, logs, path)')
|
|
66
51
|
.allowUnknownOption()
|
|
67
52
|
.action((subcommand, args) => {
|
|
68
53
|
handleInterface(subcommand ? subcommand : 'help', args || []);
|
|
@@ -202,81 +187,6 @@ program
|
|
|
202
187
|
handleServers(subcommand ? [subcommand, ...(args || [])] : []);
|
|
203
188
|
});
|
|
204
189
|
|
|
205
|
-
// Logs command - view dev server logs
|
|
206
|
-
program
|
|
207
|
-
.command('logs [interface]')
|
|
208
|
-
.description('View logs from a running dev server')
|
|
209
|
-
.option('-f, --follow', 'Follow log output (like tail -f)')
|
|
210
|
-
.option('-n, --lines <number>', 'Number of lines to show', '50')
|
|
211
|
-
.option('-c, --console', 'Show browser console logs instead of terminal logs')
|
|
212
|
-
.action((interfaceArg, options) => {
|
|
213
|
-
handleLogs(interfaceArg ? [interfaceArg] : [], {
|
|
214
|
-
follow: options.follow,
|
|
215
|
-
lines: parseInt(options.lines, 10),
|
|
216
|
-
console: options.console,
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Webview control commands (top-level)
|
|
221
|
-
const { screenshot, click, type: typeText, evaluate, getUrl, navigate, wait } = require('./commands/webview');
|
|
222
|
-
|
|
223
|
-
program
|
|
224
|
-
.command('screenshot <interface-id>')
|
|
225
|
-
.description('Take a screenshot of the interface preview')
|
|
226
|
-
.action(async (interfaceId) => {
|
|
227
|
-
await screenshot(interfaceId);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
program
|
|
231
|
-
.command('click <interface-id> <selector>')
|
|
232
|
-
.description('Click an element in the interface preview')
|
|
233
|
-
.action(async (interfaceId, selector) => {
|
|
234
|
-
await click(interfaceId, selector);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
program
|
|
238
|
-
.command('type <interface-id> <selector> <text>')
|
|
239
|
-
.description('Type text into an element in the interface preview')
|
|
240
|
-
.action(async (interfaceId, selector, text) => {
|
|
241
|
-
await typeText(interfaceId, selector, text);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
program
|
|
245
|
-
.command('eval <interface-id> <code>')
|
|
246
|
-
.description('Execute JavaScript in the interface preview')
|
|
247
|
-
.action(async (interfaceId, code) => {
|
|
248
|
-
await evaluate(interfaceId, code);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
program
|
|
252
|
-
.command('url <interface-id>')
|
|
253
|
-
.description('Get current URL of the interface preview')
|
|
254
|
-
.action(async (interfaceId) => {
|
|
255
|
-
await getUrl(interfaceId);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
program
|
|
259
|
-
.command('nav <interface-id> <url>')
|
|
260
|
-
.description('Navigate the interface preview to a URL')
|
|
261
|
-
.action(async (interfaceId, url) => {
|
|
262
|
-
await navigate(interfaceId, url);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
program
|
|
266
|
-
.command('wait <interface-id> <ms>')
|
|
267
|
-
.description('Wait for a duration (milliseconds)')
|
|
268
|
-
.action(async (interfaceId, ms) => {
|
|
269
|
-
await wait(interfaceId, ms);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
program
|
|
273
|
-
.command('preview <interface-id>')
|
|
274
|
-
.description('Start the interface preview (opens in Lux Studio)')
|
|
275
|
-
.action(async (interfaceId) => {
|
|
276
|
-
const { startPreview } = require('./commands/webview');
|
|
277
|
-
await startPreview(interfaceId);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
190
|
// Project spec command
|
|
281
191
|
program
|
|
282
192
|
.command('spec [subcommand] [args...]')
|