chatporter 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/src/index.js ADDED
@@ -0,0 +1,929 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import inquirer from 'inquirer';
6
+ import { readFile, stat, readdir } from 'fs/promises';
7
+ import { join, resolve, extname, basename, relative, dirname as pathDirname } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname } from 'path';
10
+ import open from 'open';
11
+ import dotenv from 'dotenv';
12
+ import { existsSync, createReadStream } from 'fs';
13
+ import archiver from 'archiver';
14
+ import { glob } from 'glob';
15
+
16
+ // Load environment variables
17
+ dotenv.config();
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ const program = new Command();
23
+
24
+ // Platform formatters
25
+ const platformFormatters = {
26
+ v0: (content, metadata) => formatForV0(content, metadata),
27
+ chatgpt: (content, metadata) => formatForChatGPT(content, metadata),
28
+ claude: (content, metadata) => formatForClaude(content, metadata),
29
+ cursor: (content, metadata) => formatForCursor(content, metadata),
30
+ raw: (content, metadata) => formatRaw(content, metadata),
31
+ };
32
+
33
+ // Format content for v0.dev
34
+ function formatForV0(content, metadata) {
35
+ let formatted = `# Context Upload\n\n`;
36
+
37
+ if (metadata.files.length > 1) {
38
+ formatted += `## Files Included\n\n`;
39
+ metadata.files.forEach((file, idx) => {
40
+ formatted += `${idx + 1}. \`${file.name}\`\n`;
41
+ });
42
+ formatted += `\n---\n\n`;
43
+ }
44
+
45
+ formatted += content;
46
+
47
+ return formatted;
48
+ }
49
+
50
+ // Format content for ChatGPT
51
+ function formatForChatGPT(content, metadata) {
52
+ let formatted = `I'm sharing ${metadata.files.length} document${metadata.files.length > 1 ? 's' : ''} for context:\n\n`;
53
+
54
+ metadata.files.forEach((file, idx) => {
55
+ formatted += `## Document ${idx + 1}: ${file.name}\n\n`;
56
+ });
57
+
58
+ formatted += `\n---\n\n${content}`;
59
+
60
+ return formatted;
61
+ }
62
+
63
+ // Format content for Claude
64
+ function formatForClaude(content, metadata) {
65
+ let formatted = `<documents>\n`;
66
+
67
+ metadata.files.forEach((file) => {
68
+ formatted += `<document name="${file.name}">\n`;
69
+ });
70
+
71
+ formatted += `</documents>\n\n${content}`;
72
+
73
+ return formatted;
74
+ }
75
+
76
+ // Format content for Cursor
77
+ function formatForCursor(content, metadata) {
78
+ let formatted = `// Context from ${metadata.files.length} file${metadata.files.length > 1 ? 's' : ''}\n\n`;
79
+ formatted += content;
80
+ return formatted;
81
+ }
82
+
83
+ // Raw format (just the content)
84
+ function formatRaw(content, metadata) {
85
+ return content;
86
+ }
87
+
88
+ // Read and combine markdown files
89
+ async function readMarkdownFiles(filePaths) {
90
+ const files = [];
91
+ const contents = [];
92
+
93
+ for (const filePath of filePaths) {
94
+ try {
95
+ const fullPath = resolve(filePath);
96
+ const stats = await stat(fullPath);
97
+
98
+ if (!stats.isFile()) {
99
+ console.warn(chalk.yellow(`Skipping ${filePath}: not a file`));
100
+ continue;
101
+ }
102
+
103
+ if (extname(filePath).toLowerCase() !== '.md') {
104
+ console.warn(chalk.yellow(`Skipping ${filePath}: not a markdown file`));
105
+ continue;
106
+ }
107
+
108
+ const content = await readFile(fullPath, 'utf-8');
109
+ files.push({
110
+ name: basename(filePath),
111
+ path: filePath,
112
+ size: stats.size,
113
+ });
114
+
115
+ contents.push({
116
+ name: basename(filePath),
117
+ content: content,
118
+ });
119
+ } catch (error) {
120
+ console.error(chalk.red(`Error reading ${filePath}: ${error.message}`));
121
+ }
122
+ }
123
+
124
+ return { files, contents };
125
+ }
126
+
127
+ // Combine multiple markdown files into one formatted string
128
+ function combineMarkdownFiles(fileContents) {
129
+ if (fileContents.length === 1) {
130
+ return fileContents[0].content;
131
+ }
132
+
133
+ let combined = '';
134
+
135
+ fileContents.forEach((file, index) => {
136
+ if (index > 0) {
137
+ combined += '\n\n---\n\n';
138
+ }
139
+ combined += `# ${file.name}\n\n${file.content}`;
140
+ });
141
+
142
+ return combined;
143
+ }
144
+
145
+ // Check if a path is a GitHub URL
146
+ function isGitHubUrl(url) {
147
+ return /^https?:\/\/(www\.)?github\.com\/[\w\-\.]+\/[\w\-\.]+/.test(url);
148
+ }
149
+
150
+ // Check if a path is a directory
151
+ async function isDirectory(path) {
152
+ try {
153
+ const stats = await stat(path);
154
+ return stats.isDirectory();
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ // Walk directory and collect all files (excluding node_modules, .git, etc.)
161
+ async function walkDirectory(dirPath, basePath = dirPath) {
162
+ const files = [];
163
+ const ignorePatterns = [
164
+ 'node_modules',
165
+ '.git',
166
+ '.next',
167
+ '.vercel',
168
+ 'dist',
169
+ 'build',
170
+ '.DS_Store',
171
+ '*.log',
172
+ '.env',
173
+ '.env.local',
174
+ 'coverage',
175
+ '.nyc_output',
176
+ ];
177
+
178
+ try {
179
+ const entries = await readdir(dirPath, { withFileTypes: true });
180
+
181
+ for (const entry of entries) {
182
+ const fullPath = join(dirPath, entry.name);
183
+ const relativePath = relative(basePath, fullPath);
184
+
185
+ // Skip ignored patterns
186
+ if (ignorePatterns.some(pattern => {
187
+ if (pattern.includes('*')) {
188
+ return entry.name.match(new RegExp(pattern.replace('*', '.*')));
189
+ }
190
+ return entry.name === pattern || relativePath.includes(pattern);
191
+ })) {
192
+ continue;
193
+ }
194
+
195
+ if (entry.isDirectory()) {
196
+ // Recursively walk subdirectories
197
+ const subFiles = await walkDirectory(fullPath, basePath);
198
+ files.push(...subFiles);
199
+ } else if (entry.isFile()) {
200
+ try {
201
+ const content = await readFile(fullPath, 'utf-8');
202
+ files.push({
203
+ name: relativePath,
204
+ content: content,
205
+ path: fullPath,
206
+ });
207
+ } catch (error) {
208
+ // Skip binary files or files that can't be read
209
+ console.warn(chalk.yellow(` Skipping ${relativePath}: ${error.message}`));
210
+ }
211
+ }
212
+ }
213
+ } catch (error) {
214
+ console.error(chalk.red(`Error reading directory ${dirPath}: ${error.message}`));
215
+ }
216
+
217
+ return files;
218
+ }
219
+
220
+ // Create v0 chat from GitHub repository
221
+ async function createV0ChatFromRepo(repoUrl, options) {
222
+ const apiKey = process.env.V0_API_KEY || options.apiKey;
223
+
224
+ if (!apiKey) {
225
+ throw new Error(
226
+ 'V0_API_KEY not found. Set it in your environment or use --api-key option.\n' +
227
+ 'Get your API key from: https://v0.app/settings/api'
228
+ );
229
+ }
230
+
231
+ // Try using v0-sdk first
232
+ try {
233
+ const { v0, createClient } = await import('v0-sdk');
234
+
235
+ console.log(chalk.blue(`🚀 Creating v0 chat from repository: ${repoUrl}\n`));
236
+
237
+ // Create client with API key (SDK will use env var if not provided)
238
+ const v0Client = createClient({ apiKey });
239
+
240
+ const chat = await v0Client.chats.init({
241
+ type: 'repo',
242
+ repo: {
243
+ url: repoUrl,
244
+ branch: options.branch || undefined,
245
+ },
246
+ name: options.name || `ChatPorter: ${basename(repoUrl)}`,
247
+ projectId: options.projectId || undefined,
248
+ lockAllFiles: options.lockAllFiles || false,
249
+ });
250
+
251
+ console.log(chalk.green(`✓ Chat created successfully!`));
252
+ console.log(chalk.cyan(` Chat ID: ${chat.id}`));
253
+ console.log(chalk.cyan(` Chat URL: https://v0.dev/chat/${chat.id}`));
254
+
255
+ if (options.open !== false) {
256
+ await open(`https://v0.dev/chat/${chat.id}`);
257
+ console.log(chalk.green(`\n🌐 Opened chat in browser`));
258
+ }
259
+
260
+ return chat;
261
+ } catch (sdkError) {
262
+ // Log SDK error for debugging
263
+ if (sdkError.code !== 'ERR_MODULE_NOT_FOUND') {
264
+ console.log(chalk.yellow(`⚠ SDK error: ${sdkError.message}`));
265
+ console.log(chalk.gray(' Falling back to direct API calls...\n'));
266
+ }
267
+ return await createV0ChatFromRepoDirect(repoUrl, { ...options, apiKey });
268
+ }
269
+ }
270
+
271
+ // Direct HTTP implementation for repository import
272
+ async function createV0ChatFromRepoDirect(repoUrl, options) {
273
+ const apiKey = options.apiKey;
274
+ // Use correct v0 API endpoint (from SDK source: https://api.v0.dev/v1)
275
+ const apiUrl = process.env.V0_API_URL || 'https://api.v0.dev/v1/chats/init';
276
+
277
+ console.log(chalk.blue(`🚀 Creating v0 chat from repository: ${repoUrl}\n`));
278
+ console.log(chalk.gray(` Using direct API calls (SDK fallback)\n`));
279
+
280
+ const payload = {
281
+ type: 'repo',
282
+ repo: {
283
+ url: repoUrl,
284
+ },
285
+ name: options.name || `ChatPorter: ${basename(repoUrl)}`,
286
+ lockAllFiles: options.lockAllFiles || false,
287
+ };
288
+
289
+ if (options.branch) {
290
+ payload.repo.branch = options.branch;
291
+ }
292
+
293
+ if (options.projectId) {
294
+ payload.projectId = options.projectId;
295
+ }
296
+
297
+ try {
298
+ const response = await fetch(apiUrl, {
299
+ method: 'POST',
300
+ headers: {
301
+ 'Authorization': `Bearer ${apiKey}`,
302
+ 'Content-Type': 'application/json',
303
+ },
304
+ body: JSON.stringify(payload),
305
+ });
306
+
307
+ if (!response.ok) {
308
+ const errorText = await response.text();
309
+ let errorData;
310
+ try {
311
+ errorData = JSON.parse(errorText);
312
+ } catch {
313
+ errorData = { message: errorText || response.statusText };
314
+ }
315
+ throw new Error(`API Error: ${response.status} - ${errorData.message || response.statusText}`);
316
+ }
317
+
318
+ const chat = await response.json();
319
+
320
+ console.log(chalk.green(`✓ Chat created successfully!`));
321
+ console.log(chalk.cyan(` Chat ID: ${chat.id}`));
322
+ console.log(chalk.cyan(` Chat URL: https://v0.dev/chat/${chat.id}`));
323
+
324
+ if (options.open !== false) {
325
+ await open(`https://v0.dev/chat/${chat.id}`);
326
+ console.log(chalk.green(`\n🌐 Opened chat in browser`));
327
+ }
328
+
329
+ return chat;
330
+ } catch (error) {
331
+ console.error(chalk.red(`\n✗ Error creating v0 chat: ${error.message}`));
332
+ console.error(chalk.yellow(`\n💡 Tips:`));
333
+ console.error(chalk.yellow(` 1. Verify your API key is correct: https://v0.app/settings/api`));
334
+ console.error(chalk.yellow(` 2. Check that the repository URL is accessible`));
335
+ console.error(chalk.yellow(` 3. API endpoint: ${apiUrl}`));
336
+ throw error;
337
+ }
338
+ }
339
+
340
+ // Create v0 chat from local directory
341
+ async function createV0ChatFromDirectory(dirPath, options) {
342
+ console.log(chalk.blue(`📁 Reading directory: ${dirPath}\n`));
343
+
344
+ const files = await walkDirectory(resolve(dirPath));
345
+
346
+ if (files.length === 0) {
347
+ throw new Error('No files found in directory');
348
+ }
349
+
350
+ console.log(chalk.green(`✓ Found ${files.length} file(s) in directory`));
351
+
352
+ // Convert to fileContents format
353
+ const fileContents = files.map(file => ({
354
+ name: file.name,
355
+ content: file.content,
356
+ }));
357
+
358
+ // Use the existing file upload function
359
+ return await createV0Chat(fileContents, {
360
+ ...options,
361
+ name: options.name || `ChatPorter: ${basename(dirPath)}`,
362
+ });
363
+ }
364
+
365
+ // Create v0 chat from zip URL
366
+ async function createV0ChatFromZip(zipUrl, options) {
367
+ const apiKey = process.env.V0_API_KEY || options.apiKey;
368
+
369
+ if (!apiKey) {
370
+ throw new Error(
371
+ 'V0_API_KEY not found. Set it in your environment or use --api-key option.\n' +
372
+ 'Get your API key from: https://v0.app/settings/api'
373
+ );
374
+ }
375
+
376
+ // Try using v0-sdk first
377
+ try {
378
+ const { createClient } = await import('v0-sdk');
379
+
380
+ console.log(chalk.blue(`🚀 Creating v0 chat from zip archive: ${zipUrl}\n`));
381
+
382
+ // Create client with API key
383
+ const v0Client = createClient({ apiKey });
384
+
385
+ const chat = await v0Client.chats.init({
386
+ type: 'zip',
387
+ zip: {
388
+ url: zipUrl,
389
+ },
390
+ name: options.name || `ChatPorter: Zip Archive`,
391
+ projectId: options.projectId || undefined,
392
+ lockAllFiles: options.lockAllFiles || false,
393
+ });
394
+
395
+ console.log(chalk.green(`✓ Chat created successfully!`));
396
+ console.log(chalk.cyan(` Chat ID: ${chat.id}`));
397
+ console.log(chalk.cyan(` Chat URL: https://v0.dev/chat/${chat.id}`));
398
+
399
+ if (options.open !== false) {
400
+ await open(`https://v0.dev/chat/${chat.id}`);
401
+ console.log(chalk.green(`\n🌐 Opened chat in browser`));
402
+ }
403
+
404
+ return chat;
405
+ } catch (sdkError) {
406
+ return await createV0ChatFromZipDirect(zipUrl, { ...options, apiKey });
407
+ }
408
+ }
409
+
410
+ // Direct HTTP implementation for zip import
411
+ async function createV0ChatFromZipDirect(zipUrl, options) {
412
+ const apiKey = options.apiKey;
413
+ const apiUrl = process.env.V0_API_URL || 'https://api.v0.dev/v1/chats/init';
414
+
415
+ console.log(chalk.blue(`🚀 Creating v0 chat from zip archive: ${zipUrl}\n`));
416
+
417
+ const payload = {
418
+ type: 'zip',
419
+ zip: {
420
+ url: zipUrl,
421
+ },
422
+ name: options.name || `ChatPorter: Zip Archive`,
423
+ lockAllFiles: options.lockAllFiles || false,
424
+ };
425
+
426
+ if (options.projectId) {
427
+ payload.projectId = options.projectId;
428
+ }
429
+
430
+ try {
431
+ const response = await fetch(apiUrl, {
432
+ method: 'POST',
433
+ headers: {
434
+ 'Authorization': `Bearer ${apiKey}`,
435
+ 'Content-Type': 'application/json',
436
+ },
437
+ body: JSON.stringify(payload),
438
+ });
439
+
440
+ if (!response.ok) {
441
+ const errorText = await response.text();
442
+ let errorData;
443
+ try {
444
+ errorData = JSON.parse(errorText);
445
+ } catch {
446
+ errorData = { message: errorText || response.statusText };
447
+ }
448
+ throw new Error(`API Error: ${response.status} - ${errorData.message || response.statusText}`);
449
+ }
450
+
451
+ const chat = await response.json();
452
+
453
+ console.log(chalk.green(`✓ Chat created successfully!`));
454
+ console.log(chalk.cyan(` Chat ID: ${chat.id}`));
455
+ console.log(chalk.cyan(` Chat URL: https://v0.dev/chat/${chat.id}`));
456
+
457
+ if (options.open !== false) {
458
+ await open(`https://v0.dev/chat/${chat.id}`);
459
+ console.log(chalk.green(`\n🌐 Opened chat in browser`));
460
+ }
461
+
462
+ return chat;
463
+ } catch (error) {
464
+ console.error(chalk.red(`\n✗ Error creating v0 chat: ${error.message}`));
465
+ throw error;
466
+ }
467
+ }
468
+
469
+ // Create v0 chat via API
470
+ // Uses direct HTTP calls based on v0 Platform API documentation
471
+ // Reference: https://v0.app/docs/api/platform/guides/start-from-existing-code
472
+ async function createV0Chat(fileContents, options) {
473
+ const apiKey = process.env.V0_API_KEY || options.apiKey;
474
+
475
+ if (!apiKey) {
476
+ throw new Error(
477
+ 'V0_API_KEY not found. Set it in your environment or use --api-key option.\n' +
478
+ 'Get your API key from: https://v0.app/settings/api'
479
+ );
480
+ }
481
+
482
+ // Try using v0-sdk first (if available and properly configured)
483
+ try {
484
+ const { createClient } = await import('v0-sdk');
485
+
486
+ console.log(chalk.blue('🚀 Creating v0 chat via SDK...\n'));
487
+
488
+ // Create client with API key
489
+ const v0Client = createClient({ apiKey });
490
+
491
+ // Prepare files for v0 API
492
+ const v0Files = fileContents.map((file) => ({
493
+ name: `docs/${file.name}`,
494
+ content: file.content,
495
+ locked: options.lockFiles || false,
496
+ }));
497
+
498
+ const chat = await v0Client.chats.init({
499
+ type: 'files',
500
+ files: v0Files,
501
+ name: options.name || `ChatPorter: ${fileContents.length} file(s)`,
502
+ projectId: options.projectId || undefined,
503
+ lockAllFiles: options.lockAllFiles || false,
504
+ });
505
+
506
+ console.log(chalk.green(`✓ Chat created successfully!`));
507
+ console.log(chalk.cyan(` Chat ID: ${chat.id}`));
508
+ console.log(chalk.cyan(` Chat URL: https://v0.dev/chat/${chat.id}`));
509
+
510
+ // Open in browser if requested
511
+ if (options.open !== false) {
512
+ await open(`https://v0.dev/chat/${chat.id}`);
513
+ console.log(chalk.green(`\n🌐 Opened chat in browser`));
514
+ }
515
+
516
+ return chat;
517
+ } catch (sdkError) {
518
+ // Fallback to direct HTTP API calls if SDK fails
519
+ if (sdkError.code === 'ERR_MODULE_NOT_FOUND') {
520
+ console.log(chalk.gray('ℹ v0-sdk not found, using direct API calls\n'));
521
+ } else {
522
+ console.log(chalk.yellow(`⚠ SDK error, falling back to direct API: ${sdkError.message}\n`));
523
+ }
524
+
525
+ return await createV0ChatDirect(fileContents, { ...options, apiKey });
526
+ }
527
+ }
528
+
529
+ // Direct HTTP implementation for v0 Platform API
530
+ // API Reference: https://v0.app/docs/api/platform
531
+ async function createV0ChatDirect(fileContents, options) {
532
+ const apiKey = options.apiKey;
533
+ // v0 Platform API endpoint (may need to verify actual endpoint)
534
+ const apiUrl = process.env.V0_API_URL || 'https://api.v0.dev/v1/chats/init';
535
+
536
+ console.log(chalk.blue('🚀 Creating v0 chat via API...\n'));
537
+
538
+ // Prepare files for v0 API
539
+ const v0Files = fileContents.map((file) => ({
540
+ name: `docs/${file.name}`,
541
+ content: file.content,
542
+ locked: options.lockFiles || false,
543
+ }));
544
+
545
+ const payload = {
546
+ type: 'files',
547
+ files: v0Files,
548
+ name: options.name || `ChatPorter: ${fileContents.length} file(s)`,
549
+ lockAllFiles: options.lockAllFiles || false,
550
+ };
551
+
552
+ if (options.projectId) {
553
+ payload.projectId = options.projectId;
554
+ }
555
+
556
+ try {
557
+ const response = await fetch(apiUrl, {
558
+ method: 'POST',
559
+ headers: {
560
+ 'Authorization': `Bearer ${apiKey}`,
561
+ 'Content-Type': 'application/json',
562
+ },
563
+ body: JSON.stringify(payload),
564
+ });
565
+
566
+ if (!response.ok) {
567
+ const errorText = await response.text();
568
+ let errorData;
569
+ try {
570
+ errorData = JSON.parse(errorText);
571
+ } catch {
572
+ errorData = { message: errorText || response.statusText };
573
+ }
574
+ throw new Error(`API Error: ${response.status} - ${errorData.message || response.statusText}`);
575
+ }
576
+
577
+ const chat = await response.json();
578
+
579
+ console.log(chalk.green(`✓ Chat created successfully!`));
580
+ console.log(chalk.cyan(` Chat ID: ${chat.id}`));
581
+ console.log(chalk.cyan(` Chat URL: https://v0.dev/chat/${chat.id}`));
582
+
583
+ // Open in browser if requested
584
+ if (options.open !== false) {
585
+ await open(`https://v0.dev/chat/${chat.id}`);
586
+ console.log(chalk.green(`\n🌐 Opened chat in browser`));
587
+ }
588
+
589
+ return chat;
590
+ } catch (error) {
591
+ console.error(chalk.red(`\n✗ Error creating v0 chat: ${error.message}`));
592
+ if (error.message.includes('fetch')) {
593
+ console.error(chalk.yellow(' Tip: Check your internet connection and API endpoint'));
594
+ }
595
+ throw error;
596
+ }
597
+ }
598
+
599
+ // Main upload function - detects type and routes accordingly
600
+ async function uploadFiles(filePaths, options) {
601
+ // If single path and it's a GitHub URL, use repo import
602
+ if (filePaths.length === 1 && isGitHubUrl(filePaths[0])) {
603
+ if (options.useApi && options.platform === 'v0') {
604
+ return await createV0ChatFromRepo(filePaths[0], options);
605
+ } else {
606
+ console.error(chalk.red('Repository imports require --api flag with --platform v0'));
607
+ process.exit(1);
608
+ }
609
+ }
610
+
611
+ // If single path and it's a directory, use directory import
612
+ if (filePaths.length === 1) {
613
+ const path = resolve(filePaths[0]);
614
+ if (await isDirectory(path)) {
615
+ if (options.useApi && options.platform === 'v0') {
616
+ return await createV0ChatFromDirectory(path, options);
617
+ } else {
618
+ console.error(chalk.red('Directory imports require --api flag with --platform v0'));
619
+ process.exit(1);
620
+ }
621
+ }
622
+ }
623
+
624
+ // Otherwise, treat as individual files
625
+ console.log(chalk.blue('📦 ChatPorter: Reading files...\n'));
626
+
627
+ const { files, contents } = await readMarkdownFiles(filePaths);
628
+
629
+ if (files.length === 0) {
630
+ console.error(chalk.red('No valid markdown files found.'));
631
+ process.exit(1);
632
+ }
633
+
634
+ console.log(chalk.green(`✓ Found ${files.length} file(s):`));
635
+ files.forEach((file) => {
636
+ console.log(chalk.gray(` - ${file.name} (${(file.size / 1024).toFixed(2)} KB)`));
637
+ });
638
+
639
+ // Use v0 API if requested
640
+ if (options.useApi && options.platform === 'v0') {
641
+ try {
642
+ const chat = await createV0Chat(contents, options);
643
+ return chat;
644
+ } catch (error) {
645
+ console.log(chalk.yellow('\n⚠ Falling back to text formatting...\n'));
646
+ // Fall through to formatting mode
647
+ }
648
+ }
649
+
650
+ // Format and output (fallback or non-API mode)
651
+ const combinedContent = combineMarkdownFiles(contents);
652
+ const platform = options.platform || 'raw';
653
+ const formatter = platformFormatters[platform] || platformFormatters.raw;
654
+
655
+ const formatted = formatter(combinedContent, { files });
656
+
657
+ // Output to file if specified
658
+ if (options.output) {
659
+ const { writeFile } = await import('fs/promises');
660
+ await writeFile(options.output, formatted, 'utf-8');
661
+ console.log(chalk.green(`\n✓ Formatted content written to: ${options.output}`));
662
+ } else {
663
+ // Output to console
664
+ console.log(chalk.blue('\n' + '='.repeat(60)));
665
+ console.log(chalk.bold('Formatted Content:\n'));
666
+ console.log(formatted);
667
+ console.log(chalk.blue('\n' + '='.repeat(60)));
668
+ }
669
+
670
+ // Open in browser if requested
671
+ if (options.open) {
672
+ await openInPlatform(options.open, formatted);
673
+ }
674
+
675
+ return formatted;
676
+ }
677
+
678
+ // Open content in platform
679
+ async function openInPlatform(platform, content) {
680
+ const urls = {
681
+ v0: 'https://v0.dev/chat',
682
+ chatgpt: 'https://chat.openai.com',
683
+ claude: 'https://claude.ai',
684
+ cursor: 'cursor://',
685
+ };
686
+
687
+ const url = urls[platform];
688
+ if (url) {
689
+ console.log(chalk.blue(`\n🌐 Opening ${platform}...`));
690
+ // Note: Content would need to be passed via clipboard or API
691
+ // For now, we'll just open the platform
692
+ await open(url);
693
+ console.log(chalk.yellow('💡 Tip: Copy the formatted content above and paste it into the chat.'));
694
+ } else {
695
+ console.warn(chalk.yellow(`Unknown platform: ${platform}`));
696
+ }
697
+ }
698
+
699
+ // Interactive mode
700
+ async function interactiveMode() {
701
+ console.log(chalk.bold.blue('\n🚀 ChatPorter - Interactive Mode\n'));
702
+
703
+ const hasApiKey = !!process.env.V0_API_KEY;
704
+
705
+ const answers = await inquirer.prompt([
706
+ {
707
+ type: 'list',
708
+ name: 'importType',
709
+ message: 'What would you like to import?',
710
+ choices: [
711
+ { name: 'GitHub Repository', value: 'repo' },
712
+ { name: 'Local Directory', value: 'dir' },
713
+ { name: 'Zip Archive URL', value: 'zip' },
714
+ { name: 'Individual Files', value: 'files' },
715
+ ],
716
+ },
717
+ {
718
+ type: 'input',
719
+ name: 'source',
720
+ message: (answers) => {
721
+ const messages = {
722
+ repo: 'Enter GitHub repository URL:',
723
+ dir: 'Enter directory path:',
724
+ zip: 'Enter zip archive URL:',
725
+ files: 'Enter file paths (space-separated or glob pattern):',
726
+ };
727
+ return messages[answers.importType];
728
+ },
729
+ validate: (input) => input.trim().length > 0 || 'Please enter a valid source',
730
+ },
731
+ {
732
+ type: 'list',
733
+ name: 'platform',
734
+ message: 'Select target platform:',
735
+ choices: (answers) => {
736
+ // Repo/dir/zip imports require API
737
+ if (['repo', 'dir', 'zip'].includes(answers.importType)) {
738
+ return [
739
+ { name: 'v0.dev (API)', value: 'v0-api' },
740
+ ];
741
+ }
742
+ return [
743
+ { name: 'v0.dev (API)', value: 'v0-api' },
744
+ { name: 'v0.dev (formatted text)', value: 'v0' },
745
+ { name: 'ChatGPT', value: 'chatgpt' },
746
+ { name: 'Claude', value: 'claude' },
747
+ { name: 'Cursor AI', value: 'cursor' },
748
+ { name: 'Raw (formatted text)', value: 'raw' },
749
+ ];
750
+ },
751
+ default: hasApiKey ? 'v0-api' : 'v0',
752
+ when: (answers) => answers.importType === 'files' || hasApiKey,
753
+ },
754
+ {
755
+ type: 'input',
756
+ name: 'apiKey',
757
+ message: 'v0 API Key (or set V0_API_KEY env var):',
758
+ when: (answers) => {
759
+ const needsApi = ['repo', 'dir', 'zip'].includes(answers.importType) || answers.platform === 'v0-api';
760
+ return needsApi && !hasApiKey;
761
+ },
762
+ validate: (input) => input.trim().length > 0 || 'API key is required',
763
+ },
764
+ {
765
+ type: 'input',
766
+ name: 'branch',
767
+ message: 'Git branch (optional, defaults to main):',
768
+ when: (answers) => answers.importType === 'repo',
769
+ default: 'main',
770
+ },
771
+ {
772
+ type: 'input',
773
+ name: 'chatName',
774
+ message: 'Chat name (optional):',
775
+ when: (answers) => ['repo', 'dir', 'zip'].includes(answers.importType) || answers.platform === 'v0-api',
776
+ },
777
+ {
778
+ type: 'confirm',
779
+ name: 'lockFiles',
780
+ message: 'Lock files from AI modification?',
781
+ when: (answers) => ['dir', 'files'].includes(answers.importType) && answers.platform === 'v0-api',
782
+ default: false,
783
+ },
784
+ {
785
+ type: 'confirm',
786
+ name: 'lockAllFiles',
787
+ message: 'Lock all files from AI modification?',
788
+ when: (answers) => ['repo', 'zip'].includes(answers.importType) || (answers.platform === 'v0-api' && answers.importType === 'dir'),
789
+ default: false,
790
+ },
791
+ {
792
+ type: 'confirm',
793
+ name: 'saveToFile',
794
+ message: 'Save formatted output to file?',
795
+ when: (answers) => answers.platform !== 'v0-api',
796
+ default: false,
797
+ },
798
+ {
799
+ type: 'input',
800
+ name: 'outputFile',
801
+ message: 'Output file path:',
802
+ when: (answers) => answers.saveToFile,
803
+ default: 'chatporter-output.txt',
804
+ },
805
+ {
806
+ type: 'confirm',
807
+ name: 'openBrowser',
808
+ message: 'Open in browser?',
809
+ default: true,
810
+ },
811
+ ]);
812
+
813
+ const options = {
814
+ platform: answers.platform === 'v0-api' ? 'v0' : (answers.platform || 'v0'),
815
+ useApi: ['repo', 'dir', 'zip'].includes(answers.importType) || answers.platform === 'v0-api',
816
+ apiKey: answers.apiKey || process.env.V0_API_KEY,
817
+ name: answers.chatName,
818
+ branch: answers.branch,
819
+ lockFiles: answers.lockFiles,
820
+ lockAllFiles: answers.lockAllFiles,
821
+ output: answers.saveToFile ? answers.outputFile : null,
822
+ open: answers.openBrowser !== false,
823
+ };
824
+
825
+ // Route to appropriate function based on import type
826
+ if (answers.importType === 'repo') {
827
+ await createV0ChatFromRepo(answers.source, options);
828
+ } else if (answers.importType === 'dir') {
829
+ await createV0ChatFromDirectory(resolve(answers.source), options);
830
+ } else if (answers.importType === 'zip') {
831
+ await createV0ChatFromZip(answers.source, options);
832
+ } else {
833
+ // Individual files
834
+ const filePaths = answers.source.trim().split(/\s+/);
835
+ await uploadFiles(filePaths, options);
836
+ }
837
+ }
838
+
839
+ // CLI Setup
840
+ program
841
+ .name('chatporter')
842
+ .description('Port markdown documents into AI chat conversations')
843
+ .version('1.0.0');
844
+
845
+ program
846
+ .command('upload')
847
+ .description('Upload markdown file(s), directory, or GitHub repository')
848
+ .argument('<paths...>', 'File(s), directory, or GitHub repo URL to upload')
849
+ .option('-p, --platform <platform>', 'Target platform (v0, chatgpt, claude, cursor, raw)', 'raw')
850
+ .option('-o, --output <file>', 'Save formatted output to file')
851
+ .option('--open <platform>', 'Open in browser (v0, chatgpt, claude, cursor)')
852
+ .option('--api', 'Use v0 Platform API to create actual chat (requires V0_API_KEY)', false)
853
+ .option('--api-key <key>', 'v0 API key (or set V0_API_KEY env var)')
854
+ .option('--name <name>', 'Chat name (for API mode)')
855
+ .option('--project-id <id>', 'v0 Project ID (optional, for API mode)')
856
+ .option('--lock-files', 'Lock files from AI modification (API mode)', false)
857
+ .option('--lock-all-files', 'Lock all files from AI modification (API mode)', false)
858
+ .option('--branch <branch>', 'Git branch (for GitHub repo imports)', 'main')
859
+ .action(async (paths, options) => {
860
+ await uploadFiles(paths, options);
861
+ });
862
+
863
+ program
864
+ .command('repo')
865
+ .description('Import GitHub repository to v0 chat')
866
+ .argument('<repo-url>', 'GitHub repository URL (e.g., https://github.com/user/repo)')
867
+ .option('--api-key <key>', 'v0 API key (or set V0_API_KEY env var)')
868
+ .option('--name <name>', 'Chat name')
869
+ .option('--project-id <id>', 'v0 Project ID (optional)')
870
+ .option('--branch <branch>', 'Git branch to import', 'main')
871
+ .option('--lock-all-files', 'Lock all files from AI modification', false)
872
+ .option('--no-open', 'Don\'t open browser after creation')
873
+ .action(async (repoUrl, options) => {
874
+ await createV0ChatFromRepo(repoUrl, {
875
+ ...options,
876
+ useApi: true,
877
+ platform: 'v0',
878
+ open: options.open !== false,
879
+ });
880
+ });
881
+
882
+ program
883
+ .command('dir')
884
+ .description('Import local directory to v0 chat')
885
+ .argument('<directory>', 'Local directory path to import')
886
+ .option('--api-key <key>', 'v0 API key (or set V0_API_KEY env var)')
887
+ .option('--name <name>', 'Chat name')
888
+ .option('--project-id <id>', 'v0 Project ID (optional)')
889
+ .option('--lock-files', 'Lock files from AI modification', false)
890
+ .option('--lock-all-files', 'Lock all files from AI modification', false)
891
+ .option('--no-open', 'Don\'t open browser after creation')
892
+ .action(async (dirPath, options) => {
893
+ await createV0ChatFromDirectory(resolve(dirPath), {
894
+ ...options,
895
+ useApi: true,
896
+ platform: 'v0',
897
+ open: options.open !== false,
898
+ });
899
+ });
900
+
901
+ program
902
+ .command('zip')
903
+ .description('Import zip archive from URL to v0 chat')
904
+ .argument('<zip-url>', 'URL to zip archive (e.g., https://github.com/user/repo/archive/main.zip)')
905
+ .option('--api-key <key>', 'v0 API key (or set V0_API_KEY env var)')
906
+ .option('--name <name>', 'Chat name')
907
+ .option('--project-id <id>', 'v0 Project ID (optional)')
908
+ .option('--lock-all-files', 'Lock all files from AI modification', false)
909
+ .option('--no-open', 'Don\'t open browser after creation')
910
+ .action(async (zipUrl, options) => {
911
+ await createV0ChatFromZip(zipUrl, {
912
+ ...options,
913
+ useApi: true,
914
+ platform: 'v0',
915
+ open: options.open !== false,
916
+ });
917
+ });
918
+
919
+ program
920
+ .command('interactive', { isDefault: true })
921
+ .alias('i')
922
+ .description('Run in interactive mode')
923
+ .action(async () => {
924
+ await interactiveMode();
925
+ });
926
+
927
+ // Parse arguments
928
+ program.parse();
929
+