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,704 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const {
|
|
8
|
+
getApiUrl,
|
|
9
|
+
getAuthHeaders,
|
|
10
|
+
isAuthenticated,
|
|
11
|
+
getOrgId,
|
|
12
|
+
getProjectId,
|
|
13
|
+
LUX_STUDIO_DIR,
|
|
14
|
+
} = require('../lib/config');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the project directory path
|
|
18
|
+
* ~/.lux-studio/{orgId}/projects/{projectId}/
|
|
19
|
+
*/
|
|
20
|
+
function getProjectDir(orgId, projectId) {
|
|
21
|
+
return path.join(LUX_STUDIO_DIR, orgId, 'projects', projectId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create .gitignore for the project
|
|
26
|
+
*/
|
|
27
|
+
function createGitignore(projectDir) {
|
|
28
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
29
|
+
const gitignoreContent = `# Dependencies
|
|
30
|
+
**/node_modules/
|
|
31
|
+
|
|
32
|
+
# Build outputs
|
|
33
|
+
**/.next/
|
|
34
|
+
**/.vercel/
|
|
35
|
+
**/dist/
|
|
36
|
+
**/build/
|
|
37
|
+
**/.turbo/
|
|
38
|
+
|
|
39
|
+
# Local KV cache (synced from cloud)
|
|
40
|
+
data/kv/
|
|
41
|
+
|
|
42
|
+
# OS files
|
|
43
|
+
.DS_Store
|
|
44
|
+
Thumbs.db
|
|
45
|
+
|
|
46
|
+
# Logs
|
|
47
|
+
*.log
|
|
48
|
+
npm-debug.log*
|
|
49
|
+
|
|
50
|
+
# Environment files (secrets)
|
|
51
|
+
.env
|
|
52
|
+
.env.local
|
|
53
|
+
.env.*.local
|
|
54
|
+
|
|
55
|
+
# IDE
|
|
56
|
+
.idea/
|
|
57
|
+
.vscode/
|
|
58
|
+
*.swp
|
|
59
|
+
*.swo
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initialize git in project directory if needed
|
|
67
|
+
*/
|
|
68
|
+
function initGitIfNeeded(projectDir) {
|
|
69
|
+
const gitDir = path.join(projectDir, '.git');
|
|
70
|
+
|
|
71
|
+
if (!fs.existsSync(gitDir)) {
|
|
72
|
+
execSync('git init', { cwd: projectDir, stdio: 'pipe' });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Deploy project to GitHub
|
|
81
|
+
* Force pushes the entire project state to the remote repository
|
|
82
|
+
*/
|
|
83
|
+
async function deployProject(projectId) {
|
|
84
|
+
// Check authentication
|
|
85
|
+
if (!isAuthenticated()) {
|
|
86
|
+
console.log(
|
|
87
|
+
chalk.red('Not authenticated. Run'),
|
|
88
|
+
chalk.white('lux login'),
|
|
89
|
+
chalk.red('first.')
|
|
90
|
+
);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const orgId = getOrgId();
|
|
95
|
+
if (!orgId) {
|
|
96
|
+
console.log(chalk.red('No organization found. Please login first.'));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Use provided projectId or get current project
|
|
101
|
+
const targetProjectId = projectId || getProjectId();
|
|
102
|
+
const projectDir = getProjectDir(orgId, targetProjectId);
|
|
103
|
+
|
|
104
|
+
// Check project exists
|
|
105
|
+
if (!fs.existsSync(projectDir)) {
|
|
106
|
+
console.log(chalk.red(`Project directory not found: ${projectDir}`));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const repoName = `${orgId}_${targetProjectId}`;
|
|
111
|
+
const apiUrl = getApiUrl();
|
|
112
|
+
|
|
113
|
+
console.log(chalk.dim(`\nProject: ${targetProjectId}`));
|
|
114
|
+
console.log(chalk.dim(`Directory: ${projectDir}`));
|
|
115
|
+
console.log(chalk.dim(`Repository: LuxUserProjects/${repoName}\n`));
|
|
116
|
+
|
|
117
|
+
// Step 0: Build check - verify all interfaces build successfully (if any exist)
|
|
118
|
+
const interfacesDir = path.join(projectDir, 'interfaces');
|
|
119
|
+
let interfaceIds = [];
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(interfacesDir)) {
|
|
122
|
+
interfaceIds = fs.readdirSync(interfacesDir).filter(f => {
|
|
123
|
+
const stat = fs.statSync(path.join(interfacesDir, f));
|
|
124
|
+
return stat.isDirectory();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (interfaceIds.length > 0) {
|
|
128
|
+
console.log(chalk.dim(`Found ${interfaceIds.length} interface(s) to build\n`));
|
|
129
|
+
|
|
130
|
+
for (const interfaceId of interfaceIds) {
|
|
131
|
+
const repoDir = path.join(interfacesDir, interfaceId, 'repo');
|
|
132
|
+
const packageJsonPath = path.join(repoDir, 'package.json');
|
|
133
|
+
|
|
134
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
135
|
+
const buildSpinner = ora(`Building interface ${interfaceId.substring(0, 8)}...`).start();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Install dependencies if node_modules doesn't exist
|
|
139
|
+
const nodeModulesPath = path.join(repoDir, 'node_modules');
|
|
140
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
141
|
+
buildSpinner.text = `Installing dependencies for ${interfaceId.substring(0, 8)}...`;
|
|
142
|
+
execSync('npm install', { cwd: repoDir, stdio: 'pipe', timeout: 300000 });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Run build
|
|
146
|
+
buildSpinner.text = `Building interface ${interfaceId.substring(0, 8)}...`;
|
|
147
|
+
execSync('npm run build', { cwd: repoDir, stdio: 'pipe', timeout: 300000 });
|
|
148
|
+
buildSpinner.succeed(`Interface ${interfaceId.substring(0, 8)} built successfully`);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
buildSpinner.fail(`Interface ${interfaceId.substring(0, 8)} build failed`);
|
|
151
|
+
|
|
152
|
+
console.log(chalk.red('\n═══════════════════════════════════════════════════════════════'));
|
|
153
|
+
console.log(chalk.red(' BUILD FAILED'));
|
|
154
|
+
console.log(chalk.red('═══════════════════════════════════════════════════════════════\n'));
|
|
155
|
+
|
|
156
|
+
// Try to extract useful error info
|
|
157
|
+
if (error.stdout) {
|
|
158
|
+
console.log(chalk.dim('--- Build Output ---'));
|
|
159
|
+
console.log(chalk.dim(error.stdout.toString().slice(-3000)));
|
|
160
|
+
}
|
|
161
|
+
if (error.stderr) {
|
|
162
|
+
console.log(chalk.dim('\n--- Error Output ---'));
|
|
163
|
+
console.log(chalk.dim(error.stderr.toString().slice(-3000)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(chalk.dim('\n───────────────────────────────────────────────────────────────'));
|
|
167
|
+
console.log(chalk.cyan('\n💡 To fix this issue, please copy the build error above and'));
|
|
168
|
+
console.log(chalk.cyan(' share it with your coding agent (Claude) for assistance.\n'));
|
|
169
|
+
console.log(chalk.dim(' Interface directory:'));
|
|
170
|
+
console.log(chalk.dim(` ${repoDir}\n`));
|
|
171
|
+
console.log(chalk.dim('───────────────────────────────────────────────────────────────\n'));
|
|
172
|
+
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
console.log(chalk.dim('No interfaces found. Skipping build step.\n'));
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
console.log(chalk.dim('No interfaces directory. Skipping build step.\n'));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 1: Get deploy credentials (GitHub token, and Vercel projects if interfaces exist)
|
|
185
|
+
const tokenSpinner = ora('Getting deploy credentials...').start();
|
|
186
|
+
|
|
187
|
+
let repoUrl, token, interfaces;
|
|
188
|
+
try {
|
|
189
|
+
const { data } = await axios.post(
|
|
190
|
+
`${apiUrl}/api/projects/${targetProjectId}/deploy/init`,
|
|
191
|
+
{ orgId, interfaceIds },
|
|
192
|
+
{ headers: getAuthHeaders() }
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
repoUrl = data.repoUrl;
|
|
196
|
+
token = data.token;
|
|
197
|
+
interfaces = data.interfaces || [];
|
|
198
|
+
|
|
199
|
+
if (interfaces.length > 0) {
|
|
200
|
+
tokenSpinner.succeed(`Got deploy credentials for ${interfaces.length} interface(s)`);
|
|
201
|
+
} else {
|
|
202
|
+
tokenSpinner.succeed('Got deploy credentials (no interfaces to deploy to Vercel)');
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
tokenSpinner.fail('Failed to get deploy credentials');
|
|
206
|
+
console.error(
|
|
207
|
+
chalk.red('\nError:'),
|
|
208
|
+
error.response?.data?.error || error.message
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (error.response?.status === 401) {
|
|
212
|
+
console.log(
|
|
213
|
+
chalk.yellow('\nYour session may have expired. Try running:'),
|
|
214
|
+
chalk.white('lux login')
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Step 2: Initialize git if needed
|
|
222
|
+
const gitSpinner = ora('Preparing git repository...').start();
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const wasInitialized = initGitIfNeeded(projectDir);
|
|
226
|
+
if (wasInitialized) {
|
|
227
|
+
gitSpinner.text = 'Initialized git repository';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create/update .gitignore
|
|
231
|
+
createGitignore(projectDir);
|
|
232
|
+
|
|
233
|
+
// Always configure git user (required for commits)
|
|
234
|
+
execSync('git config user.email "deploy@uselux.ai"', { cwd: projectDir, stdio: 'pipe' });
|
|
235
|
+
execSync('git config user.name "Lux Deploy"', { cwd: projectDir, stdio: 'pipe' });
|
|
236
|
+
|
|
237
|
+
gitSpinner.succeed('Git repository ready');
|
|
238
|
+
} catch (error) {
|
|
239
|
+
gitSpinner.fail('Failed to prepare git repository');
|
|
240
|
+
console.error(chalk.red('\nError:'), error.message);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Step 3: Stage all files
|
|
245
|
+
const stageSpinner = ora('Staging files...').start();
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
execSync('git add -A', { cwd: projectDir, stdio: 'pipe' });
|
|
249
|
+
stageSpinner.succeed('Files staged');
|
|
250
|
+
} catch (error) {
|
|
251
|
+
stageSpinner.fail('Failed to stage files');
|
|
252
|
+
console.error(chalk.red('\nError:'), error.message);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Step 4: Commit
|
|
257
|
+
const commitSpinner = ora('Creating commit...').start();
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const timestamp = new Date().toISOString();
|
|
261
|
+
const commitMessage = `Deploy ${timestamp}`;
|
|
262
|
+
|
|
263
|
+
// Check if we have any commits at all
|
|
264
|
+
let hasCommits = false;
|
|
265
|
+
try {
|
|
266
|
+
execSync('git rev-parse HEAD', { cwd: projectDir, stdio: 'pipe' });
|
|
267
|
+
hasCommits = true;
|
|
268
|
+
} catch {
|
|
269
|
+
hasCommits = false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check if there are staged changes
|
|
273
|
+
const stagedDiff = execSync('git diff --cached --name-only', { cwd: projectDir, encoding: 'utf-8' });
|
|
274
|
+
|
|
275
|
+
if (stagedDiff.trim() === '' && hasCommits) {
|
|
276
|
+
// No staged changes and already has commits
|
|
277
|
+
commitSpinner.succeed('No changes to commit');
|
|
278
|
+
} else if (!hasCommits) {
|
|
279
|
+
// First commit - use --allow-empty if needed
|
|
280
|
+
try {
|
|
281
|
+
execSync(`git commit -m "${commitMessage}"`, { cwd: projectDir, stdio: 'pipe' });
|
|
282
|
+
commitSpinner.succeed('Created initial commit');
|
|
283
|
+
} catch {
|
|
284
|
+
execSync(`git commit --allow-empty -m "${commitMessage}"`, { cwd: projectDir, stdio: 'pipe' });
|
|
285
|
+
commitSpinner.succeed('Created initial commit');
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
// Has staged changes
|
|
289
|
+
execSync(`git commit -m "${commitMessage}"`, { cwd: projectDir, stdio: 'pipe' });
|
|
290
|
+
commitSpinner.succeed('Commit created');
|
|
291
|
+
}
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// If commit fails because nothing to commit, that's OK
|
|
294
|
+
const errorStr = error.message + (error.stderr || '');
|
|
295
|
+
if (errorStr.includes('nothing to commit') || errorStr.includes('nothing added to commit')) {
|
|
296
|
+
commitSpinner.succeed('No changes to commit');
|
|
297
|
+
} else {
|
|
298
|
+
commitSpinner.fail('Failed to create commit');
|
|
299
|
+
console.error(chalk.red('\nError:'), error.message);
|
|
300
|
+
if (error.stderr) console.error(chalk.dim(error.stderr));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Step 5: Push to GitHub (force push)
|
|
306
|
+
const pushSpinner = ora('Pushing to GitHub...').start();
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// Construct authenticated URL
|
|
310
|
+
const authUrl = repoUrl.replace('https://github.com/', `https://x-access-token:${token}@github.com/`);
|
|
311
|
+
|
|
312
|
+
// Set remote (or update if exists)
|
|
313
|
+
try {
|
|
314
|
+
execSync(`git remote add origin ${authUrl}`, { cwd: projectDir, stdio: 'pipe' });
|
|
315
|
+
} catch {
|
|
316
|
+
// Remote might already exist, update it
|
|
317
|
+
execSync(`git remote set-url origin ${authUrl}`, { cwd: projectDir, stdio: 'pipe' });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Always ensure we're on main branch before pushing
|
|
321
|
+
execSync('git branch -M main', { cwd: projectDir, stdio: 'pipe' });
|
|
322
|
+
|
|
323
|
+
// Force push to main
|
|
324
|
+
execSync('git push -f origin main', { cwd: projectDir, stdio: 'pipe', timeout: 120000 });
|
|
325
|
+
|
|
326
|
+
// Clear the token from remote URL after push (security)
|
|
327
|
+
execSync(`git remote set-url origin ${repoUrl}`, { cwd: projectDir, stdio: 'pipe' });
|
|
328
|
+
|
|
329
|
+
pushSpinner.succeed('Pushed to GitHub');
|
|
330
|
+
} catch (error) {
|
|
331
|
+
pushSpinner.fail('Failed to push to GitHub');
|
|
332
|
+
console.error(chalk.red('\nError:'), error.message);
|
|
333
|
+
|
|
334
|
+
// Clear token from URL even on failure
|
|
335
|
+
try {
|
|
336
|
+
execSync(`git remote set-url origin ${repoUrl}`, { cwd: projectDir, stdio: 'pipe' });
|
|
337
|
+
} catch {
|
|
338
|
+
// Ignore
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Track overall deployment errors
|
|
345
|
+
let interfaceHasErrors = false;
|
|
346
|
+
|
|
347
|
+
// Step 6: Trigger Vercel deployment for each interface (if any)
|
|
348
|
+
if (interfaces.length > 0) {
|
|
349
|
+
console.log(chalk.dim(`\nDeploying ${interfaces.length} interface(s) to Vercel...\n`));
|
|
350
|
+
|
|
351
|
+
const deploymentResults = [];
|
|
352
|
+
|
|
353
|
+
for (const iface of interfaces) {
|
|
354
|
+
const vercelSpinner = ora(`Deploying interface ${iface.interfaceId.substring(0, 8)}...`).start();
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const { data: deployData } = await axios.post(
|
|
358
|
+
`${apiUrl}/api/projects/${targetProjectId}/deploy/trigger`,
|
|
359
|
+
{
|
|
360
|
+
vercelProjectId: iface.vercelProjectId,
|
|
361
|
+
vercelProjectName: iface.vercelProjectName,
|
|
362
|
+
repoName: repoName.toLowerCase(),
|
|
363
|
+
interfaceId: iface.interfaceId
|
|
364
|
+
},
|
|
365
|
+
{ headers: getAuthHeaders() }
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
vercelSpinner.succeed(`Interface ${iface.interfaceId.substring(0, 8)} deployment triggered`);
|
|
369
|
+
|
|
370
|
+
// Sync local custom domains to Vercel after successful deployment
|
|
371
|
+
const interfaceMetadataPath = path.join(interfacesDir, iface.interfaceId, 'metadata.json');
|
|
372
|
+
if (fs.existsSync(interfaceMetadataPath)) {
|
|
373
|
+
try {
|
|
374
|
+
const metadata = JSON.parse(fs.readFileSync(interfaceMetadataPath, 'utf-8'));
|
|
375
|
+
const localDomains = metadata.customDomains || [];
|
|
376
|
+
const pendingDomains = localDomains.filter(d => !d.synced);
|
|
377
|
+
|
|
378
|
+
if (pendingDomains.length > 0) {
|
|
379
|
+
const domainSpinner = ora(`Syncing ${pendingDomains.length} custom domain(s)...`).start();
|
|
380
|
+
|
|
381
|
+
for (const domainInfo of pendingDomains) {
|
|
382
|
+
try {
|
|
383
|
+
// Add domain to Vercel via backend API
|
|
384
|
+
await axios.post(
|
|
385
|
+
`${apiUrl}/api/interfaces/${iface.interfaceId}/domains`,
|
|
386
|
+
{ domain: domainInfo.domain },
|
|
387
|
+
{ headers: getAuthHeaders() }
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// Mark as synced in local metadata
|
|
391
|
+
domainInfo.synced = true;
|
|
392
|
+
domainInfo.syncedAt = new Date().toISOString();
|
|
393
|
+
} catch (domainError) {
|
|
394
|
+
console.log(chalk.dim(`\n Warning: Failed to add domain ${domainInfo.domain}: ${domainError.response?.data?.error || domainError.message}`));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Save updated metadata
|
|
399
|
+
fs.writeFileSync(interfaceMetadataPath, JSON.stringify(metadata, null, 2));
|
|
400
|
+
|
|
401
|
+
const syncedCount = pendingDomains.filter(d => d.synced).length;
|
|
402
|
+
if (syncedCount === pendingDomains.length) {
|
|
403
|
+
domainSpinner.succeed(`${syncedCount} custom domain(s) synced`);
|
|
404
|
+
} else {
|
|
405
|
+
domainSpinner.warn(`${syncedCount}/${pendingDomains.length} custom domain(s) synced`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch (metadataError) {
|
|
409
|
+
// Ignore metadata errors - domain sync is optional
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
deploymentResults.push({
|
|
414
|
+
interfaceId: iface.interfaceId,
|
|
415
|
+
name: iface.interfaceName || iface.interfaceId.substring(0, 8),
|
|
416
|
+
success: true,
|
|
417
|
+
url: deployData.deployment_url,
|
|
418
|
+
subdomainUrl: iface.subdomainUrl,
|
|
419
|
+
deploymentId: deployData.deployment_id
|
|
420
|
+
});
|
|
421
|
+
} catch (error) {
|
|
422
|
+
interfaceHasErrors = true;
|
|
423
|
+
vercelSpinner.fail(`Interface ${iface.interfaceId.substring(0, 8)} deployment failed`);
|
|
424
|
+
const errorData = error.response?.data;
|
|
425
|
+
deploymentResults.push({
|
|
426
|
+
interfaceId: iface.interfaceId,
|
|
427
|
+
name: iface.interfaceName || iface.interfaceId.substring(0, 8),
|
|
428
|
+
success: false,
|
|
429
|
+
error: errorData?.error || error.message
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Summary
|
|
435
|
+
console.log('');
|
|
436
|
+
if (interfaceHasErrors) {
|
|
437
|
+
console.log(chalk.yellow('═══════════════════════════════════════════════════════════════'));
|
|
438
|
+
console.log(chalk.yellow(' DEPLOYMENT COMPLETED WITH ERRORS'));
|
|
439
|
+
console.log(chalk.yellow('═══════════════════════════════════════════════════════════════\n'));
|
|
440
|
+
} else {
|
|
441
|
+
console.log(chalk.green('═══════════════════════════════════════════════════════════════'));
|
|
442
|
+
console.log(chalk.green(' ALL DEPLOYMENTS SUCCESSFUL'));
|
|
443
|
+
console.log(chalk.green('═══════════════════════════════════════════════════════════════\n'));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log(chalk.dim(`GitHub: ${repoUrl}\n`));
|
|
447
|
+
|
|
448
|
+
for (const result of deploymentResults) {
|
|
449
|
+
if (result.success) {
|
|
450
|
+
console.log(chalk.green(`✓ ${result.interfaceId.substring(0, 8)}`));
|
|
451
|
+
console.log(chalk.dim(` URL: ${result.url}`));
|
|
452
|
+
console.log(chalk.dim(` Deployment ID: ${result.deploymentId}`));
|
|
453
|
+
} else {
|
|
454
|
+
console.log(chalk.red(`✗ ${result.interfaceId.substring(0, 8)}`));
|
|
455
|
+
console.log(chalk.dim(` Error: ${result.error}`));
|
|
456
|
+
}
|
|
457
|
+
console.log('');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (interfaceHasErrors) {
|
|
461
|
+
console.log(chalk.dim('───────────────────────────────────────────────────────────────'));
|
|
462
|
+
console.log(chalk.cyan('\n💡 To debug failed deployments, check the Vercel dashboard:'));
|
|
463
|
+
console.log(chalk.dim(' https://vercel.com/dashboard\n'));
|
|
464
|
+
console.log(chalk.dim('───────────────────────────────────────────────────────────────\n'));
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
// No interfaces - just show GitHub success
|
|
468
|
+
console.log('');
|
|
469
|
+
console.log(chalk.green('═══════════════════════════════════════════════════════════════'));
|
|
470
|
+
console.log(chalk.green(' PROJECT PUSHED TO GITHUB'));
|
|
471
|
+
console.log(chalk.green('═══════════════════════════════════════════════════════════════\n'));
|
|
472
|
+
console.log(chalk.dim(`GitHub: ${repoUrl}\n`));
|
|
473
|
+
console.log(chalk.dim('No interfaces to deploy to Vercel.\n'));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Step 7: Deploy flows (if any exist)
|
|
477
|
+
const flowsDir = path.join(projectDir, 'flows');
|
|
478
|
+
if (fs.existsSync(flowsDir)) {
|
|
479
|
+
const flowFiles = fs.readdirSync(flowsDir).filter(f => f.endsWith('.json'));
|
|
480
|
+
|
|
481
|
+
if (flowFiles.length > 0) {
|
|
482
|
+
console.log(chalk.dim(`\nDeploying ${flowFiles.length} flow(s)...\n`));
|
|
483
|
+
|
|
484
|
+
const flowResults = [];
|
|
485
|
+
let flowHasErrors = false;
|
|
486
|
+
|
|
487
|
+
for (const flowFile of flowFiles) {
|
|
488
|
+
const flowId = flowFile.replace('.json', '');
|
|
489
|
+
const flowSpinner = ora(`Deploying flow ${flowId.substring(0, 8)}...`).start();
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
// Read flow data
|
|
493
|
+
const flowPath = path.join(flowsDir, flowFile);
|
|
494
|
+
const flowData = JSON.parse(fs.readFileSync(flowPath, 'utf-8'));
|
|
495
|
+
|
|
496
|
+
// Deploy to cloud API using /publish endpoint
|
|
497
|
+
// The publish endpoint supports CLI auth and will create the flow if it doesn't exist
|
|
498
|
+
const { data: deployData } = await axios.post(
|
|
499
|
+
`${apiUrl}/api/workflows/${flowId}/publish`,
|
|
500
|
+
{
|
|
501
|
+
// Wrap flow data in config object as expected by /publish
|
|
502
|
+
config: {
|
|
503
|
+
nodes: flowData.nodes,
|
|
504
|
+
edges: flowData.edges,
|
|
505
|
+
variables: flowData.variables || {},
|
|
506
|
+
metadata: {
|
|
507
|
+
name: flowData.name,
|
|
508
|
+
description: flowData.description,
|
|
509
|
+
...flowData.metadata,
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
{ headers: getAuthHeaders() }
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
flowSpinner.succeed(`Flow ${flowId.substring(0, 8)} deployed`);
|
|
517
|
+
flowResults.push({
|
|
518
|
+
flowId,
|
|
519
|
+
name: flowData.name,
|
|
520
|
+
success: true,
|
|
521
|
+
webhookUrl: deployData.workflow?.webhook_token ? `${apiUrl}/api/webhooks/${deployData.workflow.webhook_token}` : null,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Update local flow with webhook URL
|
|
525
|
+
flowData.deployedAt = new Date().toISOString();
|
|
526
|
+
const webhookToken = deployData.workflow?.webhook_token;
|
|
527
|
+
flowData.webhookUrl = webhookToken ? `${apiUrl}/api/webhooks/${webhookToken}` : null;
|
|
528
|
+
fs.writeFileSync(flowPath, JSON.stringify(flowData, null, 2));
|
|
529
|
+
} catch (error) {
|
|
530
|
+
flowHasErrors = true;
|
|
531
|
+
flowSpinner.fail(`Flow ${flowId.substring(0, 8)} deployment failed`);
|
|
532
|
+
const errorData = error.response?.data;
|
|
533
|
+
flowResults.push({
|
|
534
|
+
flowId,
|
|
535
|
+
success: false,
|
|
536
|
+
error: errorData?.error || error.message,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Flow deployment summary
|
|
542
|
+
console.log('');
|
|
543
|
+
if (flowHasErrors) {
|
|
544
|
+
console.log(chalk.yellow('Some flow deployments failed:\n'));
|
|
545
|
+
} else {
|
|
546
|
+
console.log(chalk.green('All flows deployed successfully:\n'));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const result of flowResults) {
|
|
550
|
+
if (result.success) {
|
|
551
|
+
console.log(chalk.green(`✓ ${result.name || result.flowId.substring(0, 8)}`));
|
|
552
|
+
if (result.webhookUrl) {
|
|
553
|
+
console.log(chalk.dim(` Webhook: ${result.webhookUrl}`));
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
console.log(chalk.red(`✗ ${result.flowId.substring(0, 8)}`));
|
|
557
|
+
console.log(chalk.dim(` Error: ${result.error}`));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
console.log('');
|
|
561
|
+
|
|
562
|
+
// Exit with error if any flows failed
|
|
563
|
+
if (flowHasErrors) {
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Output JSON summary for programmatic parsing (Lux Studio)
|
|
570
|
+
// Format: __LUX_DEPLOY_RESULT__{json}__END_LUX_DEPLOY_RESULT__
|
|
571
|
+
if (interfaces.length > 0) {
|
|
572
|
+
const deploymentSummary = {
|
|
573
|
+
success: !interfaceHasErrors,
|
|
574
|
+
interfaces: deploymentResults.map(r => ({
|
|
575
|
+
id: r.interfaceId,
|
|
576
|
+
name: r.name,
|
|
577
|
+
success: r.success,
|
|
578
|
+
subdomainUrl: r.subdomainUrl,
|
|
579
|
+
deploymentUrl: r.url,
|
|
580
|
+
error: r.error
|
|
581
|
+
}))
|
|
582
|
+
};
|
|
583
|
+
console.log(`__LUX_DEPLOY_RESULT__${JSON.stringify(deploymentSummary)}__END_LUX_DEPLOY_RESULT__`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Exit with error if any interface deployments failed (and no flows were deployed)
|
|
587
|
+
if (interfaceHasErrors) {
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Show project status
|
|
594
|
+
*/
|
|
595
|
+
async function projectStatus(projectId) {
|
|
596
|
+
if (!isAuthenticated()) {
|
|
597
|
+
console.log(
|
|
598
|
+
chalk.red('Not authenticated. Run'),
|
|
599
|
+
chalk.white('lux login'),
|
|
600
|
+
chalk.red('first.')
|
|
601
|
+
);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const orgId = getOrgId();
|
|
606
|
+
if (!orgId) {
|
|
607
|
+
console.log(chalk.red('No organization found. Please login first.'));
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const targetProjectId = projectId || getProjectId();
|
|
612
|
+
const projectDir = getProjectDir(orgId, targetProjectId);
|
|
613
|
+
const repoName = `${orgId}_${targetProjectId}`;
|
|
614
|
+
|
|
615
|
+
console.log(chalk.bold('\nProject Status\n'));
|
|
616
|
+
console.log(` Project ID: ${chalk.cyan(targetProjectId)}`);
|
|
617
|
+
console.log(` Directory: ${chalk.dim(projectDir)}`);
|
|
618
|
+
console.log(` Repository: ${chalk.dim(`LuxOrg/${repoName}`)}`);
|
|
619
|
+
|
|
620
|
+
// Check if project exists locally
|
|
621
|
+
if (!fs.existsSync(projectDir)) {
|
|
622
|
+
console.log(chalk.yellow('\n Local project directory not found.'));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Check git status
|
|
627
|
+
const gitDir = path.join(projectDir, '.git');
|
|
628
|
+
if (fs.existsSync(gitDir)) {
|
|
629
|
+
try {
|
|
630
|
+
const status = execSync('git status --porcelain', { cwd: projectDir, encoding: 'utf-8' });
|
|
631
|
+
const lines = status.trim().split('\n').filter(l => l.trim());
|
|
632
|
+
|
|
633
|
+
if (lines.length === 0) {
|
|
634
|
+
console.log(chalk.green('\n No uncommitted changes.'));
|
|
635
|
+
} else {
|
|
636
|
+
console.log(chalk.yellow(`\n ${lines.length} uncommitted changes:`));
|
|
637
|
+
lines.slice(0, 10).forEach(line => {
|
|
638
|
+
console.log(chalk.dim(` ${line}`));
|
|
639
|
+
});
|
|
640
|
+
if (lines.length > 10) {
|
|
641
|
+
console.log(chalk.dim(` ... and ${lines.length - 10} more`));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Check if remote exists
|
|
646
|
+
try {
|
|
647
|
+
const remote = execSync('git remote get-url origin', { cwd: projectDir, encoding: 'utf-8' }).trim();
|
|
648
|
+
console.log(chalk.dim(`\n Remote: ${remote}`));
|
|
649
|
+
} catch {
|
|
650
|
+
console.log(chalk.yellow('\n No remote configured. Run `lux project deploy` to push to GitHub.'));
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
console.log(chalk.dim('\n Unable to get git status.'));
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
console.log(chalk.yellow('\n Git not initialized. Run `lux project deploy` to initialize and push.'));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
console.log('');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Handle project commands
|
|
664
|
+
*/
|
|
665
|
+
async function handleProject(args) {
|
|
666
|
+
const subcommand = args[0];
|
|
667
|
+
const subArgs = args.slice(1);
|
|
668
|
+
|
|
669
|
+
switch (subcommand) {
|
|
670
|
+
case 'deploy':
|
|
671
|
+
case 'push': {
|
|
672
|
+
const projectId = subArgs[0]; // Optional: specific project ID
|
|
673
|
+
await deployProject(projectId);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
case 'status': {
|
|
678
|
+
const projectId = subArgs[0];
|
|
679
|
+
await projectStatus(projectId);
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
case 'help':
|
|
684
|
+
default:
|
|
685
|
+
console.log(chalk.bold('\nLux Project Commands\n'));
|
|
686
|
+
console.log('Usage: lux project <command> [options]\n');
|
|
687
|
+
console.log('Commands:');
|
|
688
|
+
console.log(' deploy [projectId] Push project to GitHub (force push entire state)');
|
|
689
|
+
console.log(' push [projectId] Alias for deploy');
|
|
690
|
+
console.log(' status [projectId] Show project status');
|
|
691
|
+
console.log('');
|
|
692
|
+
console.log('Examples:');
|
|
693
|
+
console.log(' lux project deploy Deploy current project');
|
|
694
|
+
console.log(' lux project deploy my-project-id Deploy specific project');
|
|
695
|
+
console.log(' lux project status Show current project status');
|
|
696
|
+
console.log('');
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
module.exports = {
|
|
702
|
+
handleProject,
|
|
703
|
+
deployProject,
|
|
704
|
+
};
|