vigthoria-cli 1.10.47 → 1.10.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/commands/agent-session-menu.js +2 -8
  2. package/dist/commands/auth.js +51 -68
  3. package/dist/commands/bridge.js +42 -22
  4. package/dist/commands/cancel.js +15 -22
  5. package/dist/commands/chat.d.ts +3 -0
  6. package/dist/commands/chat.js +326 -295
  7. package/dist/commands/config.js +33 -73
  8. package/dist/commands/deploy.js +83 -123
  9. package/dist/commands/device.js +21 -61
  10. package/dist/commands/edit.js +32 -39
  11. package/dist/commands/explain.js +18 -25
  12. package/dist/commands/fork.d.ts +17 -0
  13. package/dist/commands/fork.js +164 -0
  14. package/dist/commands/generate.js +37 -44
  15. package/dist/commands/history.d.ts +17 -0
  16. package/dist/commands/history.js +113 -0
  17. package/dist/commands/hub.js +95 -102
  18. package/dist/commands/index.js +41 -46
  19. package/dist/commands/legion.js +146 -186
  20. package/dist/commands/preview.d.ts +55 -0
  21. package/dist/commands/preview.js +467 -0
  22. package/dist/commands/replay.d.ts +18 -0
  23. package/dist/commands/replay.js +156 -0
  24. package/dist/commands/repo.d.ts +97 -0
  25. package/dist/commands/repo.js +773 -0
  26. package/dist/commands/review.js +29 -36
  27. package/dist/commands/security.js +5 -12
  28. package/dist/commands/update.d.ts +9 -0
  29. package/dist/commands/update.js +201 -0
  30. package/dist/commands/wallet.js +28 -35
  31. package/dist/commands/workflow.js +13 -20
  32. package/dist/index.d.ts +21 -0
  33. package/dist/index.js +1652 -0
  34. package/dist/utils/api.d.ts +544 -0
  35. package/dist/utils/api.js +5486 -0
  36. package/dist/utils/brain-hub-client.js +1 -5
  37. package/dist/utils/bridge-client.js +11 -52
  38. package/dist/utils/cli-state.d.ts +54 -0
  39. package/dist/utils/cli-state.js +185 -0
  40. package/dist/utils/codebase-indexer.js +4 -41
  41. package/dist/utils/config.d.ts +82 -0
  42. package/dist/utils/config.js +269 -0
  43. package/dist/utils/context-ranker.js +15 -21
  44. package/dist/utils/desktop-bridge-client.d.ts +12 -0
  45. package/dist/utils/desktop-bridge-client.js +30 -0
  46. package/dist/utils/files.js +5 -42
  47. package/dist/utils/logger.js +42 -50
  48. package/dist/utils/persona.js +3 -8
  49. package/dist/utils/post-write-validator.js +26 -33
  50. package/dist/utils/project-memory.js +16 -23
  51. package/dist/utils/session.d.ts +118 -0
  52. package/dist/utils/session.js +423 -0
  53. package/dist/utils/task-display.js +13 -20
  54. package/dist/utils/tools.d.ts +269 -0
  55. package/dist/utils/tools.js +3450 -0
  56. package/dist/utils/workspace-brain-service.js +8 -45
  57. package/dist/utils/workspace-cache.js +18 -26
  58. package/dist/utils/workspace-stream.js +21 -63
  59. package/package.json +2 -1
  60. package/scripts/release/validate-no-go-gates.sh +7 -4
