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,52 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the templates directory path
|
|
6
|
+
* Uses the canonical templates directory at project root (single source of truth)
|
|
7
|
+
*/
|
|
8
|
+
function getTemplatesDir() {
|
|
9
|
+
return path.join(__dirname, '../../../../templates/interface-boilerplate');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Recursively copy a directory
|
|
14
|
+
*/
|
|
15
|
+
function copyDirRecursive(src, dest) {
|
|
16
|
+
if (!fs.existsSync(dest)) {
|
|
17
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const srcPath = path.join(src, entry.name);
|
|
24
|
+
const destPath = path.join(dest, entry.name);
|
|
25
|
+
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
copyDirRecursive(srcPath, destPath);
|
|
28
|
+
} else {
|
|
29
|
+
fs.copyFileSync(srcPath, destPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create boilerplate files in a directory
|
|
36
|
+
* Copies from the templates/ directory
|
|
37
|
+
*/
|
|
38
|
+
function writeBoilerplateFiles(targetDir) {
|
|
39
|
+
const templatesDir = getTemplatesDir();
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(templatesDir)) {
|
|
42
|
+
throw new Error(`Templates directory not found at ${templatesDir}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
copyDirRecursive(templatesDir, targetDir);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
getTemplatesDir,
|
|
50
|
+
copyDirRecursive,
|
|
51
|
+
writeBoilerplateFiles,
|
|
52
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run a git command and return a promise
|
|
6
|
+
*/
|
|
7
|
+
function runGitCommand(args, cwd, timeoutMs = 60000) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const proc = spawn('git', args, { cwd, stdio: 'pipe' });
|
|
10
|
+
|
|
11
|
+
let stderr = '';
|
|
12
|
+
proc.stdout?.on('data', () => {}); // Consume stdout to prevent buffer deadlock
|
|
13
|
+
proc.stderr?.on('data', (data) => {
|
|
14
|
+
stderr += data.toString();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
proc.on('close', (code) => {
|
|
18
|
+
if (code === 0) {
|
|
19
|
+
resolve();
|
|
20
|
+
} else {
|
|
21
|
+
reject(new Error(`git ${args[0]} failed with code ${code}: ${stderr}`));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
proc.on('error', reject);
|
|
26
|
+
|
|
27
|
+
const timeout = setTimeout(() => {
|
|
28
|
+
proc.kill();
|
|
29
|
+
reject(new Error(`git ${args[0]} timed out`));
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
|
|
32
|
+
proc.on('close', () => clearTimeout(timeout));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clone a git repository with retry logic
|
|
38
|
+
*/
|
|
39
|
+
async function cloneRepoWithRetry(url, targetDir, maxRetries = 5) {
|
|
40
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
const cloneProc = spawn('git', ['clone', url, targetDir], { stdio: 'pipe' });
|
|
44
|
+
|
|
45
|
+
let stderr = '';
|
|
46
|
+
cloneProc.stdout?.on('data', () => {}); // Consume stdout to prevent buffer deadlock
|
|
47
|
+
cloneProc.stderr?.on('data', (data) => {
|
|
48
|
+
stderr += data.toString();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
cloneProc.on('close', (code) => {
|
|
52
|
+
if (code === 0) {
|
|
53
|
+
resolve();
|
|
54
|
+
} else {
|
|
55
|
+
reject(new Error(`Git clone failed with code ${code}: ${stderr}`));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
cloneProc.on('error', reject);
|
|
60
|
+
|
|
61
|
+
// Timeout after 60 seconds
|
|
62
|
+
setTimeout(() => reject(new Error('Git clone timed out')), 60000);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return true; // Success
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (attempt < maxRetries) {
|
|
68
|
+
const delay = attempt * 2000; // 2s, 4s, 6s, 8s
|
|
69
|
+
await new Promise(r => setTimeout(r, delay));
|
|
70
|
+
|
|
71
|
+
// Clean up failed clone directory
|
|
72
|
+
if (fs.existsSync(targetDir)) {
|
|
73
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
throw err; // All retries exhausted
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
runGitCommand,
|
|
84
|
+
cloneRepoWithRetry,
|
|
85
|
+
};
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const {
|
|
8
|
+
getStudioApiUrl,
|
|
9
|
+
getAuthHeaders,
|
|
10
|
+
isAuthenticated,
|
|
11
|
+
getInterfaceRepoDir,
|
|
12
|
+
ensureInterfaceDir,
|
|
13
|
+
interfaceExists,
|
|
14
|
+
saveInterfaceMetadata,
|
|
15
|
+
loadConfig,
|
|
16
|
+
slugify,
|
|
17
|
+
} = require('../../lib/config');
|
|
18
|
+
const { runGitCommand } = require('./git-utils');
|
|
19
|
+
const { writeBoilerplateFiles } = require('./boilerplate');
|
|
20
|
+
const { getNpmPath, getNodeEnv } = require('../../lib/node-helper');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run npm install with optimized flags using bundled Node.js
|
|
24
|
+
*/
|
|
25
|
+
function runNpmInstall(cwd) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const npmPath = getNpmPath();
|
|
28
|
+
const nodeEnv = getNodeEnv();
|
|
29
|
+
|
|
30
|
+
const npmProc = spawn(npmPath, ['install', '--prefer-offline', '--no-audit', '--no-fund'], {
|
|
31
|
+
cwd,
|
|
32
|
+
stdio: 'pipe',
|
|
33
|
+
shell: process.platform === 'win32',
|
|
34
|
+
env: nodeEnv,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
npmProc.on('close', (code) => {
|
|
38
|
+
if (code === 0) resolve();
|
|
39
|
+
else reject(new Error(`npm install exited with code ${code}`));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
npmProc.on('error', reject);
|
|
43
|
+
|
|
44
|
+
// Timeout after 3 minutes
|
|
45
|
+
setTimeout(() => reject(new Error('npm install timed out')), 3 * 60 * 1000);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize local repo and push to GitHub
|
|
51
|
+
*/
|
|
52
|
+
async function initAndPushToGitHub(repoDir, githubRepoUrl, githubToken) {
|
|
53
|
+
const targetUrl = githubRepoUrl.replace('https://github.com/', `https://${githubToken}@github.com/`) + '.git';
|
|
54
|
+
|
|
55
|
+
// Initialize git repo
|
|
56
|
+
await runGitCommand(['init'], repoDir);
|
|
57
|
+
await runGitCommand(['add', '.'], repoDir);
|
|
58
|
+
await runGitCommand(['commit', '-m', 'Initial commit from Lux'], repoDir);
|
|
59
|
+
|
|
60
|
+
// Add remote and push to main
|
|
61
|
+
await runGitCommand(['remote', 'add', 'origin', targetUrl], repoDir);
|
|
62
|
+
await runGitCommand(['branch', '-M', 'main'], repoDir);
|
|
63
|
+
await runGitCommand(['push', '-u', 'origin', 'main', '--force'], repoDir);
|
|
64
|
+
|
|
65
|
+
// Remove .git directory after push (we don't want nested git repos)
|
|
66
|
+
const gitDir = path.join(repoDir, '.git');
|
|
67
|
+
if (fs.existsSync(gitDir)) {
|
|
68
|
+
fs.rmSync(gitDir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize a new interface with boilerplate code
|
|
74
|
+
* Creates git repo, package.json, and registers in system.interfaces table
|
|
75
|
+
*/
|
|
76
|
+
async function initInterface(options) {
|
|
77
|
+
// Check authentication
|
|
78
|
+
if (!isAuthenticated()) {
|
|
79
|
+
console.log(
|
|
80
|
+
chalk.red('❌ Not authenticated. Run'),
|
|
81
|
+
chalk.white('lux login'),
|
|
82
|
+
chalk.red('first.')
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(chalk.cyan('\n📦 Initialize New App\n'));
|
|
88
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
89
|
+
console.log(chalk.blue('[STEP 0/6]'), 'Validating options...');
|
|
90
|
+
|
|
91
|
+
// Get interface details from flags (non-interactive)
|
|
92
|
+
let name = options.name;
|
|
93
|
+
let description = options.description || '';
|
|
94
|
+
|
|
95
|
+
if (!name) {
|
|
96
|
+
console.log(chalk.red('❌ --name flag is required'));
|
|
97
|
+
console.log(chalk.dim('Usage: lux i init --name <name> [--description <desc>]'));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Slugify the name for folder
|
|
102
|
+
const slug = slugify(name);
|
|
103
|
+
console.log(chalk.dim(' Name:'), name);
|
|
104
|
+
console.log(chalk.dim(' Slug:'), slug);
|
|
105
|
+
|
|
106
|
+
// Check if interface with this slug already exists
|
|
107
|
+
if (interfaceExists(slug)) {
|
|
108
|
+
console.log(chalk.red(`❌ Interface already exists: ${slug}`));
|
|
109
|
+
console.log(chalk.dim('Use a different name or delete the existing interface first.'));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(chalk.green(' ✓ Options validated'));
|
|
114
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const apiUrl = getStudioApiUrl();
|
|
118
|
+
|
|
119
|
+
// STEP 1: Create interface via API (registers in system.interfaces + creates GitHub repo)
|
|
120
|
+
console.log(chalk.blue('[STEP 1/6]'), 'Creating interface via API...');
|
|
121
|
+
console.log(chalk.dim(' API URL:'), apiUrl);
|
|
122
|
+
console.log(chalk.dim(' Endpoint:'), '/api/interfaces');
|
|
123
|
+
|
|
124
|
+
const { data } = await axios.post(
|
|
125
|
+
`${apiUrl}/api/interfaces`,
|
|
126
|
+
{
|
|
127
|
+
name,
|
|
128
|
+
description,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
headers: getAuthHeaders(),
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const interfaceData = data.interface || data;
|
|
136
|
+
const interfaceId = interfaceData.id; // UUID from API - stored in metadata
|
|
137
|
+
const githubRepoUrl = interfaceData.github_repo_url;
|
|
138
|
+
const vercelProjectId = interfaceData.vercel_project_id;
|
|
139
|
+
const vercelProjectUrl = interfaceData.vercel_project_url;
|
|
140
|
+
|
|
141
|
+
console.log(chalk.green(' ✓ Interface registered in database'));
|
|
142
|
+
console.log(chalk.dim(' Interface ID:'), interfaceId);
|
|
143
|
+
if (githubRepoUrl) {
|
|
144
|
+
console.log(chalk.green(' ✓ GitHub repository created'));
|
|
145
|
+
console.log(chalk.dim(' GitHub URL:'), githubRepoUrl);
|
|
146
|
+
} else {
|
|
147
|
+
console.log(chalk.yellow(' ⚠ No GitHub repository URL returned'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (vercelProjectId) {
|
|
151
|
+
console.log(chalk.green(' ✓ Vercel project created'));
|
|
152
|
+
console.log(chalk.dim(' Vercel Project ID:'), vercelProjectId);
|
|
153
|
+
if (vercelProjectUrl) {
|
|
154
|
+
console.log(chalk.dim(' Vercel Dashboard:'), vercelProjectUrl);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
console.log(chalk.yellow(' ⚠ Vercel project not created (may need VERCEL_TOKEN)'));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Output parseable format for API wrapper
|
|
161
|
+
console.log(`App created: ${interfaceId}`);
|
|
162
|
+
if (githubRepoUrl) {
|
|
163
|
+
console.log(`GitHub repo: ${githubRepoUrl}`);
|
|
164
|
+
}
|
|
165
|
+
if (vercelProjectId) {
|
|
166
|
+
console.log(`Vercel project: ${vercelProjectId}`);
|
|
167
|
+
}
|
|
168
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
169
|
+
|
|
170
|
+
// Set up local files and push to GitHub
|
|
171
|
+
if (githubRepoUrl) {
|
|
172
|
+
// STEP 2: Get GitHub token
|
|
173
|
+
console.log(chalk.blue('[STEP 2/6]'), 'Fetching GitHub token...');
|
|
174
|
+
const config = loadConfig();
|
|
175
|
+
let githubToken = config?.githubPat;
|
|
176
|
+
|
|
177
|
+
// If no local githubPat, fetch from API
|
|
178
|
+
if (!githubToken) {
|
|
179
|
+
console.log(chalk.dim(' No local token, fetching from API...'));
|
|
180
|
+
try {
|
|
181
|
+
const tokenRes = await axios.get(`${apiUrl}/api/studio/github-token`, {
|
|
182
|
+
headers: getAuthHeaders(),
|
|
183
|
+
});
|
|
184
|
+
if (tokenRes.data?.token) {
|
|
185
|
+
githubToken = tokenRes.data.token;
|
|
186
|
+
console.log(chalk.green(' ✓ GitHub token fetched from API'));
|
|
187
|
+
} else {
|
|
188
|
+
console.log(chalk.yellow(' ⚠ No token returned from API'));
|
|
189
|
+
}
|
|
190
|
+
} catch (tokenErr) {
|
|
191
|
+
console.log(chalk.yellow(' ⚠ Failed to fetch token:'), tokenErr.message);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.log(chalk.green(' ✓ Using local GitHub token'));
|
|
195
|
+
}
|
|
196
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
197
|
+
|
|
198
|
+
// STEP 3: Create interface directory and copy template files
|
|
199
|
+
console.log(chalk.blue('[STEP 3/6]'), 'Copying template files...');
|
|
200
|
+
try {
|
|
201
|
+
const interfaceDir = ensureInterfaceDir(interfaceId);
|
|
202
|
+
if (!interfaceDir) {
|
|
203
|
+
throw new Error('Could not create interface directory (not authenticated or no org ID)');
|
|
204
|
+
}
|
|
205
|
+
console.log(chalk.dim(' Interface dir:'), interfaceDir);
|
|
206
|
+
|
|
207
|
+
const repoDir = getInterfaceRepoDir(interfaceId);
|
|
208
|
+
console.log(chalk.dim(' Repo dir:'), repoDir);
|
|
209
|
+
|
|
210
|
+
if (fs.existsSync(repoDir)) {
|
|
211
|
+
console.log(chalk.yellow(' ⚠ Project directory already exists'));
|
|
212
|
+
} else {
|
|
213
|
+
fs.mkdirSync(repoDir, { recursive: true });
|
|
214
|
+
console.log(chalk.dim(' Created directory'));
|
|
215
|
+
|
|
216
|
+
const templatesDir = path.join(__dirname, '../../templates');
|
|
217
|
+
console.log(chalk.dim(' Templates from:'), templatesDir);
|
|
218
|
+
|
|
219
|
+
if (!fs.existsSync(templatesDir)) {
|
|
220
|
+
throw new Error(`Templates directory not found at ${templatesDir}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
writeBoilerplateFiles(repoDir);
|
|
224
|
+
|
|
225
|
+
// List what was copied
|
|
226
|
+
const copiedFiles = fs.readdirSync(repoDir);
|
|
227
|
+
console.log(chalk.dim(' Copied files:'), copiedFiles.join(', '));
|
|
228
|
+
console.log(chalk.green(' ✓ Template files copied successfully'));
|
|
229
|
+
}
|
|
230
|
+
} catch (copyError) {
|
|
231
|
+
console.log(chalk.red(' ✗ Failed to copy templates:'), copyError.message);
|
|
232
|
+
throw copyError;
|
|
233
|
+
}
|
|
234
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
235
|
+
|
|
236
|
+
// STEP 4: Install dependencies
|
|
237
|
+
console.log(chalk.blue('[STEP 4/6]'), 'Installing npm dependencies...');
|
|
238
|
+
const repoDir = getInterfaceRepoDir(interfaceId);
|
|
239
|
+
console.log(chalk.dim(' Running: npm install --prefer-offline --no-audit --no-fund'));
|
|
240
|
+
try {
|
|
241
|
+
const npmStart = Date.now();
|
|
242
|
+
await runNpmInstall(repoDir);
|
|
243
|
+
const npmDuration = ((Date.now() - npmStart) / 1000).toFixed(1);
|
|
244
|
+
console.log(chalk.green(` ✓ Dependencies installed (${npmDuration}s)`));
|
|
245
|
+
} catch (npmError) {
|
|
246
|
+
console.log(chalk.yellow(` ⚠ npm install failed: ${npmError.message}`));
|
|
247
|
+
console.log(chalk.dim(' Dependencies will be installed when you start the dev server'));
|
|
248
|
+
}
|
|
249
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
250
|
+
|
|
251
|
+
// STEP 5: Initialize git repository
|
|
252
|
+
console.log(chalk.blue('[STEP 5/6]'), 'Initializing git repository...');
|
|
253
|
+
try {
|
|
254
|
+
// Check if .gitignore exists
|
|
255
|
+
const gitignorePath = path.join(repoDir, '.gitignore');
|
|
256
|
+
if (fs.existsSync(gitignorePath)) {
|
|
257
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
258
|
+
const hasNodeModules = gitignoreContent.includes('node_modules');
|
|
259
|
+
console.log(chalk.dim(' .gitignore exists:'), hasNodeModules ? 'includes node_modules' : 'WARNING: missing node_modules!');
|
|
260
|
+
} else {
|
|
261
|
+
console.log(chalk.yellow(' ⚠ No .gitignore found'));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Initialize git repo
|
|
265
|
+
await runGitCommand(['init'], repoDir);
|
|
266
|
+
console.log(chalk.green(' ✓ git init completed'));
|
|
267
|
+
|
|
268
|
+
// Add remote origin
|
|
269
|
+
if (githubToken) {
|
|
270
|
+
const targetUrl = githubRepoUrl.replace('https://github.com/', `https://${githubToken}@github.com/`) + '.git';
|
|
271
|
+
await runGitCommand(['remote', 'add', 'origin', targetUrl], repoDir);
|
|
272
|
+
console.log(chalk.green(' ✓ Remote origin added'));
|
|
273
|
+
console.log(chalk.dim(' Remote URL:'), githubRepoUrl + '.git');
|
|
274
|
+
} else {
|
|
275
|
+
console.log(chalk.yellow(' ⚠ No GitHub token - remote not added'));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Set main branch
|
|
279
|
+
await runGitCommand(['branch', '-M', 'main'], repoDir);
|
|
280
|
+
console.log(chalk.green(' ✓ Branch set to main'));
|
|
281
|
+
|
|
282
|
+
// Initial commit (but don't push yet - that happens on first deploy)
|
|
283
|
+
await runGitCommand(['add', '.'], repoDir);
|
|
284
|
+
await runGitCommand(['commit', '-m', 'Initial commit from Lux Studio'], repoDir);
|
|
285
|
+
console.log(chalk.green(' ✓ Initial commit created'));
|
|
286
|
+
console.log(chalk.dim(' Note: Push to GitHub will happen on first deploy'));
|
|
287
|
+
} catch (gitError) {
|
|
288
|
+
console.log(chalk.yellow(` ⚠ Git setup failed: ${gitError.message}`));
|
|
289
|
+
}
|
|
290
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
291
|
+
|
|
292
|
+
// STEP 6: Save metadata
|
|
293
|
+
console.log(chalk.blue('[STEP 6/6]'), 'Saving interface metadata...');
|
|
294
|
+
try {
|
|
295
|
+
saveInterfaceMetadata(interfaceId, {
|
|
296
|
+
id: interfaceId,
|
|
297
|
+
slug, // Keep slug for display/reference
|
|
298
|
+
name,
|
|
299
|
+
description,
|
|
300
|
+
status: 'draft', // New interfaces are drafts until first deploy
|
|
301
|
+
githubUrl: githubRepoUrl,
|
|
302
|
+
vercelProjectId: vercelProjectId || null,
|
|
303
|
+
vercelProjectUrl: vercelProjectUrl || null,
|
|
304
|
+
createdAt: new Date().toISOString(),
|
|
305
|
+
updatedAt: new Date().toISOString(),
|
|
306
|
+
});
|
|
307
|
+
console.log(chalk.green(' ✓ Metadata saved'));
|
|
308
|
+
console.log(chalk.dim(' ID:'), interfaceId);
|
|
309
|
+
console.log(chalk.dim(' Slug:'), slug);
|
|
310
|
+
if (vercelProjectId) {
|
|
311
|
+
console.log(chalk.dim(' Vercel:'), vercelProjectId);
|
|
312
|
+
}
|
|
313
|
+
} catch (metadataError) {
|
|
314
|
+
console.log(chalk.red(' ✗ Failed to save metadata:'), metadataError.message);
|
|
315
|
+
}
|
|
316
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
317
|
+
|
|
318
|
+
console.log(chalk.green('\n✅ Interface created successfully!\n'));
|
|
319
|
+
console.log(chalk.dim(' Created:'));
|
|
320
|
+
console.log(chalk.dim(' • Registered in system.interfaces'));
|
|
321
|
+
console.log(chalk.dim(' • GitHub repository created'));
|
|
322
|
+
if (vercelProjectId) {
|
|
323
|
+
console.log(chalk.dim(' • Vercel project created (linked to GitHub)'));
|
|
324
|
+
}
|
|
325
|
+
console.log(chalk.dim(' • Project files copied from templates'));
|
|
326
|
+
console.log(chalk.dim(' • Dependencies installed'));
|
|
327
|
+
console.log(chalk.dim(' • Git repository initialized'));
|
|
328
|
+
console.log(chalk.dim(' • Metadata saved locally'));
|
|
329
|
+
|
|
330
|
+
console.log(chalk.cyan('\n Next steps:'));
|
|
331
|
+
console.log(chalk.dim(' • Start the dev server'));
|
|
332
|
+
console.log(chalk.dim(' • Edit your code'));
|
|
333
|
+
console.log(chalk.dim(' • Run'), chalk.white('lux i deploy'), chalk.dim('to push to GitHub (auto-deploys to Vercel)\n'));
|
|
334
|
+
} else {
|
|
335
|
+
console.log(chalk.yellow('\n⚠ No GitHub URL returned - skipping local setup'));
|
|
336
|
+
console.log(chalk.dim('\n Created:'));
|
|
337
|
+
console.log(chalk.dim(' • Registered in system.interfaces'));
|
|
338
|
+
console.log(chalk.cyan('\n Next steps:'));
|
|
339
|
+
console.log(chalk.dim(' • Run'), chalk.white('lux i deploy'), chalk.dim('when ready\n'));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
process.exit(0);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.log(chalk.red('\n❌ Failed to initialize interface'));
|
|
345
|
+
|
|
346
|
+
// Detailed error logging
|
|
347
|
+
if (error.response) {
|
|
348
|
+
// Server responded with an error status
|
|
349
|
+
console.error(chalk.red('\n❌ API Error:'));
|
|
350
|
+
console.error(chalk.dim(' Status:'), error.response.status);
|
|
351
|
+
console.error(chalk.dim(' Message:'), error.response.data?.error || error.response.data?.message || 'Unknown error');
|
|
352
|
+
if (error.response.data?.details) {
|
|
353
|
+
console.error(chalk.dim(' Details:'), error.response.data.details);
|
|
354
|
+
}
|
|
355
|
+
if (error.response.data && typeof error.response.data === 'object') {
|
|
356
|
+
console.error(chalk.dim(' Full response:'), JSON.stringify(error.response.data, null, 2));
|
|
357
|
+
}
|
|
358
|
+
} else if (error.request) {
|
|
359
|
+
// Request was made but no response received
|
|
360
|
+
console.error(chalk.red('\n❌ Network Error:'));
|
|
361
|
+
console.error(chalk.dim(' No response received from server'));
|
|
362
|
+
console.error(chalk.dim(' URL:'), error.config?.url);
|
|
363
|
+
} else {
|
|
364
|
+
// Something else went wrong
|
|
365
|
+
console.error(chalk.red('\n❌ Error:'), error.message);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
initInterface,
|
|
374
|
+
initAndPushToGitHub,
|
|
375
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const {
|
|
5
|
+
getStudioApiUrl,
|
|
6
|
+
getAuthHeaders,
|
|
7
|
+
isAuthenticated,
|
|
8
|
+
getInterfaceRepoDir,
|
|
9
|
+
} = require('../../lib/config');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the local repo path for an interface by ID or name
|
|
13
|
+
*/
|
|
14
|
+
async function getInterfacePath(nameOrId) {
|
|
15
|
+
if (!nameOrId) {
|
|
16
|
+
console.log(chalk.red('Error: Interface name or ID is required'));
|
|
17
|
+
console.log(chalk.dim('Usage: lux app path <name-or-id>'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// First check if it's directly an ID (looks like a UUID or hex string)
|
|
22
|
+
const isLikelyId = /^[a-f0-9-]{20,}$/i.test(nameOrId);
|
|
23
|
+
|
|
24
|
+
if (isLikelyId) {
|
|
25
|
+
// Try to use it directly as an ID
|
|
26
|
+
const repoDir = getInterfaceRepoDir(nameOrId);
|
|
27
|
+
if (repoDir && fs.existsSync(repoDir)) {
|
|
28
|
+
console.log(repoDir);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Fetch interfaces from API to find by name or ID
|
|
34
|
+
if (!isAuthenticated()) {
|
|
35
|
+
console.log(chalk.red('Not authenticated. Run lux login first.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await axios.get(`${getStudioApiUrl()}/api/interfaces`, {
|
|
41
|
+
headers: getAuthHeaders(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const interfaces = response.data.interfaces || [];
|
|
45
|
+
|
|
46
|
+
// Find by ID or name (case-insensitive)
|
|
47
|
+
const match = interfaces.find(i =>
|
|
48
|
+
i.id === nameOrId ||
|
|
49
|
+
i.name.toLowerCase() === nameOrId.toLowerCase()
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!match) {
|
|
53
|
+
console.log(chalk.red(`Interface not found: ${nameOrId}`));
|
|
54
|
+
console.log(chalk.dim('Run "lux app list" to see available interfaces'));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const repoDir = getInterfaceRepoDir(match.id);
|
|
59
|
+
if (repoDir && fs.existsSync(repoDir)) {
|
|
60
|
+
console.log(repoDir);
|
|
61
|
+
} else {
|
|
62
|
+
console.log(chalk.red(`Repo directory not found for ${match.name}`));
|
|
63
|
+
console.log(chalk.dim(`Expected: ${repoDir || 'unknown'}`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.log(chalk.red('Failed to fetch interfaces:'), error.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
getInterfacePath,
|
|
74
|
+
};
|