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,569 @@
1
+ const { spawn, execSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const https = require('https');
6
+ const chalk = require('chalk');
7
+ const axios = require('axios');
8
+ const chokidar = require('chokidar');
9
+ const {
10
+ loadConfig,
11
+ loadInterfaceConfig,
12
+ getDashboardUrl,
13
+ getAuthHeaders,
14
+ isAuthenticated,
15
+ LUX_STUDIO_DIR,
16
+ } = require('../lib/config');
17
+ const { info, warn, error, success } = require('../lib/helpers');
18
+ const { getNodePath, getNpmPath, getNodeEnv, isBundledNodeAvailable } = require('../lib/node-helper');
19
+
20
+ // Cloudflared binary paths
21
+ const CLOUDFLARED_DIR = path.join(LUX_STUDIO_DIR, 'bin');
22
+ const CLOUDFLARED_PATH = path.join(
23
+ CLOUDFLARED_DIR,
24
+ process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared'
25
+ );
26
+
27
+ // Download URLs for cloudflared
28
+ const CLOUDFLARED_URLS = {
29
+ darwin: {
30
+ arm64:
31
+ 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz',
32
+ x64: 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz',
33
+ },
34
+ linux: {
35
+ arm64:
36
+ 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64',
37
+ x64: 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64',
38
+ },
39
+ win32: {
40
+ x64: 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe',
41
+ },
42
+ };
43
+
44
+ // State
45
+ let devServerProcess = null;
46
+ let tunnelProcess = null;
47
+ let syncWatcher = null;
48
+ let heartbeatInterval = null;
49
+ let tunnelUrl = null;
50
+ let isShuttingDown = false;
51
+
52
+ /**
53
+ * Main dev command
54
+ */
55
+ async function dev(options = {}) {
56
+ const port = options.port || 3000;
57
+ const noTunnel = options.noTunnel || false;
58
+ const noSync = options.noSync || false;
59
+
60
+ console.log(chalk.cyan('\nšŸš€ Lux Dev Server\n'));
61
+
62
+ // Check authentication
63
+ if (!isAuthenticated()) {
64
+ console.log(
65
+ chalk.red('āŒ Not authenticated. Run'),
66
+ chalk.white('lux login'),
67
+ chalk.red('first.')
68
+ );
69
+ process.exit(1);
70
+ }
71
+
72
+ // Check if this is a Lux interface
73
+ const interfaceConfig = loadInterfaceConfig();
74
+ if (!interfaceConfig) {
75
+ console.log(
76
+ chalk.yellow('āš ļø No .lux/interface.json found.'),
77
+ chalk.dim('Run'),
78
+ chalk.white('lux init'),
79
+ chalk.dim('first, or run from an interface directory.')
80
+ );
81
+ process.exit(1);
82
+ }
83
+
84
+ // Check for package.json
85
+ if (!fs.existsSync('package.json')) {
86
+ console.log(chalk.red('āŒ No package.json found in current directory.'));
87
+ process.exit(1);
88
+ }
89
+
90
+ // Setup graceful shutdown
91
+ setupShutdownHandlers();
92
+
93
+ try {
94
+ // Step 1: Check/install dependencies
95
+ await checkDependencies();
96
+
97
+ // Step 2: Start Next.js dev server
98
+ await startDevServer(port);
99
+
100
+ // Step 3: Start tunnel (unless disabled)
101
+ if (!noTunnel) {
102
+ await startTunnel(port);
103
+
104
+ // Step 4: Register tunnel with Lux cloud
105
+ if (tunnelUrl && interfaceConfig.id) {
106
+ await registerTunnel(interfaceConfig.id, tunnelUrl);
107
+
108
+ // Step 4b: Start heartbeat to keep tunnel alive
109
+ startHeartbeat(interfaceConfig.id);
110
+ }
111
+ }
112
+
113
+ // Step 5: Start file sync (unless disabled)
114
+ if (!noSync && interfaceConfig.githubRepoUrl) {
115
+ startFileSync();
116
+ }
117
+
118
+ // Print status
119
+ printStatus(port, noTunnel, noSync, interfaceConfig);
120
+ } catch (err) {
121
+ console.error(chalk.red('\nāŒ Failed to start dev server:'), err.message);
122
+ await cleanup();
123
+ process.exit(1);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Check and install npm dependencies if needed
129
+ */
130
+ async function checkDependencies() {
131
+ const nodeModulesExists = fs.existsSync('node_modules');
132
+ const nextBinExists = fs.existsSync('./node_modules/.bin/next');
133
+
134
+ if (!nodeModulesExists || !nextBinExists) {
135
+ console.log(chalk.yellow('šŸ“¦ Installing dependencies...'));
136
+ try {
137
+ const npmPath = getNpmPath();
138
+ const nodeEnv = getNodeEnv();
139
+
140
+ // Use bundled npm if available
141
+ if (isBundledNodeAvailable()) {
142
+ execSync(`"${npmPath}" install`, { stdio: 'inherit', env: nodeEnv });
143
+ } else {
144
+ execSync('npm install', { stdio: 'inherit' });
145
+ }
146
+ console.log(chalk.green('āœ“ Dependencies installed\n'));
147
+ } catch (err) {
148
+ throw new Error('npm install failed');
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Start the Next.js dev server
155
+ */
156
+ async function startDevServer(port) {
157
+ return new Promise((resolve, reject) => {
158
+ console.log(chalk.dim(`Starting Next.js on port ${port}...`));
159
+
160
+ const nextBin = './node_modules/.bin/next';
161
+ const nodePath = getNodePath();
162
+ const nodeEnv = getNodeEnv();
163
+
164
+ // Use bundled Node.js to run Next.js dev server
165
+ devServerProcess = spawn(nodePath, [nextBin, 'dev', '-p', String(port)], {
166
+ stdio: ['pipe', 'pipe', 'pipe'],
167
+ env: { ...nodeEnv, FORCE_COLOR: '1' },
168
+ });
169
+
170
+ let started = false;
171
+
172
+ devServerProcess.stdout.on('data', (data) => {
173
+ const output = data.toString();
174
+ process.stdout.write(chalk.dim(output));
175
+
176
+ // Detect when server is ready
177
+ if (!started && (output.includes('Ready') || output.includes('started'))) {
178
+ started = true;
179
+ console.log(chalk.green(`\nāœ“ Dev server running on http://localhost:${port}\n`));
180
+ resolve();
181
+ }
182
+ });
183
+
184
+ devServerProcess.stderr.on('data', (data) => {
185
+ const output = data.toString();
186
+ // Filter out noisy warnings
187
+ if (!output.includes('ExperimentalWarning')) {
188
+ process.stderr.write(chalk.yellow(output));
189
+ }
190
+ });
191
+
192
+ devServerProcess.on('error', (err) => {
193
+ reject(new Error(`Failed to start dev server: ${err.message}`));
194
+ });
195
+
196
+ devServerProcess.on('exit', (code) => {
197
+ if (!isShuttingDown && code !== 0) {
198
+ console.error(chalk.red(`\nDev server exited with code ${code}`));
199
+ }
200
+ });
201
+
202
+ // Timeout for server start
203
+ setTimeout(() => {
204
+ if (!started) {
205
+ started = true;
206
+ console.log(chalk.yellow('\nāš ļø Server may still be starting...\n'));
207
+ resolve();
208
+ }
209
+ }, 30000);
210
+ });
211
+ }
212
+
213
+ /**
214
+ * Download cloudflared if not present
215
+ */
216
+ async function ensureCloudflared() {
217
+ if (fs.existsSync(CLOUDFLARED_PATH)) {
218
+ return;
219
+ }
220
+
221
+ console.log(chalk.dim('Downloading cloudflared tunnel client...'));
222
+
223
+ // Create bin directory
224
+ if (!fs.existsSync(CLOUDFLARED_DIR)) {
225
+ fs.mkdirSync(CLOUDFLARED_DIR, { recursive: true });
226
+ }
227
+
228
+ const platform = process.platform;
229
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
230
+
231
+ const urls = CLOUDFLARED_URLS[platform];
232
+ if (!urls || !urls[arch]) {
233
+ throw new Error(
234
+ `Unsupported platform: ${platform}/${arch}. Please install cloudflared manually.`
235
+ );
236
+ }
237
+
238
+ const url = urls[arch];
239
+
240
+ return new Promise((resolve, reject) => {
241
+ const downloadFile = (downloadUrl, destPath, callback) => {
242
+ const file = fs.createWriteStream(destPath);
243
+
244
+ https
245
+ .get(downloadUrl, (response) => {
246
+ // Handle redirects
247
+ if (response.statusCode === 302 || response.statusCode === 301) {
248
+ downloadFile(response.headers.location, destPath, callback);
249
+ return;
250
+ }
251
+
252
+ response.pipe(file);
253
+ file.on('finish', () => {
254
+ file.close(callback);
255
+ });
256
+ })
257
+ .on('error', (err) => {
258
+ fs.unlink(destPath, () => {});
259
+ reject(err);
260
+ });
261
+ };
262
+
263
+ const isTarball = url.endsWith('.tgz');
264
+ const downloadPath = isTarball
265
+ ? path.join(CLOUDFLARED_DIR, 'cloudflared.tgz')
266
+ : CLOUDFLARED_PATH;
267
+
268
+ downloadFile(url, downloadPath, async () => {
269
+ try {
270
+ if (isTarball) {
271
+ // Extract tarball (macOS)
272
+ execSync(`tar -xzf "${downloadPath}" -C "${CLOUDFLARED_DIR}"`, {
273
+ stdio: 'ignore',
274
+ });
275
+ fs.unlinkSync(downloadPath);
276
+ }
277
+
278
+ // Make executable
279
+ if (platform !== 'win32') {
280
+ fs.chmodSync(CLOUDFLARED_PATH, '755');
281
+ }
282
+
283
+ console.log(chalk.green('āœ“ cloudflared installed\n'));
284
+ resolve();
285
+ } catch (err) {
286
+ reject(new Error(`Failed to setup cloudflared: ${err.message}`));
287
+ }
288
+ });
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Start cloudflare tunnel
294
+ */
295
+ async function startTunnel(port) {
296
+ await ensureCloudflared();
297
+
298
+ return new Promise((resolve, reject) => {
299
+ console.log(chalk.dim('Starting tunnel...'));
300
+
301
+ tunnelProcess = spawn(CLOUDFLARED_PATH, [
302
+ 'tunnel',
303
+ '--url',
304
+ `http://localhost:${port}`,
305
+ '--no-autoupdate',
306
+ ]);
307
+
308
+ let resolved = false;
309
+
310
+ tunnelProcess.stderr.on('data', (data) => {
311
+ const output = data.toString();
312
+
313
+ // Extract tunnel URL from output
314
+ const urlMatch = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
315
+ if (urlMatch && !resolved) {
316
+ tunnelUrl = urlMatch[0];
317
+ resolved = true;
318
+ console.log(chalk.green(`āœ“ Tunnel active: ${chalk.cyan(tunnelUrl)}\n`));
319
+ resolve();
320
+ }
321
+
322
+ // Debug output (only show errors)
323
+ if (output.includes('ERR') || output.includes('error')) {
324
+ console.error(chalk.red(output));
325
+ }
326
+ });
327
+
328
+ tunnelProcess.on('error', (err) => {
329
+ reject(new Error(`Failed to start tunnel: ${err.message}`));
330
+ });
331
+
332
+ tunnelProcess.on('exit', (code) => {
333
+ if (!isShuttingDown && code !== 0) {
334
+ console.error(chalk.red(`\nTunnel exited with code ${code}`));
335
+ }
336
+ });
337
+
338
+ // Timeout
339
+ setTimeout(() => {
340
+ if (!resolved) {
341
+ resolved = true;
342
+ console.log(chalk.yellow('\nāš ļø Tunnel may still be connecting...\n'));
343
+ resolve();
344
+ }
345
+ }, 30000);
346
+ });
347
+ }
348
+
349
+ /**
350
+ * Register tunnel URL with Lux cloud (dashboard)
351
+ */
352
+ async function registerTunnel(interfaceId, url) {
353
+ try {
354
+ const dashboardUrl = getDashboardUrl();
355
+ const headers = getAuthHeaders();
356
+
357
+ await axios.post(
358
+ `${dashboardUrl}/api/tunnel/register`,
359
+ {
360
+ interface_id: interfaceId,
361
+ tunnel_url: url,
362
+ },
363
+ { headers }
364
+ );
365
+
366
+ console.log(chalk.green('āœ“ Tunnel registered with Lux cloud\n'));
367
+ } catch (err) {
368
+ console.log(
369
+ chalk.yellow('āš ļø Could not register tunnel with cloud:'),
370
+ err.response?.data?.error || err.message
371
+ );
372
+ console.log(chalk.dim('Preview in dashboard may not work.\n'));
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Unregister tunnel when shutting down
378
+ */
379
+ async function unregisterTunnel(interfaceId) {
380
+ try {
381
+ const dashboardUrl = getDashboardUrl();
382
+ const headers = getAuthHeaders();
383
+
384
+ await axios.post(
385
+ `${dashboardUrl}/api/tunnel/unregister`,
386
+ { interface_id: interfaceId },
387
+ { headers, timeout: 3000 }
388
+ );
389
+ } catch (err) {
390
+ // Ignore errors during shutdown
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Start heartbeat to keep tunnel alive
396
+ */
397
+ function startHeartbeat(interfaceId) {
398
+ const dashboardUrl = getDashboardUrl();
399
+ const headers = getAuthHeaders();
400
+
401
+ // Send heartbeat every 30 seconds
402
+ heartbeatInterval = setInterval(async () => {
403
+ try {
404
+ await axios.post(
405
+ `${dashboardUrl}/api/tunnel/heartbeat`,
406
+ { interface_id: interfaceId },
407
+ { headers, timeout: 5000 }
408
+ );
409
+ } catch (err) {
410
+ // Silently ignore heartbeat failures
411
+ // The tunnel will be marked stale after 2 minutes without heartbeat
412
+ }
413
+ }, 30000);
414
+
415
+ // Send initial heartbeat
416
+ axios.post(
417
+ `${dashboardUrl}/api/tunnel/heartbeat`,
418
+ { interface_id: interfaceId },
419
+ { headers, timeout: 5000 }
420
+ ).catch(() => {});
421
+ }
422
+
423
+ /**
424
+ * Start file watcher for GitHub sync
425
+ */
426
+ function startFileSync() {
427
+ console.log(chalk.dim('Starting file sync...\n'));
428
+
429
+ // Debounce sync operations
430
+ let syncTimeout = null;
431
+ const pendingChanges = new Set();
432
+
433
+ const doSync = () => {
434
+ if (pendingChanges.size === 0) return;
435
+
436
+ const changes = Array.from(pendingChanges);
437
+ pendingChanges.clear();
438
+
439
+ console.log(chalk.dim(`Syncing ${changes.length} file(s) to GitHub...`));
440
+
441
+ try {
442
+ // Add all changes
443
+ execSync('git add -A', { stdio: 'ignore' });
444
+
445
+ // Check if there are changes to commit
446
+ const status = execSync('git status --porcelain').toString();
447
+ if (status.trim()) {
448
+ // Commit with auto message
449
+ const message = `Auto-sync: ${changes.slice(0, 3).join(', ')}${changes.length > 3 ? ` +${changes.length - 3} more` : ''}`;
450
+ execSync(`git commit -m "${message}"`, { stdio: 'ignore' });
451
+
452
+ // Push to dev branch
453
+ execSync('git push origin dev', { stdio: 'ignore' });
454
+
455
+ console.log(chalk.green('āœ“ Synced to GitHub\n'));
456
+ }
457
+ } catch (err) {
458
+ console.log(chalk.yellow('āš ļø Sync failed:'), err.message);
459
+ }
460
+ };
461
+
462
+ // Watch for file changes
463
+ syncWatcher = chokidar.watch('.', {
464
+ ignored: [
465
+ /node_modules/,
466
+ /\.next/,
467
+ /\.git/,
468
+ /\.lux/,
469
+ /\.DS_Store/,
470
+ ],
471
+ ignoreInitial: true,
472
+ persistent: true,
473
+ });
474
+
475
+ syncWatcher.on('all', (event, filePath) => {
476
+ pendingChanges.add(filePath);
477
+
478
+ // Debounce: sync after 5 seconds of no changes
479
+ clearTimeout(syncTimeout);
480
+ syncTimeout = setTimeout(doSync, 5000);
481
+ });
482
+
483
+ console.log(chalk.green('āœ“ File sync active (auto-commits to GitHub)\n'));
484
+ }
485
+
486
+ /**
487
+ * Print final status
488
+ */
489
+ function printStatus(port, noTunnel, noSync, interfaceConfig) {
490
+ console.log(chalk.cyan('─'.repeat(50)));
491
+ console.log(chalk.cyan.bold('\nšŸ“ Dev Server Status\n'));
492
+
493
+ console.log(chalk.white(' Local:'), `http://localhost:${port}`);
494
+
495
+ if (!noTunnel && tunnelUrl) {
496
+ console.log(chalk.white(' Tunnel:'), chalk.cyan(tunnelUrl));
497
+ console.log(chalk.white(' Preview:'), chalk.dim('Available in Lux dashboard'));
498
+ }
499
+
500
+ if (!noSync) {
501
+ console.log(chalk.white(' Sync:'), chalk.green('Active'), chalk.dim('(auto-push to GitHub)'));
502
+ }
503
+
504
+ console.log(chalk.white(' Interface:'), interfaceConfig.name || interfaceConfig.id);
505
+
506
+ console.log(chalk.cyan('\n─'.repeat(50)));
507
+ console.log(chalk.dim('\nPress Ctrl+C to stop\n'));
508
+ }
509
+
510
+ /**
511
+ * Setup graceful shutdown handlers
512
+ */
513
+ function setupShutdownHandlers() {
514
+ const shutdown = async (signal) => {
515
+ if (isShuttingDown) return;
516
+ isShuttingDown = true;
517
+
518
+ console.log(chalk.yellow(`\n\nReceived ${signal}, shutting down...\n`));
519
+
520
+ await cleanup();
521
+ process.exit(0);
522
+ };
523
+
524
+ process.on('SIGINT', () => shutdown('SIGINT'));
525
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
526
+ process.on('SIGHUP', () => shutdown('SIGHUP'));
527
+ }
528
+
529
+ /**
530
+ * Cleanup all processes
531
+ */
532
+ async function cleanup() {
533
+ const interfaceConfig = loadInterfaceConfig();
534
+
535
+ // Stop heartbeat
536
+ if (heartbeatInterval) {
537
+ clearInterval(heartbeatInterval);
538
+ console.log(chalk.dim('Stopped heartbeat'));
539
+ }
540
+
541
+ // Unregister tunnel
542
+ if (interfaceConfig?.id) {
543
+ await unregisterTunnel(interfaceConfig.id);
544
+ }
545
+
546
+ // Stop file watcher
547
+ if (syncWatcher) {
548
+ await syncWatcher.close();
549
+ console.log(chalk.dim('Stopped file sync'));
550
+ }
551
+
552
+ // Stop tunnel
553
+ if (tunnelProcess) {
554
+ tunnelProcess.kill();
555
+ console.log(chalk.dim('Stopped tunnel'));
556
+ }
557
+
558
+ // Stop dev server
559
+ if (devServerProcess) {
560
+ devServerProcess.kill();
561
+ console.log(chalk.dim('Stopped dev server'));
562
+ }
563
+
564
+ console.log(chalk.green('\nāœ“ Shutdown complete\n'));
565
+ }
566
+
567
+ module.exports = {
568
+ dev,
569
+ };
@@ -0,0 +1,126 @@
1
+ const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+ const fs = require('fs');
4
+ const { saveInterfaceConfig, loadInterfaceConfig } = require('../lib/config');
5
+
6
+ async function init(options) {
7
+ console.log(chalk.cyan('\nšŸ“¦ Initialize Lux Interface\n'));
8
+
9
+ // Check if already initialized
10
+ const existing = loadInterfaceConfig();
11
+ if (existing) {
12
+ console.log(
13
+ chalk.yellow(
14
+ 'āš ļø This directory is already initialized as a Lux interface.'
15
+ )
16
+ );
17
+ console.log(chalk.dim(`Interface: ${existing.name} (${existing.id})\n`));
18
+
19
+ const { overwrite } = await inquirer.prompt([
20
+ {
21
+ type: 'confirm',
22
+ name: 'overwrite',
23
+ message: 'Do you want to reinitialize?',
24
+ default: false,
25
+ },
26
+ ]);
27
+
28
+ if (!overwrite) {
29
+ console.log(chalk.dim('Cancelled.'));
30
+ return;
31
+ }
32
+ }
33
+
34
+ // Get interface details
35
+ let answers;
36
+
37
+ // If name is provided via flags, skip interactive prompts
38
+ if (options.name) {
39
+ answers = {
40
+ name: options.name,
41
+ description: options.description || '',
42
+ };
43
+ console.log(chalk.dim(`Name: ${answers.name}`));
44
+ if (answers.description) {
45
+ console.log(chalk.dim(`Description: ${answers.description}`));
46
+ }
47
+ } else {
48
+ // Interactive prompts
49
+ answers = await inquirer.prompt([
50
+ {
51
+ type: 'input',
52
+ name: 'name',
53
+ message: 'Interface name:',
54
+ default: options.name || require('path').basename(process.cwd()),
55
+ validate: (input) => {
56
+ if (!input || input.trim().length === 0) {
57
+ return 'Interface name is required';
58
+ }
59
+ return true;
60
+ },
61
+ },
62
+ {
63
+ type: 'input',
64
+ name: 'description',
65
+ message: 'Description (optional):',
66
+ default: options.description || '',
67
+ },
68
+ ]);
69
+ }
70
+
71
+ // Create interface config
72
+ const config = {
73
+ name: answers.name,
74
+ description: answers.description,
75
+ createdAt: new Date().toISOString(),
76
+ };
77
+
78
+ // Save config
79
+ saveInterfaceConfig(config);
80
+
81
+ console.log(chalk.green('\nāœ“ Interface initialized successfully!\n'));
82
+ console.log(chalk.dim('Configuration saved to .lux/interface.json\n'));
83
+ console.log(chalk.cyan('Next steps:'));
84
+ console.log(
85
+ chalk.dim(' 1. Run'),
86
+ chalk.white('lux up'),
87
+ chalk.dim('to upload your code')
88
+ );
89
+ console.log(
90
+ chalk.dim(' 2. Run'),
91
+ chalk.white('lux deploy'),
92
+ chalk.dim('to deploy to production\n')
93
+ );
94
+
95
+ // Create .gitignore entry if needed
96
+ addToGitignore();
97
+ }
98
+
99
+ function addToGitignore() {
100
+ const gitignorePath = '.gitignore';
101
+ const entry = '.lux/interface.json';
102
+
103
+ if (fs.existsSync(gitignorePath)) {
104
+ const content = fs.readFileSync(gitignorePath, 'utf8');
105
+
106
+ if (!content.includes(entry)) {
107
+ const { confirm } = require('inquirer').prompt([
108
+ {
109
+ type: 'confirm',
110
+ name: 'confirm',
111
+ message: 'Add .lux/interface.json to .gitignore?',
112
+ default: true,
113
+ },
114
+ ]);
115
+
116
+ if (confirm) {
117
+ fs.appendFileSync(gitignorePath, `\n# Lux CLI\n${entry}\n`);
118
+ console.log(chalk.dim('āœ“ Added to .gitignore'));
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ module.exports = {
125
+ init,
126
+ };