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.
@@ -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,7 @@
1
+ // Re-export all interface modules
2
+ module.exports = {
3
+ ...require('./git-utils'),
4
+ ...require('./boilerplate'),
5
+ ...require('./init'),
6
+ ...require('./path'),
7
+ };
@@ -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
+ };