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
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
|
+
};
|