@impulselab/directory 1.0.8 → 2.7.4

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.
Files changed (4) hide show
  1. package/README.md +60 -120
  2. package/dist/index.js +1595 -0
  3. package/package.json +28 -20
  4. package/index.js +0 -666
package/index.js DELETED
@@ -1,666 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const https = require('https');
6
- const http = require('http');
7
- const os = require('os');
8
-
9
- const PACKAGE = {
10
- scopeName: '@impulselab/directory',
11
- commandName: 'impulse-directory',
12
- aliasName: 'directory',
13
- brand: 'Impulse Lab',
14
- websiteUrl: 'https://impulselab.ai',
15
- };
16
-
17
- const DEFAULT_BASE_URL = 'impulse.directory';
18
- const RAW_PATH = '/raw/';
19
- const STACK_API_PREFIX = '/api/stacks/';
20
- const STACK_API_SUFFIX = '/npx';
21
-
22
- function normalizeBaseHost(input) {
23
- const raw = (input || '').trim();
24
- if (!raw) return '';
25
- let host = raw.replace(/^\s*https?:\/\//i, '');
26
- host = host.replace(/\/+$/, '');
27
- return host;
28
- }
29
-
30
- function isLocalHost(host) {
31
- const hostnameOnly = (host || '').split(':')[0];
32
- return (
33
- hostnameOnly === 'localhost' ||
34
- hostnameOnly === '127.0.0.1' ||
35
- hostnameOnly === '::1'
36
- );
37
- }
38
-
39
- function canFallbackToHttp(host) {
40
- return process.env.ALLOW_HTTP_FALLBACK === '1' || isLocalHost(host);
41
- }
42
-
43
- const MODES = {
44
- COMMAND: 'command',
45
- STACK: 'stack',
46
- };
47
-
48
- const INSTALLATION_TARGETS = {
49
- CLAUDE_SLASH: 'claude-slash',
50
- CLAUDE_SUB_AGENT: 'claude-sub-agent',
51
- CLAUDE_SKILL: 'claude-skill',
52
- CURSOR_COMMAND: 'cursor-command',
53
- CURSOR_RULE: 'cursor-rule',
54
- };
55
-
56
- const DEFAULT_TARGET = INSTALLATION_TARGETS.CLAUDE_SLASH;
57
-
58
- const TARGET_CONFIG = {
59
- [INSTALLATION_TARGETS.CLAUDE_SLASH]: {
60
- label: 'Claude slash command',
61
- description: '.claude/commands (falls back to ~/.claude/commands)',
62
- resolveDirectory: () => resolveClaudeDirectory('commands'),
63
- extension: '.md',
64
- },
65
- [INSTALLATION_TARGETS.CLAUDE_SUB_AGENT]: {
66
- label: 'Claude sub agent',
67
- description: '.claude/agents (falls back to ~/.claude/agents)',
68
- resolveDirectory: () => resolveClaudeDirectory('agents'),
69
- extension: '.md',
70
- },
71
- [INSTALLATION_TARGETS.CLAUDE_SKILL]: {
72
- label: 'Claude Code skill',
73
- description: '.claude/skills (project only)',
74
- resolveDirectory: () => resolveClaudeSkillsDirectory(),
75
- extension: '.md',
76
- isSkill: true,
77
- },
78
- [INSTALLATION_TARGETS.CURSOR_COMMAND]: {
79
- label: 'Cursor command',
80
- description: '.cursor/commands (project only)',
81
- resolveDirectory: () => resolveCursorDirectory('commands'),
82
- extension: '.md',
83
- },
84
- [INSTALLATION_TARGETS.CURSOR_RULE]: {
85
- label: 'Cursor rule',
86
- description: '.cursor/rules (project only)',
87
- resolveDirectory: () => resolveCursorDirectory('rules'),
88
- extension: '.mdc',
89
- },
90
- };
91
-
92
- const KNOWN_TARGETS = Object.keys(TARGET_CONFIG);
93
-
94
- function showHelp() {
95
- const primaryUsageCmd = `npx impulse-directory`;
96
- console.log(`
97
- Impulse Directory Command Installer
98
- Made by ${PACKAGE.brand} (${PACKAGE.websiteUrl})
99
-
100
- Usage:
101
- ${primaryUsageCmd} <user-slug>/<command-slug> [--target <target>]
102
- ${primaryUsageCmd} stack <stack-id>
103
-
104
-
105
- Description:
106
- Downloads a command from Impulse Directory and installs it in the right tool folder.
107
- When using the stack mode, installs each prompt in the stack to its inferred target.
108
-
109
- Arguments:
110
- user-slug/command-slug The full path to the command (user/command)
111
- stack-id The stack ULID shown in the Impulse Directory UI
112
-
113
- Options:
114
- --target <target> Installation target (optional for single commands)
115
- -t <target> Short alias for --target
116
- --help, -h Show this help text
117
-
118
- Targets:
119
- ${KNOWN_TARGETS.map((key) => {
120
- const config = TARGET_CONFIG[key];
121
- const defaultLabel = key === DEFAULT_TARGET ? ' (default)' : '';
122
- return ` ${key.padEnd(18)} ${config.label}${defaultLabel}\n -> ${config.description}`;
123
- }).join('\n')}
124
-
125
- Environment Variables:
126
- IMPULSE_BASE_URL Base host for Impulse Directory (hostname[:port], no scheme, no trailing slash)
127
- Examples: impulse.directory | localhost:3000
128
-
129
- Examples:
130
- ${primaryUsageCmd} john/my-awesome-command
131
- ${primaryUsageCmd} john/my-awesome-command --target claude-sub-agent
132
- ${primaryUsageCmd} john/my-awesome-command --target cursor-command
133
- ${primaryUsageCmd} stack 01JH0000000ABCDEF --target claude-slash
134
- `);
135
- }
136
-
137
- function shouldUseHttps(baseUrl) {
138
- if (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1')) {
139
- return false;
140
- }
141
- return true;
142
- }
143
-
144
- function toKebabCase(input) {
145
- return String(input)
146
- .normalize('NFKD') // strip accents
147
- .replace(/[\u0300-\u036f]/g, '')
148
- .replace(/['"]/g, '')
149
- .replace(/[^a-zA-Z0-9]+/g, '-') // collapse non-alnum to dashes
150
- .replace(/^-+|-+$/g, '')
151
- .toLowerCase();
152
- }
153
-
154
- function stripTrailingIdSegment(slug) {
155
- const normalized = toKebabCase(slug || '');
156
- const segments = normalized.split('-');
157
- if (segments.length <= 1) return normalized;
158
- const last = segments[segments.length - 1];
159
- if (/^[a-z0-9]{5,}$/i.test(last)) {
160
- segments.pop();
161
- }
162
- return segments.join('-');
163
- }
164
-
165
- function sanitizeIdentifierForFilename(identifier) {
166
- return String(identifier || '')
167
- .normalize('NFKD')
168
- .replace(/[\u0300-\u036f]/g, '') // remove accents
169
- .replace(/[\u0000-\u001f\u007f-\u009f]/g, '') // remove control chars
170
- .replace(/[<>:"/\\|?*]/g, '') // remove filesystem unsafe chars
171
- .replace(/\.\./g, '') // remove parent directory traversal
172
- .replace(/[/\\]+/g, '-') // replace all path separators with dashes
173
- .replace(/^\.+/g, '') // remove leading dots
174
- .replace(/\.+$/g, '') // remove trailing dots
175
- .replace(/^-+|-+$/g, '') // trim edge dashes
176
- .replace(/-+/g, '-') // collapse multiple dashes
177
- .toLowerCase()
178
- || 'unknown'; // fallback for empty results
179
- }
180
-
181
- function downloadStack(baseUrl, stackId) {
182
- return new Promise((resolve, reject) => {
183
- const useHttps = shouldUseHttps(baseUrl);
184
- const protocol = useHttps ? https : http;
185
- const scheme = useHttps ? 'https' : 'http';
186
- const url = `${scheme}://${baseUrl}${STACK_API_PREFIX}${stackId}${STACK_API_SUFFIX}`;
187
-
188
- protocol
189
- .get(url, (res) => {
190
- if (res.statusCode === 200) {
191
- let data = '';
192
- res.on('data', (chunk) => {
193
- data += chunk;
194
- });
195
- res.on('end', () => {
196
- try {
197
- const parsed = JSON.parse(data);
198
- resolve(parsed);
199
- } catch (parseError) {
200
- reject(new Error('Received invalid JSON while downloading stack'));
201
- }
202
- });
203
- } else if (res.statusCode === 404) {
204
- reject(new Error(`Stack '${stackId}' not found (404)`));
205
- } else if (useHttps && (res.statusCode === 500 || res.statusCode >= 400)) {
206
- if (canFallbackToHttp(baseUrl)) {
207
- console.log(`HTTPS failed (${res.statusCode}), trying HTTP...`);
208
- downloadStackWithHttp(baseUrl, stackId)
209
- .then(resolve)
210
- .catch(reject);
211
- } else {
212
- reject(new Error(`HTTPS failed (${res.statusCode}) and HTTP fallback is disabled`));
213
- }
214
- } else {
215
- reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
216
- }
217
- })
218
- .on('error', (err) => {
219
- if (useHttps) {
220
- if (canFallbackToHttp(baseUrl)) {
221
- console.log(`HTTPS failed (${err.message}), trying HTTP...`);
222
- downloadStackWithHttp(baseUrl, stackId)
223
- .then(resolve)
224
- .catch(reject);
225
- } else {
226
- reject(err);
227
- }
228
- } else {
229
- reject(err);
230
- }
231
- });
232
- });
233
- }
234
-
235
- function downloadStackWithHttp(baseUrl, stackId) {
236
- return new Promise((resolve, reject) => {
237
- const url = `http://${baseUrl}${STACK_API_PREFIX}${stackId}${STACK_API_SUFFIX}`;
238
-
239
- console.log(`Trying HTTP: ${url}`);
240
-
241
- http
242
- .get(url, (res) => {
243
- if (res.statusCode === 200) {
244
- let data = '';
245
- res.on('data', (chunk) => {
246
- data += chunk;
247
- });
248
- res.on('end', () => {
249
- try {
250
- const parsed = JSON.parse(data);
251
- resolve(parsed);
252
- } catch (parseError) {
253
- reject(new Error('Received invalid JSON while downloading stack'));
254
- }
255
- });
256
- } else if (res.statusCode === 404) {
257
- reject(new Error(`Stack '${stackId}' not found (404)`));
258
- } else {
259
- reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
260
- }
261
- })
262
- .on('error', reject);
263
- });
264
- }
265
-
266
- function downloadRawWithHeaders(baseUrl, fullPath) {
267
- return new Promise((resolve, reject) => {
268
- const useHttps = shouldUseHttps(baseUrl);
269
- const protocol = useHttps ? https : http;
270
- const scheme = useHttps ? 'https' : 'http';
271
- const url = `${scheme}://${baseUrl}${RAW_PATH}${fullPath}`;
272
-
273
- const handleOk = (res) => {
274
- let data = '';
275
- res.on('data', (chunk) => { data += chunk; });
276
- res.on('end', () => resolve({ content: data, headers: res.headers }));
277
- };
278
-
279
- const handleHttpsFailover = (reason) => {
280
- if (useHttps && canFallbackToHttp(baseUrl)) {
281
- console.log(`HTTPS failed (${reason}), trying HTTP...`);
282
- downloadRawWithHeadersHttp(baseUrl, fullPath).then(resolve).catch(reject);
283
- } else {
284
- reject(new Error(typeof reason === 'string' ? reason : reason.message));
285
- }
286
- };
287
-
288
- protocol
289
- .get(url, (res) => {
290
- if (res.statusCode === 200) return handleOk(res);
291
- if (res.statusCode === 404) return reject(new Error(`Command '${fullPath}' not found (404)`));
292
- return handleHttpsFailover(`${res.statusCode}`);
293
- })
294
- .on('error', handleHttpsFailover);
295
- });
296
- }
297
-
298
- function downloadRawWithHeadersHttp(baseUrl, fullPath) {
299
- return new Promise((resolve, reject) => {
300
- const url = `http://${baseUrl}${RAW_PATH}${fullPath}`;
301
- console.log(`Trying HTTP: ${url}`);
302
- http
303
- .get(url, (res) => {
304
- if (res.statusCode === 200) {
305
- let data = '';
306
- res.on('data', (chunk) => { data += chunk; });
307
- res.on('end', () => resolve({ content: data, headers: res.headers }));
308
- } else if (res.statusCode === 404) {
309
- reject(new Error(`Command '${fullPath}' not found (404)`));
310
- } else {
311
- reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
312
- }
313
- })
314
- .on('error', reject);
315
- });
316
- }
317
-
318
- function findClosestFolder(folderName) {
319
- let currentDir = process.cwd();
320
-
321
- while (true) {
322
- const candidate = path.join(currentDir, folderName);
323
- if (fs.existsSync(candidate)) {
324
- return candidate;
325
- }
326
-
327
- const parentDir = path.dirname(currentDir);
328
- if (parentDir === currentDir) {
329
- break;
330
- }
331
- currentDir = parentDir;
332
- }
333
-
334
- return null;
335
- }
336
-
337
- function resolveClaudeDirectory(subFolder) {
338
- const projectClaudeDir = findClosestFolder('.claude');
339
- if (projectClaudeDir) {
340
- return path.join(projectClaudeDir, subFolder);
341
- }
342
- return path.join(os.homedir(), '.claude', subFolder);
343
- }
344
-
345
- function resolveCursorDirectory(subFolder) {
346
- const projectCursorDir = findClosestFolder('.cursor');
347
- if (!projectCursorDir) {
348
- const message =
349
- 'Unable to find a .cursor directory. Run this command from inside a Cursor project.';
350
- throw new Error(message);
351
- }
352
- return path.join(projectCursorDir, subFolder);
353
- }
354
-
355
- function resolveClaudeSkillsDirectory() {
356
- const projectClaudeDir = findClosestFolder('.claude');
357
- if (!projectClaudeDir) {
358
- const message =
359
- 'Unable to find a .claude directory. Run this command from inside a project with a .claude folder.';
360
- throw new Error(message);
361
- }
362
- return path.join(projectClaudeDir, 'skills');
363
- }
364
-
365
- function ensureDirectoryExists(dirPath) {
366
- if (!fs.existsSync(dirPath)) {
367
- fs.mkdirSync(dirPath, { recursive: true });
368
- console.log(`Created directory: ${dirPath}`);
369
- }
370
- }
371
-
372
- function checkFileExists(commandsDir, filename) {
373
- const filePath = path.join(commandsDir, filename);
374
- return fs.existsSync(filePath);
375
- }
376
-
377
- function saveCommand({
378
- directory,
379
- filename,
380
- content,
381
- fullPath,
382
- target,
383
- }) {
384
- const targetConfig = TARGET_CONFIG[target];
385
- const isSkill = targetConfig && targetConfig.isSkill;
386
-
387
- let filePath;
388
- let displayPath;
389
- let commandName;
390
-
391
- if (isSkill) {
392
- const skillName = filename.replace(path.extname(filename), '');
393
- const skillDir = path.join(directory, skillName);
394
- ensureDirectoryExists(skillDir);
395
- filePath = path.join(skillDir, 'SKILL.md');
396
- const relativeDir = path.relative(process.cwd(), skillDir);
397
- displayPath = relativeDir
398
- ? path.join(relativeDir, 'SKILL.md')
399
- : path.join(skillName, 'SKILL.md');
400
- commandName = skillName;
401
- } else {
402
- filePath = path.join(directory, filename);
403
- const relativeDir = path.relative(process.cwd(), directory);
404
- displayPath = relativeDir
405
- ? path.join(relativeDir, filename)
406
- : filename;
407
- commandName = filename.replace(path.extname(filename), '');
408
- }
409
-
410
- console.log(`📁 Saving ${isSkill ? 'skill' : 'command'} to ${displayPath}`);
411
-
412
- fs.writeFileSync(filePath, content, 'utf8');
413
-
414
- logSuccessMessage(target, fullPath, commandName);
415
- }
416
-
417
- function logSuccessMessage(target, fullPath, commandName) {
418
- switch (target) {
419
- case INSTALLATION_TARGETS.CLAUDE_SLASH:
420
- console.log(`✅ Command '${fullPath}' installed successfully`);
421
- console.log(`\n🎉 You can now use: /${commandName}`);
422
- break;
423
- case INSTALLATION_TARGETS.CLAUDE_SUB_AGENT:
424
- console.log(`✅ Agent '${fullPath}' installed successfully`);
425
- console.log(
426
- `\n🎉 You can now select '${commandName}' from Claude Desktop's agent menu.`
427
- );
428
- break;
429
- case INSTALLATION_TARGETS.CLAUDE_SKILL:
430
- console.log(`✅ Skill '${fullPath}' installed successfully`);
431
- console.log(
432
- `\n🎉 Claude Code will now automatically use the '${commandName}' skill when relevant.`
433
- );
434
- break;
435
- case INSTALLATION_TARGETS.CURSOR_COMMAND:
436
- console.log(`✅ Cursor command '${fullPath}' installed successfully`);
437
- console.log(
438
- `\n🎉 Open Cursor and run '${commandName}' from the command palette.`
439
- );
440
- break;
441
- case INSTALLATION_TARGETS.CURSOR_RULE:
442
- console.log(`✅ Cursor rule '${fullPath}' installed successfully`);
443
- console.log(
444
- `\n🎉 Cursor will apply '${commandName}' to this project.`
445
- );
446
- break;
447
- default:
448
- console.log(`✅ '${fullPath}' installed successfully`);
449
- }
450
- }
451
-
452
- function parseCommandArgs(args) {
453
- let mode = MODES.COMMAND;
454
- let identifier = null;
455
- let target = null;
456
- let showHelpFlag = false;
457
-
458
- for (let i = 0; i < args.length; i += 1) {
459
- const arg = args[i];
460
-
461
- if (arg === '--help' || arg === '-h') {
462
- showHelpFlag = true;
463
- continue;
464
- }
465
-
466
- if (arg.startsWith('--target')) {
467
- let rawValue = null;
468
- if (arg.includes('=')) {
469
- rawValue = arg.split('=')[1];
470
- } else {
471
- rawValue = args[i + 1];
472
- i += 1;
473
- }
474
-
475
- if (!rawValue) {
476
- throw new Error('Missing value for --target option');
477
- }
478
-
479
- const normalizedTarget = rawValue.toLowerCase();
480
- if (!KNOWN_TARGETS.includes(normalizedTarget)) {
481
- throw new Error(
482
- `Unknown target '${rawValue}'. Available targets: ${KNOWN_TARGETS.join(', ')}`
483
- );
484
- }
485
-
486
- target = normalizedTarget;
487
- continue;
488
- }
489
-
490
- if (arg === '-t') {
491
- const rawValue = args[i + 1];
492
- i += 1;
493
-
494
- if (!rawValue) {
495
- throw new Error('Missing value for -t option');
496
- }
497
-
498
- const normalizedTarget = rawValue.toLowerCase();
499
- if (!KNOWN_TARGETS.includes(normalizedTarget)) {
500
- throw new Error(
501
- `Unknown target '${rawValue}'. Available targets: ${KNOWN_TARGETS.join(', ')}`
502
- );
503
- }
504
-
505
- target = normalizedTarget;
506
- continue;
507
- }
508
-
509
- if (mode === MODES.COMMAND && arg === 'stack' && !identifier) {
510
- mode = MODES.STACK;
511
- continue;
512
- }
513
-
514
- if (!identifier) {
515
- identifier = arg;
516
- continue;
517
- }
518
-
519
- throw new Error(`Unknown argument: ${arg}`);
520
- }
521
-
522
- return { mode, identifier, target, showHelp: showHelpFlag };
523
- }
524
-
525
- async function main() {
526
- const args = process.argv.slice(2);
527
-
528
- try {
529
- const {
530
- mode,
531
- identifier,
532
- target,
533
- showHelp: helpRequested,
534
- } = parseCommandArgs(args);
535
-
536
- if (helpRequested || args.length === 0) {
537
- showHelp();
538
- return;
539
- }
540
-
541
- const baseUrl = normalizeBaseHost(process.env.IMPULSE_BASE_URL || DEFAULT_BASE_URL);
542
-
543
- if (mode === MODES.COMMAND) {
544
- if (!identifier) {
545
- console.error('❌ Error: Missing command path');
546
- showHelp();
547
- process.exit(1);
548
- }
549
-
550
- if (!identifier.includes('/')) {
551
- console.error('❌ Error: Path must include user/command format');
552
- showHelp();
553
- process.exit(1);
554
- }
555
-
556
- const resolvedTarget = target || DEFAULT_TARGET;
557
- const targetConfig = TARGET_CONFIG[resolvedTarget];
558
-
559
- if (!targetConfig) {
560
- console.error(`❌ Error: Unsupported target '${resolvedTarget}'`);
561
- process.exit(1);
562
- }
563
-
564
- const directory = targetConfig.resolveDirectory();
565
-
566
- ensureDirectoryExists(directory);
567
-
568
- const commandNameFromPath = identifier.split('/').pop();
569
- // First download the content to capture headers (and title)
570
- const { content, headers } = await downloadRawWithHeaders(baseUrl, identifier);
571
- const titleHeader = headers['x-prompt-title'];
572
- const title = Array.isArray(titleHeader) ? titleHeader[0] : titleHeader;
573
-
574
- const baseNameCandidate = title
575
- ? toKebabCase(title)
576
- : stripTrailingIdSegment(commandNameFromPath);
577
- const finalBaseName = sanitizeIdentifierForFilename(baseNameCandidate);
578
-
579
- // Overwrite if exists; do not append author or random suffix
580
- const filename = `${finalBaseName}${targetConfig.extension}`;
581
-
582
- saveCommand({
583
- directory,
584
- filename,
585
- content,
586
- fullPath: identifier,
587
- target: resolvedTarget,
588
- });
589
- return;
590
- }
591
-
592
- if (!identifier) {
593
- console.error('❌ Error: Missing stack identifier');
594
- showHelp();
595
- process.exit(1);
596
- }
597
-
598
- const stackPayload = await downloadStack(baseUrl, identifier);
599
- const stack = stackPayload.stack || {};
600
- const prompts = Array.isArray(stackPayload.prompts)
601
- ? stackPayload.prompts
602
- : [];
603
-
604
- console.log(
605
- `📦 Installing stack '${stack.title || identifier}' (${prompts.length} prompt${
606
- prompts.length === 1 ? '' : 's'
607
- })`
608
- );
609
-
610
- let installed = 0;
611
- let skipped = 0;
612
-
613
- for (let index = 0; index < prompts.length; index += 1) {
614
- const prompt = prompts[index];
615
- const commandPath = prompt.path || prompt.slug || prompt.id || `prompt-${index + 1}`;
616
- const baseName = prompt.title
617
- ? toKebabCase(prompt.title)
618
- : stripTrailingIdSegment(prompt.slug || `prompt-${index + 1}`);
619
-
620
- const resolvedTarget = target || prompt.target || DEFAULT_TARGET;
621
- const targetConfig = TARGET_CONFIG[resolvedTarget];
622
- if (!targetConfig) {
623
- console.warn(`⚠️ Skipping prompt '${commandPath}' (unsupported target '${resolvedTarget}')`);
624
- skipped += 1;
625
- continue;
626
- }
627
-
628
- const directory = targetConfig.resolveDirectory();
629
- ensureDirectoryExists(directory);
630
-
631
- // Sanitize and overwrite if exists (no author or random suffix)
632
- const sanitizedBaseName = sanitizeIdentifierForFilename(baseName);
633
- const filename = `${sanitizedBaseName}${targetConfig.extension}`;
634
-
635
- if (!prompt.markdown) {
636
- console.warn(`⚠️ Skipping prompt '${commandPath}' (no content available)`);
637
- skipped += 1;
638
- continue;
639
- }
640
-
641
- saveCommand({
642
- directory,
643
- filename,
644
- content: prompt.markdown,
645
- fullPath: commandPath,
646
- target: resolvedTarget,
647
- });
648
- installed += 1;
649
- }
650
-
651
- console.log(
652
- `🎉 Installed ${installed} prompt${installed === 1 ? '' : 's'} from stack '${
653
- stack.title || identifier
654
- }'${skipped ? ` (skipped ${skipped})` : ''}`
655
- );
656
- } catch (error) {
657
- console.error(`❌ Error: ${error.message}`);
658
- process.exit(1);
659
- }
660
- }
661
-
662
- if (require.main === module) {
663
- main();
664
- }
665
-
666
- module.exports = { main };