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/.github/workflows/npm-publish.yml +33 -0
- package/API.md +133 -0
- package/LICENSE +22 -0
- package/README.md +207 -0
- package/REPOSITORY.md +183 -0
- package/SETUP.md +146 -0
- package/USAGE.md +124 -0
- package/bin/chatporter.js +13 -0
- package/package.json +54 -0
- package/src/index.js +929 -0
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
|
+
|