vigthoria-cli 1.12.2 → 1.13.6

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Vigthoria CLI - Repo Commands
3
3
  *
4
- * Push and pull projects to/from Vigthoria Repository
4
+ * Push and pull projects to/from Vigthoria Community Repository
5
5
  * Enables version control and project sharing through the Vigthoria platform
6
6
  *
7
7
  * Usage:
@@ -21,6 +21,32 @@ const require = createRequire(import.meta.url);
21
21
  import inquirer from 'inquirer';
22
22
  import archiver from 'archiver';
23
23
  import { createWriteStream } from 'fs';
24
+ const MAX_REPOSITORY_CONTENT_BYTES = 100 * 1024 * 1024;
25
+ const MAX_REPOSITORY_REQUEST_BYTES = 120 * 1024 * 1024;
26
+ class RepositoryApiError extends Error {
27
+ status;
28
+ payload;
29
+ constructor(status, payload, fallback) {
30
+ super(payload?.error || payload?.message || fallback);
31
+ this.name = 'RepositoryApiError';
32
+ this.status = status;
33
+ this.payload = payload || {};
34
+ }
35
+ }
36
+ function formatBytes(bytes) {
37
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
38
+ }
39
+ async function readResponsePayload(response) {
40
+ const rawBody = await response.text();
41
+ if (!rawBody)
42
+ return {};
43
+ try {
44
+ return JSON.parse(rawBody);
45
+ }
46
+ catch {
47
+ return { error: rawBody };
48
+ }
49
+ }
24
50
  export class RepoCommand {
25
51
  config;
26
52
  logger;
@@ -73,7 +99,11 @@ export class RepoCommand {
73
99
  ...(init.headers || {})
74
100
  }
75
101
  });
