vigthoria-cli 1.1.0 → 1.3.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,701 @@
1
+ /**
2
+ * Vigthoria CLI - Repo Commands
3
+ *
4
+ * Push and pull projects to/from Vigthoria Repository
5
+ * Enables version control and project sharing through the Vigthoria platform
6
+ *
7
+ * Usage:
8
+ * vigthoria repo push [path] - Push current/specified project to Vigthoria Repo
9
+ * vigthoria repo pull <name> - Pull a project from your Vigthoria Repo
10
+ * vigthoria repo list - List all your projects in Vigthoria Repo
11
+ * vigthoria repo status - Show sync status of current project
12
+ * vigthoria repo share <name> - Generate shareable link for a project
13
+ * vigthoria repo delete <name> - Remove a project from your Vigthoria Repo
14
+ */
15
+
16
+ import chalk from 'chalk';
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import { Config } from '../utils/config.js';
20
+ import { Logger } from '../utils/logger.js';
21
+ import ora from 'ora';
22
+ import inquirer from 'inquirer';
23
+ import archiver from 'archiver';
24
+ import { createWriteStream, createReadStream } from 'fs';
25
+ import { pipeline } from 'stream/promises';
26
+
27
+ interface Project {
28
+ id: number;
29
+ project_name: string;
30
+ project_path: string;
31
+ description: string;
32
+ tech_stack: string;
33
+ visibility: 'public' | 'private' | 'restricted';
34
+ is_public: boolean;
35
+ demo_url: string | null;
36
+ repo_url: string | null;
37
+ status: string;
38
+ storage_usage_mb: number;
39
+ created_at: string;
40
+ updated_at: string;
41
+ last_synced_at: string | null;
42
+ }
43
+
44
+ interface PushOptions {
45
+ path?: string;
46
+ visibility?: 'public' | 'private' | 'restricted';
47
+ description?: string;
48
+ force?: boolean;
49
+ }
50
+
51
+ interface PullOptions {
52
+ output?: string;
53
+ force?: boolean;
54
+ }
55
+
56
+ interface ListOptions {
57
+ visibility?: string;
58
+ showAll?: boolean;
59
+ }
60
+
61
+ interface ShareOptions {
62
+ expires?: string;
63
+ }
64
+
65
+ export class RepoCommand {
66
+ private config: Config;
67
+ private logger: Logger;
68
+ private apiBase: string;
69
+
70
+ constructor(config: Config, logger: Logger) {
71
+ this.config = config;
72
+ this.logger = logger;
73
+ this.apiBase = this.config.get('apiUrl') || 'https://coder.vigthoria.io';
74
+ }
75
+
76
+ private getAuthHeaders(): Record<string, string> {
77
+ const token = this.config.get('authToken');
78
+ return {
79
+ 'Authorization': `Bearer ${token}`,
80
+ 'Content-Type': 'application/json'
81
+ };
82
+ }
83
+
84
+ private isAuthenticated(): boolean {
85
+ const token = this.config.get('authToken');
86
+ return !!token;
87
+ }
88
+
89
+ private requireAuth(): void {
90
+ if (!this.isAuthenticated()) {
91
+ console.log(chalk.red('\nāŒ Authentication required. Run: vigthoria login\n'));
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Detect project info from package.json, requirements.txt, etc.
98
+ */
99
+ private detectProjectInfo(projectPath: string): { name: string; description: string; techStack: string[] } {
100
+ let name = path.basename(projectPath);
101
+ let description = '';
102
+ const techStack: string[] = [];
103
+
104
+ // Check package.json (Node.js)
105
+ const packageJsonPath = path.join(projectPath, 'package.json');
106
+ if (fs.existsSync(packageJsonPath)) {
107
+ try {
108
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
109
+ name = pkg.name || name;
110
+ description = pkg.description || '';
111
+ techStack.push('Node.js');
112
+
113
+ // Detect frameworks from dependencies
114
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
115
+ if (deps.express) techStack.push('Express');
116
+ if (deps.react) techStack.push('React');
117
+ if (deps.vue) techStack.push('Vue');
118
+ if (deps.next) techStack.push('Next.js');
119
+ if (deps.typescript) techStack.push('TypeScript');
120
+ if (deps.sqlite3 || deps['better-sqlite3']) techStack.push('SQLite');
121
+ if (deps.stripe) techStack.push('Stripe');
122
+ } catch (e) {
123
+ // Ignore parse errors
124
+ }
125
+ }
126
+
127
+ // Check requirements.txt (Python)
128
+ const requirementsPath = path.join(projectPath, 'requirements.txt');
129
+ if (fs.existsSync(requirementsPath)) {
130
+ techStack.push('Python');
131
+ const content = fs.readFileSync(requirementsPath, 'utf8').toLowerCase();
132
+ if (content.includes('flask')) techStack.push('Flask');
133
+ if (content.includes('django')) techStack.push('Django');
134
+ if (content.includes('fastapi')) techStack.push('FastAPI');
135
+ if (content.includes('torch') || content.includes('pytorch')) techStack.push('PyTorch');
136
+ if (content.includes('tensorflow')) techStack.push('TensorFlow');
137
+ }
138
+
139
+ // Check Cargo.toml (Rust)
140
+ if (fs.existsSync(path.join(projectPath, 'Cargo.toml'))) {
141
+ techStack.push('Rust');
142
+ }
143
+
144
+ // Check go.mod (Go)
145
+ if (fs.existsSync(path.join(projectPath, 'go.mod'))) {
146
+ techStack.push('Go');
147
+ }
148
+
149
+ return { name, description, techStack: [...new Set(techStack)] };
150
+ }
151
+
152
+ /**
153
+ * Create a zip archive of a project
154
+ */
155
+ private async createProjectArchive(projectPath: string): Promise<string> {
156
+ const tempDir = path.join(require('os').tmpdir(), 'vigthoria-cli');
157
+ if (!fs.existsSync(tempDir)) {
158
+ fs.mkdirSync(tempDir, { recursive: true });
159
+ }
160
+
161
+ const archivePath = path.join(tempDir, `project-${Date.now()}.zip`);
162
+ const output = createWriteStream(archivePath);
163
+ const archive = archiver('zip', { zlib: { level: 9 } });
164
+
165
+ return new Promise((resolve, reject) => {
166
+ output.on('close', () => resolve(archivePath));
167
+ archive.on('error', (err) => reject(err));
168
+
169
+ archive.pipe(output);
170
+
171
+ // Ignore patterns
172
+ const ignorePatterns = [
173
+ 'node_modules/**',
174
+ '.git/**',
175
+ 'dist/**',
176
+ 'build/**',
177
+ '__pycache__/**',
178
+ '.venv/**',
179
+ 'venv/**',
180
+ '*.log',
181
+ '.env',
182
+ '.DS_Store'
183
+ ];
184
+
185
+ archive.glob('**/*', {
186
+ cwd: projectPath,
187
+ ignore: ignorePatterns,
188
+ dot: true
189
+ });
190
+
191
+ archive.finalize();
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Push a project to Vigthoria Repository
197
+ */
198
+ async push(options: PushOptions = {}): Promise<void> {
199
+ this.requireAuth();
200
+
201
+ const projectPath = options.path ? path.resolve(options.path) : process.cwd();
202
+
203
+ if (!fs.existsSync(projectPath)) {
204
+ console.log(chalk.red(`\nāŒ Path does not exist: ${projectPath}\n`));
205
+ return;
206
+ }
207
+
208
+ const spinner = ora('Analyzing project...').start();
209
+
210
+ try {
211
+ const projectInfo = this.detectProjectInfo(projectPath);
212
+ spinner.succeed(`Project detected: ${chalk.cyan(projectInfo.name)}`);
213
+
214
+ console.log(chalk.gray(`\n šŸ“ Path: ${projectPath}`));
215
+ console.log(chalk.gray(` šŸ“ Description: ${projectInfo.description || '(none)'}`));
216
+ console.log(chalk.gray(` šŸ”§ Tech Stack: ${projectInfo.techStack.join(', ') || 'Unknown'}`));
217
+ console.log();
218
+
219
+ // Ask for confirmation and additional info
220
+ const answers = await inquirer.prompt([
221
+ {
222
+ type: 'input',
223
+ name: 'name',
224
+ message: 'Project name:',
225
+ default: projectInfo.name
226
+ },
227
+ {
228
+ type: 'input',
229
+ name: 'description',
230
+ message: 'Description:',
231
+ default: options.description || projectInfo.description
232
+ },
233
+ {
234
+ type: 'list',
235
+ name: 'visibility',
236
+ message: 'Visibility:',
237
+ choices: [
238
+ { name: 'Private - Only you can access', value: 'private' },
239
+ { name: 'Restricted - Invite only', value: 'restricted' },
240
+ { name: 'Public - Anyone can view', value: 'public' }
241
+ ],
242
+ default: options.visibility || 'private'
243
+ },
244
+ {
245
+ type: 'confirm',
246
+ name: 'confirm',
247
+ message: 'Push project to Vigthoria Repo?',
248
+ default: true
249
+ }
250
+ ]);
251
+
252
+ if (!answers.confirm) {
253
+ console.log(chalk.yellow('\nāš ļø Push cancelled.\n'));
254
+ return;
255
+ }
256
+
257
+ const uploadSpinner = ora('Creating project archive...').start();
258
+
259
+ // Create archive
260
+ const archivePath = await this.createProjectArchive(projectPath);
261
+ uploadSpinner.text = 'Uploading to Vigthoria Repo...';
262
+
263
+ // First, register/update the project
264
+ const registerResponse = await fetch(`${this.apiBase}/api/repo/push`, {
265
+ method: 'POST',
266
+ headers: this.getAuthHeaders(),
267
+ body: JSON.stringify({
268
+ projectName: answers.name,
269
+ projectPath: projectPath,
270
+ description: answers.description,
271
+ techStack: projectInfo.techStack.join(', '),
272
+ visibility: answers.visibility,
273
+ force: options.force || false
274
+ })
275
+ });
276
+
277
+ if (!registerResponse.ok) {
278
+ const error = await registerResponse.json() as { error?: string };
279
+ throw new Error(error.error || 'Failed to register project');
280
+ }
281
+
282
+ const registerData = await registerResponse.json() as {
283
+ success: boolean;
284
+ project: Project;
285
+ uploadUrl?: string;
286
+ };
287
+
288
+ // Upload the archive if there's an upload URL
289
+ if (registerData.uploadUrl) {
290
+ const archiveBuffer = fs.readFileSync(archivePath);
291
+ const uploadResponse = await fetch(registerData.uploadUrl, {
292
+ method: 'PUT',
293
+ headers: {
294
+ ...this.getAuthHeaders(),
295
+ 'Content-Type': 'application/zip'
296
+ },
297
+ body: archiveBuffer
298
+ });
299
+
300
+ if (!uploadResponse.ok) {
301
+ throw new Error('Failed to upload project archive');
302
+ }
303
+ }
304
+
305
+ // Clean up temp archive
306
+ fs.unlinkSync(archivePath);
307
+
308
+ uploadSpinner.succeed(chalk.green('Project pushed successfully!'));
309
+
310
+ console.log(chalk.cyan('\nšŸ“¦ Project Details:'));
311
+ console.log(chalk.gray(` Name: ${registerData.project.project_name}`));
312
+ console.log(chalk.gray(` Visibility: ${registerData.project.visibility}`));
313
+ console.log(chalk.gray(` ID: ${registerData.project.id}`));
314
+
315
+ if (registerData.project.visibility === 'public') {
316
+ console.log(chalk.cyan(`\nšŸ”— Public URL: https://coder.vigthoria.io/preview/${registerData.project.id}`));
317
+ }
318
+
319
+ console.log(chalk.gray('\nTip: Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
320
+
321
+ } catch (error) {
322
+ spinner.fail('Push failed');
323
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
324
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Pull a project from Vigthoria Repository
330
+ */
331
+ async pull(projectName: string, options: PullOptions = {}): Promise<void> {
332
+ this.requireAuth();
333
+
334
+ const spinner = ora(`Fetching project: ${projectName}...`).start();
335
+
336
+ try {
337
+ const response = await fetch(`${this.apiBase}/api/repo/pull/${encodeURIComponent(projectName)}`, {
338
+ method: 'GET',
339
+ headers: this.getAuthHeaders()
340
+ });
341
+
342
+ if (!response.ok) {
343
+ const error = await response.json() as { error?: string };
344
+ throw new Error(error.error || 'Project not found');
345
+ }
346
+
347
+ const data = await response.json() as {
348
+ success: boolean;
349
+ project: Project;
350
+ downloadUrl?: string;
351
+ files?: Array<{ path: string; content: string }>;
352
+ };
353
+
354
+ spinner.succeed(`Found project: ${chalk.cyan(data.project.project_name)}`);
355
+
356
+ const outputPath = options.output
357
+ ? path.resolve(options.output)
358
+ : path.join(process.cwd(), data.project.project_name);
359
+
360
+ if (fs.existsSync(outputPath) && !options.force) {
361
+ const { overwrite } = await inquirer.prompt([{
362
+ type: 'confirm',
363
+ name: 'overwrite',
364
+ message: `Directory ${outputPath} exists. Overwrite?`,
365
+ default: false
366
+ }]);
367
+
368
+ if (!overwrite) {
369
+ console.log(chalk.yellow('\nāš ļø Pull cancelled.\n'));
370
+ return;
371
+ }
372
+ }
373
+
374
+ const downloadSpinner = ora('Downloading project files...').start();
375
+
376
+ // Create output directory
377
+ fs.mkdirSync(outputPath, { recursive: true });
378
+
379
+ // If we have a download URL, fetch and extract
380
+ if (data.downloadUrl) {
381
+ const archiveResponse = await fetch(data.downloadUrl, {
382
+ headers: this.getAuthHeaders()
383
+ });
384
+
385
+ if (!archiveResponse.ok) {
386
+ throw new Error('Failed to download project archive');
387
+ }
388
+
389
+ // Save and extract the archive
390
+ const tempArchive = path.join(require('os').tmpdir(), `vigthoria-pull-${Date.now()}.zip`);
391
+ const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
392
+ fs.writeFileSync(tempArchive, archiveBuffer);
393
+
394
+ // Extract using unzip command or adm-zip
395
+ const { execSync } = await import('child_process');
396
+ execSync(`unzip -o "${tempArchive}" -d "${outputPath}"`, { stdio: 'ignore' });
397
+ fs.unlinkSync(tempArchive);
398
+ } else if (data.files) {
399
+ // If we have files inline (small projects)
400
+ for (const file of data.files) {
401
+ const filePath = path.join(outputPath, file.path);
402
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
403
+ fs.writeFileSync(filePath, file.content);
404
+ }
405
+ }
406
+
407
+ downloadSpinner.succeed(chalk.green('Project pulled successfully!'));
408
+
409
+ console.log(chalk.cyan('\nšŸ“ Project extracted to:'));
410
+ console.log(chalk.white(` ${outputPath}`));
411
+ console.log(chalk.gray(`\n Tech Stack: ${data.project.tech_stack || 'Unknown'}`));
412
+ console.log(chalk.gray(` Description: ${data.project.description || '(none)'}`));
413
+ console.log();
414
+
415
+ } catch (error) {
416
+ spinner.fail('Pull failed');
417
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
418
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
419
+ }
420
+ }
421
+
422
+ /**
423
+ * List all projects in user's Vigthoria Repository
424
+ */
425
+ async list(options: ListOptions = {}): Promise<void> {
426
+ this.requireAuth();
427
+
428
+ const spinner = ora('Fetching your projects...').start();
429
+
430
+ try {
431
+ let url = `${this.apiBase}/api/repo/projects`;
432
+ if (options.visibility) {
433
+ url += `?visibility=${options.visibility}`;
434
+ }
435
+
436
+ const response = await fetch(url, {
437
+ method: 'GET',
438
+ headers: this.getAuthHeaders()
439
+ });
440
+
441
+ if (!response.ok) {
442
+ throw new Error('Failed to fetch projects');
443
+ }
444
+
445
+ const data = await response.json() as {
446
+ success: boolean;
447
+ projects: Project[];
448
+ total: number;
449
+ };
450
+
451
+ spinner.stop();
452
+
453
+ if (data.projects.length === 0) {
454
+ console.log(chalk.yellow('\nšŸ“¦ No projects in your Vigthoria Repo yet.\n'));
455
+ console.log(chalk.gray('Use `vigthoria repo push` to upload your first project.\n'));
456
+ return;
457
+ }
458
+
459
+ console.log(chalk.cyan(`\nšŸ“¦ Your Vigthoria Repository (${data.total} project${data.total !== 1 ? 's' : ''})\n`));
460
+
461
+ // Group by visibility
462
+ const grouped = data.projects.reduce((acc: Record<string, Project[]>, project) => {
463
+ const vis = project.visibility || 'private';
464
+ if (!acc[vis]) acc[vis] = [];
465
+ acc[vis].push(project);
466
+ return acc;
467
+ }, {});
468
+
469
+ const visibilityOrder = ['private', 'restricted', 'public'];
470
+ const visibilityIcons: Record<string, string> = {
471
+ private: 'šŸ”’',
472
+ restricted: 'šŸ‘„',
473
+ public: 'šŸŒ'
474
+ };
475
+
476
+ visibilityOrder.forEach(vis => {
477
+ const projects = grouped[vis];
478
+ if (!projects || projects.length === 0) return;
479
+
480
+ console.log(chalk.bold.white(` ${visibilityIcons[vis]} ${vis.toUpperCase()}`));
481
+ console.log(chalk.gray(' ' + '─'.repeat(50)));
482
+
483
+ projects.forEach((project) => {
484
+ const syncStatus = project.last_synced_at
485
+ ? chalk.green('āœ“ synced')
486
+ : chalk.yellow('ā—‹ not synced');
487
+
488
+ console.log(chalk.white(` ${project.project_name.padEnd(30)} ${syncStatus}`));
489
+ if (project.description) {
490
+ console.log(chalk.gray(` ${project.description.substring(0, 50)}${project.description.length > 50 ? '...' : ''}`));
491
+ }
492
+ console.log(chalk.gray(` Tech: ${project.tech_stack || 'Unknown'}`));
493
+ console.log(chalk.gray(` Updated: ${new Date(project.updated_at).toLocaleDateString()}`));
494
+ console.log();
495
+ });
496
+ });
497
+
498
+ console.log(chalk.gray('─'.repeat(60)));
499
+ console.log(chalk.cyan('\nCommands:'));
500
+ console.log(chalk.gray(' vigthoria repo pull <name> - Download a project'));
501
+ console.log(chalk.gray(' vigthoria repo push - Upload current directory'));
502
+ console.log(chalk.gray(' vigthoria repo status - Check sync status'));
503
+ console.log(chalk.gray(' vigthoria repo share <name> - Share a project\n'));
504
+
505
+ } catch (error) {
506
+ spinner.fail('List failed');
507
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
508
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Show sync status of current project
514
+ */
515
+ async status(): Promise<void> {
516
+ this.requireAuth();
517
+
518
+ const projectPath = process.cwd();
519
+ const projectInfo = this.detectProjectInfo(projectPath);
520
+
521
+ const spinner = ora('Checking sync status...').start();
522
+
523
+ try {
524
+ const response = await fetch(`${this.apiBase}/api/repo/status/${encodeURIComponent(projectInfo.name)}`, {
525
+ method: 'GET',
526
+ headers: this.getAuthHeaders()
527
+ });
528
+
529
+ if (!response.ok) {
530
+ spinner.succeed('Project not tracked in Vigthoria Repo');
531
+ console.log(chalk.yellow('\nšŸ“¦ Current project is not synced with Vigthoria Repo.\n'));
532
+ console.log(chalk.gray('Use `vigthoria repo push` to upload it.\n'));
533
+ return;
534
+ }
535
+
536
+ const data = await response.json() as {
537
+ success: boolean;
538
+ project: Project;
539
+ syncStatus: 'synced' | 'ahead' | 'behind' | 'diverged';
540
+ localChanges?: number;
541
+ };
542
+
543
+ spinner.stop();
544
+
545
+ const statusIcons: Record<string, string> = {
546
+ synced: 'āœ…',
547
+ ahead: 'ā¬†ļø',
548
+ behind: 'ā¬‡ļø',
549
+ diverged: 'āš ļø'
550
+ };
551
+
552
+ console.log(chalk.cyan(`\nšŸ“¦ Project: ${chalk.white(data.project.project_name)}\n`));
553
+ console.log(chalk.gray(` Path: ${projectPath}`));
554
+ console.log(chalk.gray(` Visibility: ${data.project.visibility}`));
555
+ console.log(chalk.gray(` Tech Stack: ${data.project.tech_stack || 'Unknown'}`));
556
+ console.log();
557
+ console.log(` Status: ${statusIcons[data.syncStatus]} ${data.syncStatus.toUpperCase()}`);
558
+
559
+ if (data.localChanges && data.localChanges > 0) {
560
+ console.log(chalk.yellow(` Local changes: ${data.localChanges} file(s) modified`));
561
+ }
562
+
563
+ console.log(chalk.gray(`\n Last synced: ${data.project.last_synced_at
564
+ ? new Date(data.project.last_synced_at).toLocaleString()
565
+ : 'Never'}`));
566
+ console.log();
567
+
568
+ if (data.syncStatus === 'ahead' || data.syncStatus === 'diverged') {
569
+ console.log(chalk.cyan('Tip: Use `vigthoria repo push` to sync changes.\n'));
570
+ }
571
+
572
+ } catch (error) {
573
+ spinner.fail('Status check failed');
574
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
575
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Generate a shareable link for a project
581
+ */
582
+ async share(projectName: string, options: ShareOptions = {}): Promise<void> {
583
+ this.requireAuth();
584
+
585
+ const spinner = ora('Generating share link...').start();
586
+
587
+ try {
588
+ const response = await fetch(`${this.apiBase}/api/repo/share`, {
589
+ method: 'POST',
590
+ headers: this.getAuthHeaders(),
591
+ body: JSON.stringify({
592
+ projectName,
593
+ expiresIn: options.expires || '7d'
594
+ })
595
+ });
596
+
597
+ if (!response.ok) {
598
+ const error = await response.json() as { error?: string };
599
+ throw new Error(error.error || 'Failed to generate share link');
600
+ }
601
+
602
+ const data = await response.json() as {
603
+ success: boolean;
604
+ shareUrl: string;
605
+ expiresAt: string;
606
+ };
607
+
608
+ spinner.succeed(chalk.green('Share link generated!'));
609
+
610
+ console.log(chalk.cyan('\nšŸ”— Share Link:'));
611
+ console.log(chalk.white(` ${data.shareUrl}`));
612
+ console.log(chalk.gray(`\n Expires: ${new Date(data.expiresAt).toLocaleString()}`));
613
+ console.log(chalk.gray('\n Anyone with this link can view/download the project.\n'));
614
+
615
+ } catch (error) {
616
+ spinner.fail('Share failed');
617
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
618
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
619
+ }
620
+ }
621
+
622
+ /**
623
+ * Delete a project from Vigthoria Repository
624
+ */
625
+ async delete(projectName: string): Promise<void> {
626
+ this.requireAuth();
627
+
628
+ const { confirm } = await inquirer.prompt([{
629
+ type: 'confirm',
630
+ name: 'confirm',
631
+ message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Repo?`),
632
+ default: false
633
+ }]);
634
+
635
+ if (!confirm) {
636
+ console.log(chalk.yellow('\nāš ļø Delete cancelled.\n'));
637
+ return;
638
+ }
639
+
640
+ const spinner = ora('Deleting project...').start();
641
+
642
+ try {
643
+ const response = await fetch(`${this.apiBase}/api/repo/projects/${encodeURIComponent(projectName)}`, {
644
+ method: 'DELETE',
645
+ headers: this.getAuthHeaders()
646
+ });
647
+
648
+ if (!response.ok) {
649
+ const error = await response.json() as { error?: string };
650
+ throw new Error(error.error || 'Failed to delete project');
651
+ }
652
+
653
+ spinner.succeed(chalk.green('Project deleted from Vigthoria Repo'));
654
+ console.log(chalk.gray('\nNote: Your local files are not affected.\n'));
655
+
656
+ } catch (error) {
657
+ spinner.fail('Delete failed');
658
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
659
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Clone a public project from Vigthoria Repository
665
+ */
666
+ async clone(projectUrl: string, options: PullOptions = {}): Promise<void> {
667
+ // Parse project URL or name
668
+ const projectIdMatch = projectUrl.match(/preview\/(\d+)/);
669
+ const projectId = projectIdMatch ? projectIdMatch[1] : projectUrl;
670
+
671
+ const spinner = ora(`Cloning project...`).start();
672
+
673
+ try {
674
+ const response = await fetch(`${this.apiBase}/api/repo/clone/${projectId}`, {
675
+ method: 'GET',
676
+ headers: this.isAuthenticated() ? this.getAuthHeaders() : { 'Content-Type': 'application/json' }
677
+ });
678
+
679
+ if (!response.ok) {
680
+ const error = await response.json() as { error?: string };
681
+ throw new Error(error.error || 'Project not found or not public');
682
+ }
683
+
684
+ const data = await response.json() as {
685
+ success: boolean;
686
+ project: Project;
687
+ downloadUrl: string;
688
+ };
689
+
690
+ spinner.succeed(`Found project: ${chalk.cyan(data.project.project_name)}`);
691
+
692
+ // Use pull logic for the rest
693
+ await this.pull(data.project.project_name, options);
694
+
695
+ } catch (error) {
696
+ spinner.fail('Clone failed');
697
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
698
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
699
+ }
700
+ }
701
+ }