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/lib/config.js ADDED
@@ -0,0 +1,403 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ // Shared storage directory (used by both Electron app and CLI)
6
+ const LUX_STUDIO_DIR = path.join(os.homedir(), '.lux-studio');
7
+ const ACTIVE_ORG_FILE = path.join(LUX_STUDIO_DIR, 'active-org.json');
8
+ const INTERFACE_FILE = '.lux/interface.json';
9
+
10
+ /**
11
+ * Load active org from shared storage
12
+ * Returns { orgId, orgName } or null
13
+ */
14
+ function loadActiveOrg() {
15
+ if (!fs.existsSync(ACTIVE_ORG_FILE)) {
16
+ return null;
17
+ }
18
+
19
+ try {
20
+ const content = fs.readFileSync(ACTIVE_ORG_FILE, 'utf8');
21
+ return JSON.parse(content);
22
+ } catch (error) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Load credentials for a specific org
29
+ * Returns { apiKey, orgId, orgName } or null
30
+ */
31
+ function loadOrgCredentials(orgId) {
32
+ const credentialsPath = path.join(LUX_STUDIO_DIR, orgId, 'credentials.json');
33
+
34
+ if (!fs.existsSync(credentialsPath)) {
35
+ return null;
36
+ }
37
+
38
+ try {
39
+ const content = fs.readFileSync(credentialsPath, 'utf8');
40
+ return JSON.parse(content);
41
+ } catch (error) {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Load config for the currently active org
48
+ * Reads from ~/.lux-studio/active-org.json to get org, then loads credentials
49
+ */
50
+ function loadConfig() {
51
+ // Check for env vars first (for CI/automation)
52
+ if (process.env.LUX_API_KEY && process.env.LUX_ORG_ID) {
53
+ return {
54
+ apiKey: process.env.LUX_API_KEY,
55
+ orgId: process.env.LUX_ORG_ID,
56
+ };
57
+ }
58
+
59
+ // Load active org from shared storage
60
+ const activeOrg = loadActiveOrg();
61
+ if (!activeOrg || !activeOrg.orgId) {
62
+ return null;
63
+ }
64
+
65
+ // Load credentials for that org
66
+ const credentials = loadOrgCredentials(activeOrg.orgId);
67
+ if (!credentials) {
68
+ return null;
69
+ }
70
+
71
+ return {
72
+ apiKey: credentials.apiKey,
73
+ orgId: credentials.orgId,
74
+ orgName: credentials.orgName,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Save config is no longer needed - Electron app manages credentials
80
+ * Kept for backwards compatibility but does nothing
81
+ */
82
+ function saveConfig(config) {
83
+ console.warn('saveConfig is deprecated - credentials are managed by Lux Studio app');
84
+ }
85
+
86
+ /**
87
+ * Load interface config (.lux/interface.json)
88
+ */
89
+ function loadInterfaceConfig() {
90
+ if (!fs.existsSync(INTERFACE_FILE)) {
91
+ return null;
92
+ }
93
+
94
+ try {
95
+ const content = fs.readFileSync(INTERFACE_FILE, 'utf8');
96
+ return JSON.parse(content);
97
+ } catch (error) {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Save interface config
104
+ */
105
+ function saveInterfaceConfig(config) {
106
+ const dir = path.dirname(INTERFACE_FILE);
107
+
108
+ if (!fs.existsSync(dir)) {
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ }
111
+
112
+ fs.writeFileSync(INTERFACE_FILE, JSON.stringify(config, null, 2));
113
+ }
114
+
115
+ /**
116
+ * Get API base URL
117
+ * For tunnel operations, this should point to the dashboard (Vercel)
118
+ * For other operations, this points to the org's local server or cloud
119
+ */
120
+ function getApiUrl() {
121
+ const config = loadConfig();
122
+ return config?.apiUrl || process.env.LUX_API_URL || 'https://app.uselux.ai';
123
+ }
124
+
125
+ /**
126
+ * Get Studio API URL (for project-scoped operations like tables)
127
+ * Points to lux-studio-api Cloudflare Worker
128
+ */
129
+ function getStudioApiUrl() {
130
+ return process.env.LUX_STUDIO_API_URL || 'https://v2.uselux.ai';
131
+ }
132
+
133
+ /**
134
+ * Get Dashboard URL (for tunnel registration)
135
+ * Always points to the Vercel-hosted dashboard
136
+ */
137
+ function getDashboardUrl() {
138
+ return process.env.LUX_DASHBOARD_URL || 'https://app.uselux.ai';
139
+ }
140
+
141
+ /**
142
+ * Check if user is authenticated
143
+ */
144
+ function isAuthenticated() {
145
+ const config = loadConfig();
146
+ return config && config.apiKey && config.orgId;
147
+ }
148
+
149
+ /**
150
+ * Get auth headers for API requests
151
+ */
152
+ function getAuthHeaders() {
153
+ const config = loadConfig();
154
+
155
+ if (!config || !config.apiKey || !config.orgId) {
156
+ return {};
157
+ }
158
+
159
+ return {
160
+ 'Authorization': `Bearer ${config.apiKey}`,
161
+ 'X-Org-Id': config.orgId,
162
+ 'X-Project-Id': getProjectId(),
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Get the org ID from config
168
+ */
169
+ function getOrgId() {
170
+ const config = loadConfig();
171
+ return config?.orgId;
172
+ }
173
+
174
+ /**
175
+ * Get the flows directory for the current org/project
176
+ * Returns: ~/.lux-studio/{orgId}/projects/{projectId}/flows/
177
+ */
178
+ function getFlowsDir() {
179
+ const orgId = getOrgId();
180
+ const projectId = getProjectId();
181
+ if (!orgId || !projectId) return null;
182
+ return path.join(LUX_STUDIO_DIR, orgId, 'projects', projectId, 'flows');
183
+ }
184
+
185
+ /**
186
+ * Load a flow from local storage
187
+ */
188
+ function loadLocalFlow(flowId) {
189
+ const flowsDir = getFlowsDir();
190
+ if (!flowsDir) return null;
191
+
192
+ const flowPath = path.join(flowsDir, `${flowId}.json`);
193
+ if (!fs.existsSync(flowPath)) return null;
194
+
195
+ try {
196
+ return JSON.parse(fs.readFileSync(flowPath, 'utf8'));
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Save a flow to local storage
204
+ */
205
+ function saveLocalFlow(flowId, flowData) {
206
+ const flowsDir = getFlowsDir();
207
+ if (!flowsDir) return false;
208
+
209
+ // Ensure directory exists
210
+ if (!fs.existsSync(flowsDir)) {
211
+ fs.mkdirSync(flowsDir, { recursive: true });
212
+ }
213
+
214
+ const flowPath = path.join(flowsDir, `${flowId}.json`);
215
+ fs.writeFileSync(flowPath, JSON.stringify(flowData, null, 2));
216
+ return true;
217
+ }
218
+
219
+ /**
220
+ * List all local flows with sync status
221
+ */
222
+ function listLocalFlows() {
223
+ const flowsDir = getFlowsDir();
224
+ if (!flowsDir || !fs.existsSync(flowsDir)) return [];
225
+
226
+ const flows = [];
227
+ const files = fs.readdirSync(flowsDir).filter(f => f.endsWith('.json'));
228
+
229
+ for (const file of files) {
230
+ const flowId = file.replace('.json', '');
231
+ const flow = loadLocalFlow(flowId);
232
+ if (flow) {
233
+ // Determine sync status
234
+ let syncStatus = 'draft';
235
+ if (flow.publishedVersion) {
236
+ if (flow.cloudVersion && flow.cloudVersion > flow.publishedVersion) {
237
+ syncStatus = flow.localVersion > flow.publishedVersion ? 'conflict' : 'synced';
238
+ } else if (flow.localVersion > flow.publishedVersion) {
239
+ syncStatus = 'dirty';
240
+ } else {
241
+ syncStatus = 'synced';
242
+ }
243
+ }
244
+
245
+ flows.push({
246
+ ...flow,
247
+ syncStatus,
248
+ });
249
+ }
250
+ }
251
+
252
+ return flows;
253
+ }
254
+
255
+ /**
256
+ * Delete a local flow
257
+ */
258
+ function deleteLocalFlow(flowId) {
259
+ const flowsDir = getFlowsDir();
260
+ if (!flowsDir) return false;
261
+
262
+ const flowPath = path.join(flowsDir, `${flowId}.json`);
263
+ if (fs.existsSync(flowPath)) {
264
+ fs.unlinkSync(flowPath);
265
+ return true;
266
+ }
267
+ return false;
268
+ }
269
+
270
+ /**
271
+ * Get the current project ID from org config
272
+ * Defaults to 'default' if not set
273
+ */
274
+ function getProjectId() {
275
+ const orgId = getOrgId();
276
+ if (!orgId) return 'default';
277
+
278
+ const orgConfigPath = path.join(LUX_STUDIO_DIR, orgId, 'config.json');
279
+ if (!fs.existsSync(orgConfigPath)) return 'default';
280
+
281
+ try {
282
+ const orgConfig = JSON.parse(fs.readFileSync(orgConfigPath, 'utf8'));
283
+ return orgConfig.currentProject || 'default';
284
+ } catch {
285
+ return 'default';
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Slugify a name for use as folder name
291
+ * Converts "My Interface" to "my-interface"
292
+ */
293
+ function slugify(name) {
294
+ return name
295
+ .toLowerCase()
296
+ .replace(/[^a-z0-9]+/g, '-')
297
+ .replace(/^-|-$/g, '');
298
+ }
299
+
300
+ /**
301
+ * Get the interfaces directory for the current org/project
302
+ * Returns: ~/.lux-studio/{orgId}/projects/{projectId}/interfaces/
303
+ */
304
+ function getInterfacesDir() {
305
+ const orgId = getOrgId();
306
+ if (!orgId) return null;
307
+
308
+ const projectId = getProjectId();
309
+ return path.join(LUX_STUDIO_DIR, orgId, 'projects', projectId, 'interfaces');
310
+ }
311
+
312
+ /**
313
+ * Get the directory for a specific interface by slug
314
+ * Returns: ~/.lux-studio/{orgId}/projects/{projectId}/interfaces/{slug}/
315
+ */
316
+ function getInterfaceDir(slug) {
317
+ const interfacesDir = getInterfacesDir();
318
+ if (!interfacesDir) return null;
319
+
320
+ return path.join(interfacesDir, slug);
321
+ }
322
+
323
+ /**
324
+ * Get the repo directory for a specific interface
325
+ * Returns: ~/.lux-studio/{orgId}/projects/{projectId}/interfaces/{slug}/repo/
326
+ */
327
+ function getInterfaceRepoDir(slug) {
328
+ const interfaceDir = getInterfaceDir(slug);
329
+ if (!interfaceDir) return null;
330
+
331
+ return path.join(interfaceDir, 'repo');
332
+ }
333
+
334
+ /**
335
+ * Ensure interface directory exists and return the path
336
+ * @param {string} slug - The slugified interface name
337
+ */
338
+ function ensureInterfaceDir(slug) {
339
+ const interfaceDir = getInterfaceDir(slug);
340
+ if (!interfaceDir) return null;
341
+
342
+ if (!fs.existsSync(interfaceDir)) {
343
+ fs.mkdirSync(interfaceDir, { recursive: true });
344
+ }
345
+
346
+ return interfaceDir;
347
+ }
348
+
349
+ /**
350
+ * Check if interface folder already exists by slug
351
+ */
352
+ function interfaceExists(slug) {
353
+ const interfaceDir = getInterfaceDir(slug);
354
+ return interfaceDir && fs.existsSync(interfaceDir);
355
+ }
356
+
357
+ /**
358
+ * Save interface metadata
359
+ * @param {string} slug - The slugified interface name (folder name)
360
+ * @param {object} metadata - Metadata including { id: uuid, name, ... }
361
+ */
362
+ function saveInterfaceMetadata(slug, metadata) {
363
+ const interfaceDir = ensureInterfaceDir(slug);
364
+ if (!interfaceDir) return false;
365
+
366
+ const metadataPath = path.join(interfaceDir, 'metadata.json');
367
+ fs.writeFileSync(metadataPath, JSON.stringify({
368
+ ...metadata,
369
+ updatedAt: new Date().toISOString(),
370
+ }, null, 2));
371
+
372
+ return true;
373
+ }
374
+
375
+ module.exports = {
376
+ loadConfig,
377
+ saveConfig,
378
+ loadActiveOrg,
379
+ loadOrgCredentials,
380
+ loadInterfaceConfig,
381
+ saveInterfaceConfig,
382
+ getApiUrl,
383
+ getStudioApiUrl,
384
+ getDashboardUrl,
385
+ isAuthenticated,
386
+ getAuthHeaders,
387
+ getOrgId,
388
+ getFlowsDir,
389
+ loadLocalFlow,
390
+ saveLocalFlow,
391
+ listLocalFlows,
392
+ deleteLocalFlow,
393
+ getProjectId,
394
+ getInterfacesDir,
395
+ getInterfaceDir,
396
+ getInterfaceRepoDir,
397
+ ensureInterfaceDir,
398
+ interfaceExists,
399
+ saveInterfaceMetadata,
400
+ slugify,
401
+ INTERFACE_FILE,
402
+ LUX_STUDIO_DIR,
403
+ };
package/lib/helpers.js ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Shared Helper Utilities
3
+ *
4
+ * Common functions for formatting, validation, and error handling
5
+ */
6
+
7
+ /**
8
+ * Format output as table
9
+ */
10
+ function formatTable(rows) {
11
+ if (!rows || rows.length === 0) {
12
+ return '(No results)';
13
+ }
14
+ console.table(rows);
15
+ }
16
+
17
+ /**
18
+ * Format output as JSON
19
+ */
20
+ function formatJson(data, pretty = true) {
21
+ return JSON.stringify(data, null, pretty ? 2 : 0);
22
+ }
23
+
24
+ /**
25
+ * Print success message
26
+ */
27
+ function success(message) {
28
+ console.log(`✅ ${message}`);
29
+ }
30
+
31
+ /**
32
+ * Print error message and exit
33
+ */
34
+ function error(message, exitCode = 1) {
35
+ console.error(`❌ ${message}`);
36
+ process.exit(exitCode);
37
+ }
38
+
39
+ /**
40
+ * Print warning message
41
+ */
42
+ function warn(message) {
43
+ console.warn(`⚠️ ${message}`);
44
+ }
45
+
46
+ /**
47
+ * Print info message
48
+ */
49
+ function info(message) {
50
+ console.log(`ℹ️ ${message}`);
51
+ }
52
+
53
+ /**
54
+ * Format file size
55
+ */
56
+ function formatSize(bytes) {
57
+ if (bytes === 0) return '0 B';
58
+ const k = 1024;
59
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
60
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
61
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
62
+ }
63
+
64
+ /**
65
+ * Format date
66
+ */
67
+ function formatDate(timestamp) {
68
+ if (!timestamp) return 'N/A';
69
+ const date = new Date(timestamp);
70
+ return date.toISOString().split('T')[0] + ' ' + date.toTimeString().split(' ')[0];
71
+ }
72
+
73
+ /**
74
+ * Validate required arguments
75
+ */
76
+ function requireArgs(args, count, usage) {
77
+ if (args.length < count) {
78
+ error(`Missing required arguments\n\nUsage: ${usage}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Parse JSON safely
84
+ */
85
+ function parseJson(jsonString, errorContext = 'JSON') {
86
+ try {
87
+ return JSON.parse(jsonString);
88
+ } catch (err) {
89
+ error(`Invalid ${errorContext}: ${err.message}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Read file safely
95
+ */
96
+ function readFile(filePath) {
97
+ const fs = require('fs');
98
+ try {
99
+ return fs.readFileSync(filePath, 'utf-8');
100
+ } catch (err) {
101
+ error(`Failed to read file ${filePath}: ${err.message}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Write file safely
107
+ */
108
+ function writeFile(filePath, content) {
109
+ const fs = require('fs');
110
+ try {
111
+ fs.writeFileSync(filePath, content, 'utf-8');
112
+ success(`Written to ${filePath}`);
113
+ } catch (err) {
114
+ error(`Failed to write file ${filePath}: ${err.message}`);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Check if file exists
120
+ */
121
+ function fileExists(filePath) {
122
+ const fs = require('fs');
123
+ return fs.existsSync(filePath);
124
+ }
125
+
126
+ /**
127
+ * Prompt user for input
128
+ */
129
+ async function prompt(question) {
130
+ const readline = require('readline');
131
+ const rl = readline.createInterface({
132
+ input: process.stdin,
133
+ output: process.stdout
134
+ });
135
+
136
+ return new Promise((resolve) => {
137
+ rl.question(question, (answer) => {
138
+ rl.close();
139
+ resolve(answer.trim());
140
+ });
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Confirm action
146
+ */
147
+ async function confirm(question) {
148
+ const answer = await prompt(`${question} (y/n): `);
149
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
150
+ }
151
+
152
+ /**
153
+ * Show loading spinner
154
+ */
155
+ function spinner(text) {
156
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
157
+ let i = 0;
158
+
159
+ const interval = setInterval(() => {
160
+ process.stdout.write(`\r${frames[i]} ${text}`);
161
+ i = (i + 1) % frames.length;
162
+ }, 80);
163
+
164
+ return {
165
+ stop: () => {
166
+ clearInterval(interval);
167
+ process.stdout.write('\r');
168
+ }
169
+ };
170
+ }
171
+
172
+ module.exports = {
173
+ formatTable,
174
+ formatJson,
175
+ success,
176
+ error,
177
+ warn,
178
+ info,
179
+ formatSize,
180
+ formatDate,
181
+ requireArgs,
182
+ parseJson,
183
+ readFile,
184
+ writeFile,
185
+ fileExists,
186
+ prompt,
187
+ confirm,
188
+ spinner
189
+ };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Node.js Runtime Helper for CLI
3
+ *
4
+ * Provides paths to the bundled Node.js runtime.
5
+ * This is a CommonJS version for use in CLI scripts.
6
+ */
7
+
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ const NODE_VERSION = '20.18.1';
12
+
13
+ /**
14
+ * Get the base directory for bundled Node.js runtime
15
+ */
16
+ function getBundledNodeDir() {
17
+ const platform = process.platform;
18
+ const arch = process.arch;
19
+
20
+ // In CLI context, look for resources relative to the CLI location
21
+ const possiblePaths = [
22
+ // Development: from bin/lux-cli/lib/
23
+ path.join(__dirname, '..', '..', '..', 'resources', 'node-runtime'),
24
+ // Development: from project root
25
+ path.join(process.cwd(), 'resources', 'node-runtime'),
26
+ // Packaged: from resources path if available
27
+ process.resourcesPath ? path.join(process.resourcesPath, 'node-runtime') : null,
28
+ ].filter(Boolean);
29
+
30
+ const baseDir = possiblePaths.find(p => fs.existsSync(p)) || possiblePaths[0];
31
+
32
+ return path.join(baseDir, `${platform}-${arch}`);
33
+ }
34
+
35
+ /**
36
+ * Get the path to the bundled Node.js binary
37
+ */
38
+ function getBundledNodePath() {
39
+ const nodeDir = getBundledNodeDir();
40
+ const binary = process.platform === 'win32' ? 'node.exe' : 'bin/node';
41
+ return path.join(nodeDir, binary);
42
+ }
43
+
44
+ /**
45
+ * Get the path to the bundled npm binary
46
+ */
47
+ function getBundledNpmPath() {
48
+ const nodeDir = getBundledNodeDir();
49
+
50
+ if (process.platform === 'win32') {
51
+ return path.join(nodeDir, 'npm.cmd');
52
+ }
53
+
54
+ return path.join(nodeDir, 'bin', 'npm');
55
+ }
56
+
57
+ /**
58
+ * Get environment variables with bundled Node.js in PATH
59
+ */
60
+ function getBundledNodeEnv() {
61
+ const nodeDir = getBundledNodeDir();
62
+ const binPath = process.platform === 'win32' ? nodeDir : path.join(nodeDir, 'bin');
63
+
64
+ return {
65
+ ...process.env,
66
+ PATH: `${binPath}${path.delimiter}${process.env.PATH}`,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Check if the bundled Node.js is available
72
+ */
73
+ function isBundledNodeAvailable() {
74
+ const nodePath = getBundledNodePath();
75
+ return fs.existsSync(nodePath);
76
+ }
77
+
78
+ /**
79
+ * Get the path to use for node command (with fallback)
80
+ */
81
+ function getNodePath() {
82
+ if (isBundledNodeAvailable()) {
83
+ return getBundledNodePath();
84
+ }
85
+ // Fallback to system node
86
+ return 'node';
87
+ }
88
+
89
+ /**
90
+ * Get the path to use for npm command (with fallback)
91
+ */
92
+ function getNpmPath() {
93
+ if (isBundledNodeAvailable()) {
94
+ return getBundledNpmPath();
95
+ }
96
+ // Fallback to system npm
97
+ return 'npm';
98
+ }
99
+
100
+ /**
101
+ * Get environment to use for spawning processes
102
+ */
103
+ function getNodeEnv() {
104
+ if (isBundledNodeAvailable()) {
105
+ return getBundledNodeEnv();
106
+ }
107
+ return { ...process.env };
108
+ }
109
+
110
+ module.exports = {
111
+ NODE_VERSION,
112
+ getBundledNodeDir,
113
+ getBundledNodePath,
114
+ getBundledNpmPath,
115
+ getBundledNodeEnv,
116
+ isBundledNodeAvailable,
117
+ getNodePath,
118
+ getNpmPath,
119
+ getNodeEnv,
120
+ };