76
- if (response.status === 401 && allowCommunityAuthRetry && !this.communityToken) {
102
+ // Both 401 (missing/expired credentials) and 403 (a valid Coder
103
+ // token that Community cannot accept) must activate the explicit
104
+ // Community credential fallback. Previously a stale/incompatible
105
+ // Coder token made configured Community credentials unreachable.
106
+ if ([401, 403].includes(response.status) && allowCommunityAuthRetry && !this.communityToken) {
77
107
  await this.ensureCommunityAuth();
78
108
  if (this.communityToken) {
79
109
  return attempt(url, false);
@@ -83,9 +113,16 @@ export class RepoCommand {
83
113
  };
84
114
  try {
85
115
  const proxyResponse = await attempt(proxyPath, true);
86
- if (proxyResponse.ok || proxyResponse.status < 500) {
116
+ // A large repository payload can exceed the Coder API's general
117
+ // 50 MiB parser while remaining valid under Community's 100 MiB
118
+ // repository contract. Retry that one status directly.
119
+ const shouldTryDirectCommunity = [401, 403, 404, 413].includes(proxyResponse.status);
120
+ if (proxyResponse.ok || (proxyResponse.status < 500 && !shouldTryDirectCommunity)) {
87
121
  return proxyResponse;
88
122
  }
123
+ if (shouldTryDirectCommunity) {
124
+ await proxyResponse.body?.cancel();
125
+ }
89
126
  }
90
127
  catch {
91
128
  // fall through to direct community endpoint
@@ -111,13 +148,32 @@ export class RepoCommand {
111
148
  continue;
112
149
  }
113
150
  try {
151
+ const raw = fs.readFileSync(fullPath);
152
+ let content;
153
+ let encoding;
154
+ try {
155
+ const sample = new TextDecoder('utf-8', { fatal: true }).decode(raw.subarray(0, Math.min(raw.length, 64 * 1024)));
156
+ const controlCharacters = sample.match(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g)?.length || 0;
157
+ const looksBinary = sample.includes('\u0000')
158
+ || (sample.length > 0 && controlCharacters / sample.length > 0.01);
159
+ if (looksBinary)
160
+ throw new Error('binary content');
161
+ content = new TextDecoder('utf-8', { fatal: true }).decode(raw);
162
+ encoding = 'utf8';
163
+ }
164
+ catch {
165
+ content = raw.toString('base64');
166
+ encoding = 'base64';
167
+ }
114
168
  files.push({
115
169
  path: relativePath,
116
- content: fs.readFileSync(fullPath, 'utf8')
170
+ content,
171
+ encoding,
172
+ size: raw.length,
117
173
  });
118
174
  }
119
175
  catch {
120
- // Skip binary or unreadable files.
176
+ // Skip unreadable files. Binary files are transported as base64.
121
177
  }
122
178
  }
123
179
  };
@@ -129,7 +185,7 @@ export class RepoCommand {
129
185
  method: 'GET'
130
186
  });
131
187
  if (!response.ok) {
132
- const error = await response.json().catch(async () => ({ error: await response.text() }));
188
+ const error = await readResponsePayload(response);
133
189
  throw new Error(error.error || error.message || 'Failed to fetch repositories');
134
190
  }
135
191
  const data = await response.json();
@@ -152,6 +208,9 @@ export class RepoCommand {
152
208
  return match;
153
209
  }
154
210
  formatRepoError(error) {
211
+ if (error instanceof RepositoryApiError && error.payload?.code === 'REPOSITORY_SECURITY_REVIEW') {
212
+ return 'Repository is isolated pending security review. See the review details above.';
213
+ }
155
214
  const message = error instanceof Error ? error.message : 'Unknown error';
156
215
  if (/invalid or expired session/i.test(message)) {
157
216
  return 'Repo memory service is not deployed or not reachable. Check `vigthoria status` for details.';
@@ -159,8 +218,37 @@ export class RepoCommand {
159
218
  if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|socket hang up/i.test(message)) {
160
219
  return 'Repo memory service is not reachable. Check `vigthoria status` for details.';
161
220
  }
221
+ if (/PayloadTooLargeError|request entity too large|PAYLOAD_TOO_LARGE|REPOSITORY_CONTENT_TOO_LARGE|exceeds the .*MiB/i.test(message)) {
222
+ return 'Project is too large for a single Community repository push. The limit is 100 MiB of readable project content and 120 MiB for the encoded request. Exclude generated output or split large assets, then retry.';
223
+ }
162
224
  return message;
163
225
  }
226
+ printSecurityReview(review) {
227
+ console.log(chalk.yellow('\n🛡 Repository security review required'));
228
+ console.log(chalk.gray(` Review ID: ${review.reviewId}`));
229
+ console.log(chalk.gray(` Status: ${review.status}`));
230
+ if (review.projectName)
231
+ console.log(chalk.gray(` Project: ${review.projectName}`));
232
+ if (review.riskScore !== undefined)
233
+ console.log(chalk.gray(` Risk score: ${review.riskScore}/100`));
234
+ if (review.reviewerFeedback) {
235
+ console.log(chalk.white(`\n Reviewer feedback: ${review.reviewerFeedback}`));
236
+ }
237
+ for (const entry of review.findings || []) {
238
+ for (const finding of entry.findings || []) {
239
+ const location = entry.path ? `${entry.path}: ` : '';
240
+ console.log(chalk.yellow(` • ${location}${finding.message || finding.type || 'Review required'}`));
241
+ }
242
+ }
243
+ if ((review.actionItems || []).length > 0) {
244
+ console.log(chalk.cyan('\n What to fix:'));
245
+ for (const item of review.actionItems || []) {
246
+ console.log(chalk.gray(` - ${item}`));
247
+ }
248
+ }
249
+ console.log(chalk.cyan(`\n Check status: vigthoria repo review ${review.reviewId}`));
250
+ console.log(chalk.cyan(` Retry fixed content: ${review.retryCommand || `vigthoria repo push --retry ${review.reviewId}`}\n`));
251
+ }
164
252
  isAuthenticated() {
165
253
  const token = this.config.get('authToken');
166
254
  return !!token;
@@ -321,7 +409,7 @@ export class RepoCommand {
321
409
  {
322
410
  type: 'confirm',
323
411
  name: 'confirm',
324
- message: 'Push project to Vigthoria Repo?',
412
+ message: 'Push project to Vigthoria Community Repository?',
325
413
  default: true
326
414
  }
327
415
  ]);
@@ -332,24 +420,35 @@ export class RepoCommand {
332
420
  const uploadSpinner = createSpinner('Preparing project files...').start();
333
421
  const files = this.collectProjectFiles(projectPath);
334
422
  if (files.length === 0) {
335
- throw new Error('No readable text files found to push');
423
+ throw new Error('No readable project files found to push');
336
424
  }
337
- uploadSpinner.text = 'Uploading to Vigthoria Community...';
425
+ const contentBytes = files.reduce((sum, file) => sum + file.size, 0);
426
+ if (contentBytes > MAX_REPOSITORY_CONTENT_BYTES) {
427
+ throw new Error(`Repository content is ${formatBytes(contentBytes)} and exceeds the ${formatBytes(MAX_REPOSITORY_CONTENT_BYTES)} limit`);
428
+ }
429
+ const pushPayload = JSON.stringify({
430
+ projectName: answers.name,
431
+ description: answers.description,
432
+ techStack: projectInfo.techStack.join(', '),
433
+ visibility: answers.visibility,
434
+ force: options.force || false,
435
+ files,
436
+ retryOf: options.retry || undefined,
437
+ clientSource: process.env.VIGTHORIA_CLIENT_SOURCE || 'community-cli',
438
+ commitMessage: `Push from Vigthoria CLI: ${files.length} file(s)`
439
+ });
440
+ const requestBytes = Buffer.byteLength(pushPayload, 'utf8');
441
+ if (requestBytes > MAX_REPOSITORY_REQUEST_BYTES) {
442
+ throw new Error(`Encoded repository request is ${formatBytes(requestBytes)} and exceeds the ${formatBytes(MAX_REPOSITORY_REQUEST_BYTES)} limit`);
443
+ }
444
+ uploadSpinner.text = `Uploading ${files.length} file(s), ${formatBytes(contentBytes)} to Vigthoria Community...`;
338
445
  const registerResponse = await this.repoFetch('/api/repo/push', {
339
446
  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
- })
447
+ body: pushPayload
349
448
  });
350
449
  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');
450
+ const error = await readResponsePayload(registerResponse);
451
+ throw new RepositoryApiError(registerResponse.status, error, 'Failed to push project');
353
452
  }
354
453
  const registerData = await registerResponse.json();
355
454
  uploadSpinner.succeed(chalk.green('Project pushed successfully!'));
@@ -362,15 +461,65 @@ export class RepoCommand {
362
461
  if ((registerData.project?.visibility || answers.visibility) === 'public') {
363
462
  console.log(chalk.cyan(`\n🔗 Public URL: ${registerData.url || `https://community.vigthoria.io/showcase/${registerData.project?.id || registerData.projectId}`}`));
364
463
  }
365
- console.log(chalk.gray('\nTip: Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
464
+ console.log(chalk.gray('\nStored in Vigthoria Community. Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
366
465
  }
367
466
  catch (error) {
368
467
  spinner.stop();
369
468
  this.logger.error('Push failed');
469
+ if (error instanceof RepositoryApiError && error.payload?.code === 'REPOSITORY_SECURITY_REVIEW') {
470
+ this.printSecurityReview({
471
+ reviewId: error.payload.reviewId,
472
+ status: error.payload.status || 'pending_review',
473
+ riskScore: error.payload.riskScore,
474
+ findings: error.payload.findings,
475
+ actionItems: error.payload.actionItems,
476
+ retryCommand: error.payload.retryCommand,
477
+ });
478
+ }
370
479
  const errMsg = this.formatRepoError(error);
371
480
  console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
372
481
  }
373
482
  }
483
+ /**
484
+ * Show the current user's repository security review queue or one review.
485
+ */
486
+ async review(reviewId, options = {}) {
487
+ this.requireAuth();
488
+ const spinner = createSpinner(reviewId ? 'Loading security review...' : 'Loading security reviews...').start();
489
+ try {
490
+ const apiPath = reviewId
491
+ ? `/api/repo/security-reviews/${encodeURIComponent(reviewId)}`
492
+ : '/api/repo/security-reviews?mine=true';
493
+ const response = await this.repoFetch(apiPath, { method: 'GET' });
494
+ const payload = await readResponsePayload(response);
495
+ if (!response.ok) {
496
+ throw new RepositoryApiError(response.status, payload, 'Failed to load repository security review');
497
+ }
498
+ spinner.stop();
499
+ if (options.json) {
500
+ console.log(JSON.stringify(reviewId ? payload.review : payload.reviews, null, 2));
501
+ return;
502
+ }
503
+ if (reviewId) {
504
+ this.printSecurityReview(payload.review);
505
+ return;
506
+ }
507
+ const reviews = (payload.reviews || []);
508
+ if (reviews.length === 0) {
509
+ console.log(chalk.green('\n✓ No repository security reviews found.\n'));
510
+ return;
511
+ }
512
+ console.log(chalk.cyan(`\nRepository security reviews (${reviews.length})`));
513
+ for (const review of reviews) {
514
+ console.log(chalk.gray(` ${review.reviewId} ${review.status} ${review.projectName || 'repository'}`));
515
+ }
516
+ console.log(chalk.gray('\nUse `vigthoria repo review <review-id>` for findings and repair steps.\n'));
517
+ }
518
+ catch (error) {
519
+ spinner.stop();
520
+ console.log(chalk.red(`\n❌ Error: ${this.formatRepoError(error)}\n`));
521
+ }
522
+ }
374
523
  /**
375
524
  * Pull a project from Vigthoria Repository
376
525
  */
@@ -384,7 +533,7 @@ export class RepoCommand {
384
533
  body: JSON.stringify({ projectId: repo.id })
385
534
  });
386
535
  if (!response.ok) {
387
- const error = await response.json().catch(async () => ({ error: await response.text() }));
536
+ const error = await readResponsePayload(response);
388
537
  throw new Error(error.error || error.message || 'Project not found');
389
538
  }
390
539
  const data = await response.json();
@@ -478,14 +627,14 @@ export class RepoCommand {
478
627
  };
479
628
  spinner.stop();
480
629
  if (data.projects.length === 0) {
481
- console.log(chalk.yellow('\n📦 No projects in your Vigthoria Repo yet.\n'));
630
+ console.log(chalk.yellow('\n📦 No projects in your Vigthoria Community Repository yet.\n'));
482
631
  console.log(chalk.gray('Use `vigthoria repo push` to upload your first project.\n'));
483
632
  return;
484
633
  }
485
634
  // Separate owned and shared projects
486
635
  const ownedProjects = data.owned || data.projects.filter((p) => p.access_level === 'owner');
487
636
  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`));
637
+ console.log(chalk.cyan(`\n📦 Your Vigthoria Community Repository (${data.total} project${data.total !== 1 ? 's' : ''})\n`));
489
638
  // Display owned projects grouped by visibility
490
639
  if (ownedProjects.length > 0) {
491
640
  console.log(chalk.bold.cyan(' 📁 YOUR PROJECTS'));
@@ -572,8 +721,8 @@ export class RepoCommand {
572
721
  headers: this.getAuthHeaders()
573
722
  });
574
723
  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'));
724
+ spinner.succeed('Project not tracked in Vigthoria Community Repository');
725
+ console.log(chalk.yellow('\n📦 Current project is not synced with Vigthoria Community Repository.\n'));
577
726
  console.log(chalk.gray('Use `vigthoria repo push` to upload it.\n'));
578
727
  return;
579
728
  }
@@ -650,7 +799,7 @@ export class RepoCommand {
650
799
  const { confirm } = await inquirer.prompt([{
651
800
  type: 'confirm',
652
801
  name: 'confirm',
653
- message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Repo?`),
802
+ message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Community Repository?`),
654
803
  default: false
655
804
  }]);
656
805
  if (!confirm) {
@@ -667,7 +816,7 @@ export class RepoCommand {
667
816
  const error = await response.json();
668
817
  throw new Error(error.error || 'Failed to delete project');
669
818
  }
670
- spinner.succeed(chalk.green('Project deleted from Vigthoria Repo'));
819
+ spinner.succeed(chalk.green('Project deleted from Vigthoria Community Repository'));
671
820
  console.log(chalk.gray('\nNote: Your local files are not affected.\n'));
672
821
  }
673
822
  catch (error) {
@@ -704,7 +853,7 @@ export class RepoCommand {
704
853
  })
705
854
  });
706
855
  if (!response.ok) {
707
- const err = await response.json().catch(async () => ({ error: await response.text() }));
856
+ const err = await readResponsePayload(response);
708
857
  throw new Error(err.error || `Engine import failed (${response.status})`);
709
858
  }
710
859
  const data = await response.json();
@@ -11,6 +11,9 @@ type V4Options = {
11
11
  transport?: 'auto' | 'ws' | 'sse';
12
12
  harnessStreamUrl?: string;
13
13
  harnessControlUrl?: string;
14
+ gatewayUrl?: string;
15
+ prompt?: string;
16
+ workspace?: string;
14
17
  sessionId?: string;
15
18
  nonInteractive?: boolean;
16
19
  python?: string;
@@ -38,6 +38,9 @@ export class V4Command {
38
38
  .option('--transport <mode>', 'Bridge transport: auto | ws | sse', 'auto')
39
39
  .option('--harness-stream-url <url>', 'Override harness stream URL')
40
40
  .option('--harness-control-url <url>', 'Override harness control URL')
41
+ .option('--gateway-url <url>', 'DeerFlow Gateway URL')
42
+ .option('--prompt <text>', 'Task prompt to execute')
43
+ .option('--workspace <path>', 'Workspace path', process.cwd())
41
44
  .option('--session-id <id>', 'Explicit V4 session id')
42
45
  .option('--non-interactive', 'Disable interactive menu and use CLI options', false)
43
46
  .option('--python <bin>', 'Python binary to use', defaultV4PythonBin())
@@ -82,6 +85,15 @@ export class V4Command {
82
85
  if (options.harnessControlUrl) {
83
86
  args.push('--harness-control-url', String(options.harnessControlUrl));
84
87
  }
88
+ if (options.gatewayUrl) {
89
+ args.push('--gateway-url', String(options.gatewayUrl));
90
+ }
91
+ if (options.prompt) {
92
+ args.push('--prompt', String(options.prompt));
93
+ }
94
+ if (options.workspace) {
95
+ args.push('--workspace', path.resolve(String(options.workspace)));
96
+ }
85
97
  if (options.sessionId) {
86
98
  args.push('--session-id', String(options.sessionId));
87
99
  }