@@ -0,0 +1,773 @@
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
+ import chalk from 'chalk';
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import { createRequire } from 'node:module';
19
+ import { createSpinner, CH } from '../utils/logger.js';
20
+ const require = createRequire(import.meta.url);
21
+ import inquirer from 'inquirer';
22
+ import archiver from 'archiver';
23
+ import { createWriteStream } from 'fs';
24
+ export class RepoCommand {
25
+ config;
26
+ logger;
27
+ apiBase;
28
+ communityBase;
29
+ communityToken;
30
+ constructor(config, logger) {
31
+ this.config = config;
32
+ this.logger = logger;
33
+ this.apiBase = this.config.get('apiUrl') || 'https://coder.vigthoria.io';
34
+ this.communityBase = process.env.VIGTHORIA_COMMUNITY_API_URL || 'https://community.vigthoria.io';
35
+ this.communityToken = process.env.VIGTHORIA_COMMUNITY_TOKEN || null;
36
+ }
37
+ getAuthHeaders() {
38
+ const token = this.communityToken || this.config.get('authToken');
39
+ return {
40
+ 'Authorization': `Bearer ${token}`,
41
+ 'Content-Type': 'application/json'
42
+ };
43
+ }
44
+ async ensureCommunityAuth() {
45
+ if (this.communityToken) {
46
+ return;
47
+ }
48
+ const email = process.env.VIGTHORIA_COMMUNITY_EMAIL;
49
+ const password = process.env.VIGTHORIA_COMMUNITY_PASSWORD;
50
+ if (!email || !password) {
51
+ return;
52
+ }
53
+ const response = await fetch(`${this.communityBase}/api/auth/login`, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({ email, password })
57
+ });
58
+ const data = await response.json();
59
+ if (!response.ok || !data.token) {
60
+ throw new Error(data.error || data.message || 'Community login failed');
61
+ }
62
+ this.communityToken = data.token;
63
+ }
64
+ async repoFetch(apiPath, init = {}) {
65
+ const normalizedApiPath = apiPath.startsWith('/api/repo') ? apiPath : `/api/repo${apiPath.startsWith('/') ? apiPath : `/${apiPath}`}`;
66
+ const proxyPath = `${this.apiBase}/api/community-repo${normalizedApiPath.replace('/api/repo', '')}`;
67
+ const directPath = `${this.communityBase}${normalizedApiPath}`;
68
+ const attempt = async (url, allowCommunityAuthRetry) => {
69
+ const response = await fetch(url, {
70
+ ...init,
71
+ headers: {
72
+ ...this.getAuthHeaders(),
73
+ ...(init.headers || {})
74
+ }
75
+ });
76
+ if (response.status === 401 && allowCommunityAuthRetry && !this.communityToken) {
77
+ await this.ensureCommunityAuth();
78
+ if (this.communityToken) {
79
+ return attempt(url, false);
80
+ }
81
+ }
82
+ return response;
83
+ };
84
+ try {
85
+ const proxyResponse = await attempt(proxyPath, true);
86
+ if (proxyResponse.ok || proxyResponse.status < 500) {
87
+ return proxyResponse;
88
+ }
89
+ }
90
+ catch {
91
+ // fall through to direct community endpoint
92
+ }
93
+ return attempt(directPath, true);
94
+ }
95
+ collectProjectFiles(projectPath) {
96
+ const files = [];
97
+ const ignoreNames = new Set(['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv']);
98
+ const ignoreSuffixes = ['.log'];
99
+ const walk = (dir) => {
100
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
101
+ if (ignoreNames.has(entry.name) || entry.name.startsWith('.')) {
102
+ continue;
103
+ }
104
+ const fullPath = path.join(dir, entry.name);
105
+ const relativePath = path.relative(projectPath, fullPath);
106
+ if (entry.isDirectory()) {
107
+ walk(fullPath);
108
+ continue;
109
+ }
110
+ if (!entry.isFile() || ignoreSuffixes.some((suffix) => entry.name.endsWith(suffix))) {
111
+ continue;
112
+ }
113
+ try {
114
+ files.push({
115
+ path: relativePath,
116
+ content: fs.readFileSync(fullPath, 'utf8')
117
+ });
118
+ }
119
+ catch {
120
+ // Skip binary or unreadable files.
121
+ }
122
+ }
123
+ };
124
+ walk(projectPath);
125
+ return files;
126
+ }
127
+ async getMyRepos() {
128
+ const response = await this.repoFetch('/api/repo/my-repos', {
129
+ method: 'GET'
130
+ });
131
+ if (!response.ok) {
132
+ const error = await response.json().catch(async () => ({ error: await response.text() }));
133
+ throw new Error(error.error || error.message || 'Failed to fetch repositories');
134
+ }
135
+ const data = await response.json();
136
+ return data.repos || data.projects || [];
137
+ }
138
+ async resolveRepoByName(projectName) {
139
+ const repos = await this.getMyRepos();
140
+ const normalized = projectName.trim().toLowerCase();
141
+ const exactMatch = repos.find((repo) => {
142
+ const name = String(repo.name || repo.project_name || '').trim().toLowerCase();
143
+ return name === normalized || String(repo.id) === projectName.trim();
144
+ });
145
+ const match = exactMatch || repos.find((repo) => {
146
+ const name = String(repo.name || repo.project_name || '').trim().toLowerCase();
147
+ return name.includes(normalized);
148
+ });
149
+ if (!match) {
150
+ throw new Error(`Repository not found: ${projectName}`);
151
+ }
152
+ return match;
153
+ }
154
+ formatRepoError(error) {
155
+ const message = error instanceof Error ? error.message : 'Unknown error';
156
+ if (/invalid or expired session/i.test(message)) {
157
+ return 'Repo memory service is not deployed or not reachable. Check `vigthoria status` for details.';
158
+ }
159
+ if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|socket hang up/i.test(message)) {
160
+ return 'Repo memory service is not reachable. Check `vigthoria status` for details.';
161
+ }
162
+ return message;
163
+ }
164
+ isAuthenticated() {
165
+ const token = this.config.get('authToken');
166
+ return !!token;
167
+ }
168
+ requireAuth() {
169
+ if (!this.isAuthenticated()) {
170
+ console.log(chalk.red('\nāŒ Authentication required. Run: vigthoria login\n'));
171
+ process.exit(1);
172
+ }
173
+ }
174
+ /**
175
+ * Detect project info from package.json, requirements.txt, etc.
176
+ */
177
+ detectProjectInfo(projectPath) {
178
+ let name = path.basename(projectPath);
179
+ let description = '';
180
+ const techStack = [];
181
+ // Check package.json (Node.js)
182
+ const packageJsonPath = path.join(projectPath, 'package.json');
183
+ if (fs.existsSync(packageJsonPath)) {
184
+ try {
185
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
186
+ name = pkg.name || name;
187
+ description = pkg.description || '';
188
+ techStack.push('Node.js');
189
+ // Detect frameworks from dependencies
190
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
191
+ if (deps.express)
192
+ techStack.push('Express');
193
+ if (deps.react)
194
+ techStack.push('React');
195
+ if (deps.vue)
196
+ techStack.push('Vue');
197
+ if (deps.next)
198
+ techStack.push('Next.js');
199
+ if (deps.typescript)
200
+ techStack.push('TypeScript');
201
+ if (deps.sqlite3 || deps['better-sqlite3'])
202
+ techStack.push('SQLite');
203
+ if (deps.stripe)
204
+ techStack.push('Stripe');
205
+ }
206
+ catch (e) {
207
+ // Ignore parse errors
208
+ }
209
+ }
210
+ // Check requirements.txt (Python)
211
+ const requirementsPath = path.join(projectPath, 'requirements.txt');
212
+ if (fs.existsSync(requirementsPath)) {
213
+ techStack.push('Python');
214
+ const content = fs.readFileSync(requirementsPath, 'utf8').toLowerCase();
215
+ if (content.includes('flask'))
216
+ techStack.push('Flask');
217
+ if (content.includes('django'))
218
+ techStack.push('Django');
219
+ if (content.includes('fastapi'))
220
+ techStack.push('FastAPI');
221
+ if (content.includes('torch') || content.includes('pytorch'))
222
+ techStack.push('PyTorch');
223
+ if (content.includes('tensorflow'))
224
+ techStack.push('TensorFlow');
225
+ }
226
+ // Check Cargo.toml (Rust)
227
+ if (fs.existsSync(path.join(projectPath, 'Cargo.toml'))) {
228
+ techStack.push('Rust');
229
+ }
230
+ // Check go.mod (Go)
231
+ if (fs.existsSync(path.join(projectPath, 'go.mod'))) {
232
+ techStack.push('Go');
233
+ }
234
+ return { name, description, techStack: [...new Set(techStack)] };
235
+ }
236
+ /**
237
+ * Create a zip archive of a project
238
+ */
239
+ async createProjectArchive(projectPath) {
240
+ const tempDir = path.join(require('os').tmpdir(), 'vigthoria-cli');
241
+ if (!fs.existsSync(tempDir)) {
242
+ fs.mkdirSync(tempDir, { recursive: true });
243
+ }
244
+ const archivePath = path.join(tempDir, `project-${Date.now()}.zip`);
245
+ const output = createWriteStream(archivePath);
246
+ const archive = archiver('zip', { zlib: { level: 9 } });
247
+ return new Promise((resolve, reject) => {
248
+ output.on('close', () => resolve(archivePath));
249
+ archive.on('error', (err) => reject(err));
250
+ archive.pipe(output);
251
+ // Ignore patterns
252
+ const ignorePatterns = [
253
+ 'node_modules/**',
254
+ '.git/**',
255
+ 'dist/**',
256
+ 'build/**',
257
+ '__pycache__/**',
258
+ '.venv/**',
259
+ 'venv/**',
260
+ '*.log',
261
+ '.env',
262
+ '.DS_Store'
263
+ ];
264
+ archive.glob('**/*', {
265
+ cwd: projectPath,
266
+ ignore: ignorePatterns,
267
+ dot: true
268
+ });
269
+ archive.finalize();
270
+ });
271
+ }
272
+ /**
273
+ * Push a project to Vigthoria Repository
274
+ */
275
+ async push(options = {}) {
276
+ this.requireAuth();
277
+ const projectPath = options.path ? path.resolve(options.path) : process.cwd();
278
+ if (!fs.existsSync(projectPath)) {
279
+ console.log(chalk.red(`\nāŒ Path does not exist: ${projectPath}\n`));
280
+ return;
281
+ }
282
+ const spinner = createSpinner('Analyzing project...').start();
283
+ try {
284
+ const projectInfo = this.detectProjectInfo(projectPath);
285
+ spinner.succeed(`Project detected: ${chalk.cyan(projectInfo.name)}`);
286
+ console.log(chalk.gray(`\n šŸ“ Path: ${projectPath}`));
287
+ console.log(chalk.gray(` šŸ“ Description: ${projectInfo.description || '(none)'}`));
288
+ console.log(chalk.gray(` šŸ”§ Tech Stack: ${projectInfo.techStack.join(', ') || 'Unknown'}`));
289
+ console.log();
290
+ const defaults = {
291
+ name: options.name || projectInfo.name,
292
+ description: options.description || projectInfo.description,
293
+ visibility: options.visibility || 'private'
294
+ };
295
+ const answers = options.yes
296
+ ? { ...defaults, confirm: true }
297
+ : await inquirer.prompt([
298
+ {
299
+ type: 'input',
300
+ name: 'name',
301
+ message: 'Project name:',
302
+ default: defaults.name
303
+ },
304
+ {
305
+ type: 'input',
306
+ name: 'description',
307
+ message: 'Description:',
308
+ default: defaults.description
309
+ },
310
+ {
311
+ type: 'list',
312
+ name: 'visibility',
313
+ message: 'Visibility:',
314
+ choices: [
315
+ { name: 'Private - Only you can access', value: 'private' },
316
+ { name: 'Restricted - Invite only', value: 'restricted' },
317
+ { name: 'Public - Anyone can view', value: 'public' }
318
+ ],
319
+ default: defaults.visibility
320
+ },
321
+ {
322
+ type: 'confirm',
323
+ name: 'confirm',
324
+ message: 'Push project to Vigthoria Repo?',
325
+ default: true
326
+ }
327
+ ]);
328
+ if (!answers.confirm) {
329
+ console.log(chalk.yellow(`\n${CH.warnEmoji} Push cancelled.\n`));
330
+ return;
331
+ }
332
+ const uploadSpinner = createSpinner('Preparing project files...').start();
333
+ const files = this.collectProjectFiles(projectPath);
334
+ if (files.length === 0) {
335
+ throw new Error('No readable text files found to push');
336
+ }
337
+ uploadSpinner.text = 'Uploading to Vigthoria Community...';
338
+ const registerResponse = await this.repoFetch('/api/repo/push', {
339
+ method: 'POST',
340
+ body: JSON.stringify({
341
+ projectName: answers.name,
342
+ description: answers.description,
343
+ techStack: projectInfo.techStack.join(', '),
344
+ visibility: answers.visibility,
345
+ force: options.force || false,
346
+ files,
347
+ commitMessage: `Push from Vigthoria CLI: ${files.length} file(s)`
348
+ })
349
+ });
350
+ if (!registerResponse.ok) {
351
+ const error = await registerResponse.json().catch(async () => ({ error: await registerResponse.text() }));
352
+ throw new Error(error.error || error.message || 'Failed to push project');
353
+ }
354
+ const registerData = await registerResponse.json();
355
+ uploadSpinner.succeed(chalk.green('Project pushed successfully!'));
356
+ console.log(chalk.cyan('\nšŸ“¦ Project Details:'));
357
+ console.log(chalk.gray(` Name: ${registerData.project?.project_name || registerData.project?.name || answers.name}`));
358
+ console.log(chalk.gray(` Visibility: ${registerData.project?.visibility || answers.visibility}`));
359
+ if (registerData.project?.id || registerData.projectId) {
360
+ console.log(chalk.gray(` ID: ${registerData.project?.id || registerData.projectId}`));
361
+ }
362
+ if ((registerData.project?.visibility || answers.visibility) === 'public') {
363
+ console.log(chalk.cyan(`\nšŸ”— Public URL: ${registerData.url || `https://community.vigthoria.io/showcase/${registerData.project?.id || registerData.projectId}`}`));
364
+ }
365
+ console.log(chalk.gray('\nTip: Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
366
+ }
367
+ catch (error) {
368
+ spinner.stop();
369
+ this.logger.error('Push failed');
370
+ const errMsg = this.formatRepoError(error);
371
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
372
+ }
373
+ }
374
+ /**
375
+ * Pull a project from Vigthoria Repository
376
+ */
377
+ async pull(projectName, options = {}) {
378
+ this.requireAuth();
379
+ const spinner = createSpinner(`Fetching project: ${projectName}...`).start();
380
+ try {
381
+ const repo = await this.resolveRepoByName(projectName);
382
+ const response = await this.repoFetch('/api/repo/pull', {
383
+ method: 'POST',
384
+ body: JSON.stringify({ projectId: repo.id })
385
+ });
386
+ if (!response.ok) {
387
+ const error = await response.json().catch(async () => ({ error: await response.text() }));
388
+ throw new Error(error.error || error.message || 'Project not found');
389
+ }
390
+ const data = await response.json();
391
+ spinner.succeed(`Found project: ${chalk.cyan(data.project?.project_name || data.projectName || repo.name || repo.project_name || projectName)}`);
392
+ const outputPath = options.output
393
+ ? path.resolve(options.output)
394
+ : path.join(process.cwd(), data.project?.project_name || data.projectName || repo.name || repo.project_name || projectName);
395
+ if (fs.existsSync(outputPath) && !options.force) {
396
+ const { overwrite } = await inquirer.prompt([{
397
+ type: 'confirm',
398
+ name: 'overwrite',
399
+ message: `Directory ${outputPath} exists. Overwrite?`,
400
+ default: false
401
+ }]);
402
+ if (!overwrite) {
403
+ console.log(chalk.yellow(`\n${CH.warnEmoji} Pull cancelled.\n`));
404
+ return;
405
+ }
406
+ }
407
+ const downloadSpinner = createSpinner('Downloading project files...').start();
408
+ // Create output directory
409
+ fs.mkdirSync(outputPath, { recursive: true });
410
+ // If we have a download URL, fetch and extract
411
+ if (data.downloadUrl) {
412
+ const archiveResponse = await fetch(data.downloadUrl, {
413
+ headers: this.getAuthHeaders()
414
+ });
415
+ if (!archiveResponse.ok) {
416
+ throw new Error('Failed to download project archive');
417
+ }
418
+ // Save and extract the archive
419
+ const tempArchive = path.join(require('os').tmpdir(), `vigthoria-pull-${Date.now()}.zip`);
420
+ const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
421
+ fs.writeFileSync(tempArchive, archiveBuffer);
422
+ // Extract archive - cross-platform
423
+ const os = require('os');
424
+ const platform = os.platform();
425
+ if (platform === 'win32') {
426
+ // Use PowerShell's Expand-Archive — args as array to prevent injection
427
+ const { execFileSync } = await import('child_process');
428
+ execFileSync('powershell', [
429
+ '-NoProfile', '-NonInteractive', '-Command',
430
+ `Expand-Archive -Path '${tempArchive.replace(/'/g, "''")}' -DestinationPath '${outputPath.replace(/'/g, "''")}' -Force`
431
+ ], { stdio: 'ignore', windowsHide: true });
432
+ }
433
+ else {
434
+ // Use unzip on Unix-like systems — args as array to prevent injection
435
+ const { execFileSync } = await import('child_process');
436
+ execFileSync('unzip', ['-o', tempArchive, '-d', outputPath], { stdio: 'ignore' });
437
+ }
438
+ fs.unlinkSync(tempArchive);
439
+ }
440
+ else if (data.files) {
441
+ // If we have files inline (small projects)
442
+ for (const file of data.files) {
443
+ const filePath = path.join(outputPath, file.path);
444
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
445
+ fs.writeFileSync(filePath, file.content);
446
+ }
447
+ }
448
+ downloadSpinner.succeed(chalk.green('Project pulled successfully!'));
449
+ console.log(chalk.cyan('\nšŸ“ Project extracted to:'));
450
+ console.log(chalk.white(` ${outputPath}`));
451
+ console.log(chalk.gray(`\n Tech Stack: ${data.project?.tech_stack || repo.tech_stack || 'Unknown'}`));
452
+ console.log(chalk.gray(` Description: ${data.project?.description || data.description || repo.description || '(none)'}`));
453
+ console.log();
454
+ }
455
+ catch (error) {
456
+ spinner.stop();
457
+ this.logger.error('Pull failed');
458
+ const errMsg = this.formatRepoError(error);
459
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
460
+ }
461
+ }
462
+ /**
463
+ * List all projects in user's Vigthoria Repository
464
+ */
465
+ async list(options = {}) {
466
+ this.requireAuth();
467
+ const spinner = createSpinner('Fetching your projects...').start();
468
+ try {
469
+ const repos = await this.getMyRepos();
470
+ const filteredRepos = options.visibility
471
+ ? repos.filter((repo) => (repo.visibility || repo.is_public === true ? 'public' : 'private') === options.visibility)
472
+ : repos;
473
+ const data = {
474
+ projects: filteredRepos,
475
+ owned: filteredRepos,
476
+ shared: [],
477
+ total: filteredRepos.length
478
+ };
479
+ spinner.stop();
480
+ if (data.projects.length === 0) {
481
+ console.log(chalk.yellow('\nšŸ“¦ No projects in your Vigthoria Repo yet.\n'));
482
+ console.log(chalk.gray('Use `vigthoria repo push` to upload your first project.\n'));
483
+ return;
484
+ }
485
+ // Separate owned and shared projects
486
+ const ownedProjects = data.owned || data.projects.filter((p) => p.access_level === 'owner');
487
+ const sharedProjects = data.shared || data.projects.filter((p) => p.access_level !== 'owner');
488
+ console.log(chalk.cyan(`\nšŸ“¦ Your Vigthoria Repository (${data.total} project${data.total !== 1 ? 's' : ''})\n`));
489
+ // Display owned projects grouped by visibility
490
+ if (ownedProjects.length > 0) {
491
+ console.log(chalk.bold.cyan(' šŸ“ YOUR PROJECTS'));
492
+ console.log(chalk.gray(' ' + CH.hLine.repeat(50)));
493
+ // Group by visibility
494
+ const grouped = ownedProjects.reduce((acc, project) => {
495
+ const vis = project.visibility || 'private';
496
+ if (!acc[vis])
497
+ acc[vis] = [];
498
+ acc[vis].push(project);
499
+ return acc;
500
+ }, {});
501
+ const visibilityOrder = ['private', 'restricted', 'public'];
502
+ const visibilityIcons = {
503
+ private: 'šŸ”’',
504
+ restricted: 'šŸ‘„',
505
+ public: 'šŸŒ'
506
+ };
507
+ visibilityOrder.forEach(vis => {
508
+ const projects = grouped[vis];
509
+ if (!projects || projects.length === 0)
510
+ return;
511
+ console.log(chalk.bold.white(` ${visibilityIcons[vis]} ${vis.toUpperCase()}`));
512
+ projects.forEach((project) => {
513
+ const projectName = project.project_name || project.name || 'Unnamed';
514
+ const techStack = project.tech_stack || project.framework || 'Unknown';
515
+ const updatedAt = project.updated_at || project.updatedAt || project.created_at || project.createdAt;
516
+ const syncStatus = (project.last_synced_at || project.lastSyncedAt)
517
+ ? chalk.green(`${CH.success} synced`)
518
+ : chalk.yellow('- not synced');
519
+ console.log(chalk.white(` ${projectName.padEnd(30)} ${syncStatus}`));
520
+ if (project.description) {
521
+ console.log(chalk.gray(` ${project.description.substring(0, 50)}${project.description.length > 50 ? '...' : ''}`));
522
+ }
523
+ console.log(chalk.gray(` Tech: ${techStack}`));
524
+ console.log(chalk.gray(` Updated: ${updatedAt ? new Date(updatedAt).toLocaleDateString() : 'Unknown'}`));
525
+ console.log();
526
+ });
527
+ });
528
+ }
529
+ // Display shared projects
530
+ if (sharedProjects.length > 0) {
531
+ console.log(chalk.bold.magenta('\n šŸ¤ SHARED WITH YOU'));
532
+ console.log(chalk.gray(' ' + CH.hLine.repeat(50)));
533
+ sharedProjects.forEach((project) => {
534
+ const projectName = project.project_name || project.name || 'Unnamed';
535
+ const techStack = project.tech_stack || project.framework || 'Unknown';
536
+ const updatedAt = project.updated_at || project.updatedAt || project.created_at || project.createdAt;
537
+ const accessIcon = project.access_level === 'admin' ? 'šŸ‘‘' : project.access_level === 'write' ? 'āœļø' : 'šŸ‘ļø';
538
+ console.log(chalk.white(` ${accessIcon} ${projectName.padEnd(28)} [${project.access_level}]`));
539
+ if (project.description) {
540
+ console.log(chalk.gray(` ${project.description.substring(0, 50)}${project.description.length > 50 ? '...' : ''}`));
541
+ }
542
+ console.log(chalk.gray(` Tech: ${techStack}`));
543
+ console.log(chalk.gray(` Updated: ${updatedAt ? new Date(updatedAt).toLocaleDateString() : 'Unknown'}`));
544
+ console.log();
545
+ });
546
+ }
547
+ console.log(chalk.gray(CH.hLine.repeat(60)));
548
+ console.log(chalk.cyan('\nCommands:'));
549
+ console.log(chalk.gray(' vigthoria repo pull <name> - Download a project'));
550
+ console.log(chalk.gray(' vigthoria repo push - Upload current directory'));
551
+ console.log(chalk.gray(' vigthoria repo status - Check sync status'));
552
+ console.log(chalk.gray(' vigthoria repo share <name> - Share a project\n'));
553
+ }
554
+ catch (error) {
555
+ spinner.stop();
556
+ this.logger.error('List failed');
557
+ const errMsg = this.formatRepoError(error);
558
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
559
+ }
560
+ }
561
+ /**
562
+ * Show sync status of current project
563
+ */
564
+ async status() {
565
+ this.requireAuth();
566
+ const projectPath = process.cwd();
567
+ const projectInfo = this.detectProjectInfo(projectPath);
568
+ const spinner = createSpinner('Checking sync status...').start();
569
+ try {
570
+ const response = await fetch(`${this.apiBase}/api/repo/status/${encodeURIComponent(projectInfo.name)}`, {
571
+ method: 'GET',
572
+ headers: this.getAuthHeaders()
573
+ });
574
+ if (!response.ok) {
575
+ spinner.succeed('Project not tracked in Vigthoria Repo');
576
+ console.log(chalk.yellow('\nšŸ“¦ Current project is not synced with Vigthoria Repo.\n'));
577
+ console.log(chalk.gray('Use `vigthoria repo push` to upload it.\n'));
578
+ return;
579
+ }
580
+ const data = await response.json();
581
+ spinner.stop();
582
+ const statusIcons = {
583
+ synced: 'āœ…',
584
+ ahead: 'ā¬†ļø',
585
+ behind: 'ā¬‡ļø',
586
+ diverged: CH.warnEmoji
587
+ };
588
+ console.log(chalk.cyan(`\nšŸ“¦ Project: ${chalk.white(data.project.project_name)}\n`));
589
+ console.log(chalk.gray(` Path: ${projectPath}`));
590
+ console.log(chalk.gray(` Visibility: ${data.project.visibility}`));
591
+ console.log(chalk.gray(` Tech Stack: ${data.project.tech_stack || 'Unknown'}`));
592
+ console.log();
593
+ console.log(` Status: ${statusIcons[data.syncStatus]} ${data.syncStatus.toUpperCase()}`);
594
+ if (data.localChanges && data.localChanges > 0) {
595
+ console.log(chalk.yellow(` Local changes: ${data.localChanges} file(s) modified`));
596
+ }
597
+ console.log(chalk.gray(`\n Last synced: ${data.project.last_synced_at
598
+ ? new Date(data.project.last_synced_at).toLocaleString()
599
+ : 'Never'}`));
600
+ console.log();
601
+ if (data.syncStatus === 'ahead' || data.syncStatus === 'diverged') {
602
+ console.log(chalk.cyan('Tip: Use `vigthoria repo push` to sync changes.\n'));
603
+ }
604
+ }
605
+ catch (error) {
606
+ spinner.stop();
607
+ this.logger.error('Status check failed');
608
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
609
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
610
+ }
611
+ }
612
+ /**
613
+ * Generate a shareable link for a project
614
+ */
615
+ async share(projectName, options = {}) {
616
+ this.requireAuth();
617
+ const spinner = createSpinner('Generating share link...').start();
618
+ try {
619
+ const response = await fetch(`${this.apiBase}/api/repo/share`, {
620
+ method: 'POST',
621
+ headers: this.getAuthHeaders(),
622
+ body: JSON.stringify({
623
+ projectName,
624
+ expiresIn: options.expires || '7d'
625
+ })
626
+ });
627
+ if (!response.ok) {
628
+ const error = await response.json();
629
+ throw new Error(error.error || 'Failed to generate share link');
630
+ }
631
+ const data = await response.json();
632
+ spinner.succeed(chalk.green('Share link generated!'));
633
+ console.log(chalk.cyan('\nšŸ”— Share Link:'));
634
+ console.log(chalk.white(` ${data.shareUrl}`));
635
+ console.log(chalk.gray(`\n Expires: ${new Date(data.expiresAt).toLocaleString()}`));
636
+ console.log(chalk.gray('\n Anyone with this link can view/download the project.\n'));
637
+ }
638
+ catch (error) {
639
+ spinner.stop();
640
+ this.logger.error('Share failed');
641
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
642
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
643
+ }
644
+ }
645
+ /**
646
+ * Delete a project from Vigthoria Repository
647
+ */
648
+ async delete(projectName) {
649
+ this.requireAuth();
650
+ const { confirm } = await inquirer.prompt([{
651
+ type: 'confirm',
652
+ name: 'confirm',
653
+ message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Repo?`),
654
+ default: false
655
+ }]);
656
+ if (!confirm) {
657
+ console.log(chalk.yellow(`\n${CH.warnEmoji} Delete cancelled.\n`));
658
+ return;
659
+ }
660
+ const spinner = createSpinner('Deleting project...').start();
661
+ try {
662
+ const response = await fetch(`${this.apiBase}/api/repo/projects/${encodeURIComponent(projectName)}`, {
663
+ method: 'DELETE',
664
+ headers: this.getAuthHeaders()
665
+ });
666
+ if (!response.ok) {
667
+ const error = await response.json();
668
+ throw new Error(error.error || 'Failed to delete project');
669
+ }
670
+ spinner.succeed(chalk.green('Project deleted from Vigthoria Repo'));
671
+ console.log(chalk.gray('\nNote: Your local files are not affected.\n'));
672
+ }
673
+ catch (error) {
674
+ spinner.stop();
675
+ this.logger.error('Delete failed');
676
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
677
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
678
+ }
679
+ }
680
+ /**
681
+ * Open a project in a Vigthoria engine (shop, visual, game).
682
+ * Pushes to community if not yet there, then triggers engine import.
683
+ */
684
+ async openIn(engine, projectName, options = {}) {
685
+ this.requireAuth();
686
+ const engineLabels = {
687
+ shop: 'Shop Engine',
688
+ visual: 'Visual Editor',
689
+ game: 'Gaming Engine'
690
+ };
691
+ const label = engineLabels[engine] || engine;
692
+ // Resolve project name from arg or current directory
693
+ const resolvedName = projectName || this.detectProjectInfo(process.cwd()).name;
694
+ const spinner = createSpinner(`Opening "${resolvedName}" in ${label}...`).start();
695
+ try {
696
+ const response = await fetch(`${this.apiBase}/api/engine-import`, {
697
+ method: 'POST',
698
+ headers: this.getAuthHeaders(),
699
+ body: JSON.stringify({
700
+ engine,
701
+ projectName: resolvedName,
702
+ shopId: options.shopId || 'default',
703
+ source: 'repo'
704
+ })
705
+ });
706
+ if (!response.ok) {
707
+ const err = await response.json().catch(async () => ({ error: await response.text() }));
708
+ throw new Error(err.error || `Engine import failed (${response.status})`);
709
+ }
710
+ const data = await response.json();
711
+ spinner.succeed(chalk.green(`Project imported into ${label}!`));
712
+ console.log(chalk.cyan(`\nšŸš€ Open in browser:`));
713
+ console.log(chalk.bold(` ${data.url}\n`));
714
+ if (options.browser) {
715
+ try {
716
+ const { execFileSync } = await import('child_process');
717
+ const platform = process.platform;
718
+ // Validate URL scheme before passing to OS — prevents shell injection
719
+ const parsedUrl = new URL(data.url);
720
+ if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
721
+ throw new Error('Unsafe URL scheme for browser open');
722
+ }
723
+ const safeUrl = parsedUrl.href;
724
+ if (platform === 'win32')
725
+ execFileSync('cmd', ['/c', 'start', '', safeUrl], { stdio: 'ignore', windowsHide: true });
726
+ else if (platform === 'darwin')
727
+ execFileSync('open', [safeUrl], { stdio: 'ignore' });
728
+ else
729
+ execFileSync('xdg-open', [safeUrl], { stdio: 'ignore' });
730
+ }
731
+ catch { /* browser open is optional */ }
732
+ }
733
+ if (data.message) {
734
+ console.log(chalk.gray(` ${data.message}\n`));
735
+ }
736
+ }
737
+ catch (error) {
738
+ spinner.stop();
739
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
740
+ console.log(chalk.red(`\nāŒ ${label} import failed: ${errMsg}\n`));
741
+ console.log(chalk.gray('Tip: Make sure your project is pushed first with `vigthoria repo push`\n'));
742
+ }
743
+ }
744
+ /**
745
+ * Clone a public project from Vigthoria Repository
746
+ */
747
+ async clone(projectUrl, options = {}) {
748
+ // Parse project URL or name
749
+ const projectIdMatch = projectUrl.match(/preview\/(\d+)/);
750
+ const projectId = projectIdMatch ? projectIdMatch[1] : projectUrl;
751
+ const spinner = createSpinner(`Cloning project...`).start();
752
+ try {
753
+ const response = await fetch(`${this.apiBase}/api/repo/clone/${projectId}`, {
754
+ method: 'GET',
755
+ headers: this.isAuthenticated() ? this.getAuthHeaders() : { 'Content-Type': 'application/json' }
756
+ });
757
+ if (!response.ok) {
758
+ const error = await response.json();
759
+ throw new Error(error.error || 'Project not found or not public');
760
+ }
761
+ const data = await response.json();
762
+ spinner.succeed(`Found project: ${chalk.cyan(data.project.project_name)}`);
763
+ // Use pull logic for the rest
764
+ await this.pull(data.project.project_name, options);
765
+ }
766
+ catch (error) {
767
+ spinner.stop();
768
+ this.logger.error('Clone failed');
769
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
770
+ console.log(chalk.red(`\nāŒ Error: ${errMsg}\n`));
771
+ }
772
+ }
773
+ }