vigthoria-cli 1.12.3 → 1.13.7

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:
@@ -19,8 +19,34 @@ import { createRequire } from 'node:module';
19
19
  import { createSpinner, CH } from '../utils/logger.js';
20
20
  const require = createRequire(import.meta.url);
21
21
  import inquirer from 'inquirer';
22
- import archiver from 'archiver';
22
+ import * as archiverModule 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;
@@ -243,7 +331,9 @@ export class RepoCommand {
243
331
  }
244
332
  const archivePath = path.join(tempDir, `project-${Date.now()}.zip`);
245
333
  const output = createWriteStream(archivePath);
246
- const archive = archiver('zip', { zlib: { level: 9 } });
334
+ // Archiver 8 is ESM-only and exposes format-specific constructors
335
+ // instead of the legacy default factory.
336
+ const archive = new archiverModule.ZipArchive({ zlib: { level: 9 } });
247
337
  return new Promise((resolve, reject) => {
248
338
  output.on('close', () => resolve(archivePath));
249
339
  archive.on('error', (err) => reject(err));
@@ -321,7 +411,7 @@ export class RepoCommand {
321
411
  {
322
412
  type: 'confirm',
323
413
  name: 'confirm',
324
- message: 'Push project to Vigthoria Repo?',
414
+ message: 'Push project to Vigthoria Community Repository?',
325
415
  default: true
326
416
  }
327
417
  ]);
@@ -332,24 +422,35 @@ export class RepoCommand {
332
422
  const uploadSpinner = createSpinner('Preparing project files...').start();
333
423
  const files = this.collectProjectFiles(projectPath);
334
424
  if (files.length === 0) {
335
- throw new Error('No readable text files found to push');
425
+ throw new Error('No readable project files found to push');
336
426
  }
337
- uploadSpinner.text = 'Uploading to Vigthoria Community...';
427
+ const contentBytes = files.reduce((sum, file) => sum + file.size, 0);
428
+ if (contentBytes > MAX_REPOSITORY_CONTENT_BYTES) {
429
+ throw new Error(`Repository content is ${formatBytes(contentBytes)} and exceeds the ${formatBytes(MAX_REPOSITORY_CONTENT_BYTES)} limit`);
430
+ }
431
+ const pushPayload = JSON.stringify({
432
+ projectName: answers.name,
433
+ description: answers.description,
434
+ techStack: projectInfo.techStack.join(', '),
435
+ visibility: answers.visibility,
436
+ force: options.force || false,
437
+ files,
438
+ retryOf: options.retry || undefined,
439
+ clientSource: process.env.VIGTHORIA_CLIENT_SOURCE || 'community-cli',
440
+ commitMessage: `Push from Vigthoria CLI: ${files.length} file(s)`
441
+ });
442
+ const requestBytes = Buffer.byteLength(pushPayload, 'utf8');
443
+ if (requestBytes > MAX_REPOSITORY_REQUEST_BYTES) {
444
+ throw new Error(`Encoded repository request is ${formatBytes(requestBytes)} and exceeds the ${formatBytes(MAX_REPOSITORY_REQUEST_BYTES)} limit`);
445
+ }
446
+ uploadSpinner.text = `Uploading ${files.length} file(s), ${formatBytes(contentBytes)} to Vigthoria Community...`;
338
447
  const registerResponse = await this.repoFetch('/api/repo/push', {
339
448
  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
- })
449
+ body: pushPayload
349
450
  });
350
451
  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');
452
+ const error = await readResponsePayload(registerResponse);
453
+ throw new RepositoryApiError(registerResponse.status, error, 'Failed to push project');
353
454
  }
354
455
  const registerData = await registerResponse.json();
355
456
  uploadSpinner.succeed(chalk.green('Project pushed successfully!'));
@@ -362,15 +463,65 @@ export class RepoCommand {
362
463
  if ((registerData.project?.visibility || answers.visibility) === 'public') {
363
464
  console.log(chalk.cyan(`\n🔗 Public URL: ${registerData.url || `https://community.vigthoria.io/showcase/${registerData.project?.id || registerData.projectId}`}`));
364
465
  }
365
- console.log(chalk.gray('\nTip: Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
466
+ console.log(chalk.gray('\nStored in Vigthoria Community. Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
366
467
  }
367
468
  catch (error) {
368
469
  spinner.stop();
369
470
  this.logger.error('Push failed');
471
+ if (error instanceof RepositoryApiError && error.payload?.code === 'REPOSITORY_SECURITY_REVIEW') {
472
+ this.printSecurityReview({
473
+ reviewId: error.payload.reviewId,
474
+ status: error.payload.status || 'pending_review',
475
+ riskScore: error.payload.riskScore,
476
+ findings: error.payload.findings,
477
+ actionItems: error.payload.actionItems,
478
+ retryCommand: error.payload.retryCommand,
479
+ });
480
+ }
370
481
  const errMsg = this.formatRepoError(error);
371
482
  console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
372
483
  }
373
484
  }
485
+ /**
486
+ * Show the current user's repository security review queue or one review.
487
+ */
488
+ async review(reviewId, options = {}) {
489
+ this.requireAuth();
490
+ const spinner = createSpinner(reviewId ? 'Loading security review...' : 'Loading security reviews...').start();
491
+ try {
492
+ const apiPath = reviewId
493
+ ? `/api/repo/security-reviews/${encodeURIComponent(reviewId)}`
494
+ : '/api/repo/security-reviews?mine=true';
495
+ const response = await this.repoFetch(apiPath, { method: 'GET' });
496
+ const payload = await readResponsePayload(response);
497
+ if (!response.ok) {
498
+ throw new RepositoryApiError(response.status, payload, 'Failed to load repository security review');
499
+ }
500
+ spinner.stop();
501
+ if (options.json) {
502
+ console.log(JSON.stringify(reviewId ? payload.review : payload.reviews, null, 2));
503
+ return;
504
+ }
505
+ if (reviewId) {
506
+ this.printSecurityReview(payload.review);
507
+ return;
508
+ }
509
+ const reviews = (payload.reviews || []);
510
+ if (reviews.length === 0) {
511
+ console.log(chalk.green('\n✓ No repository security reviews found.\n'));
512
+ return;
513
+ }
514
+ console.log(chalk.cyan(`\nRepository security reviews (${reviews.length})`));
515
+ for (const review of reviews) {
516
+ console.log(chalk.gray(` ${review.reviewId} ${review.status} ${review.projectName || 'repository'}`));
517
+ }
518
+ console.log(chalk.gray('\nUse `vigthoria repo review <review-id>` for findings and repair steps.\n'));
519
+ }
520
+ catch (error) {
521
+ spinner.stop();
522
+ console.log(chalk.red(`\n❌ Error: ${this.formatRepoError(error)}\n`));
523
+ }
524
+ }
374
525
  /**
375
526
  * Pull a project from Vigthoria Repository
376
527
  */
@@ -384,7 +535,7 @@ export class RepoCommand {
384
535
  body: JSON.stringify({ projectId: repo.id })
385
536
  });
386
537
  if (!response.ok) {
387
- const error = await response.json().catch(async () => ({ error: await response.text() }));
538
+ const error = await readResponsePayload(response);
388
539
  throw new Error(error.error || error.message || 'Project not found');
389
540
  }
390
541
  const data = await response.json();
@@ -478,14 +629,14 @@ export class RepoCommand {
478
629
  };
479
630
  spinner.stop();
480
631
  if (data.projects.length === 0) {
481
- console.log(chalk.yellow('\n📦 No projects in your Vigthoria Repo yet.\n'));
632
+ console.log(chalk.yellow('\n📦 No projects in your Vigthoria Community Repository yet.\n'));
482
633
  console.log(chalk.gray('Use `vigthoria repo push` to upload your first project.\n'));
483
634
  return;
484
635
  }
485
636
  // Separate owned and shared projects
486
637
  const ownedProjects = data.owned || data.projects.filter((p) => p.access_level === 'owner');
487
638
  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`));
639
+ console.log(chalk.cyan(`\n📦 Your Vigthoria Community Repository (${data.total} project${data.total !== 1 ? 's' : ''})\n`));
489
640
  // Display owned projects grouped by visibility
490
641
  if (ownedProjects.length > 0) {
491
642
  console.log(chalk.bold.cyan(' 📁 YOUR PROJECTS'));
@@ -572,8 +723,8 @@ export class RepoCommand {
572
723
  headers: this.getAuthHeaders()
573
724
  });
574
725
  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'));
726
+ spinner.succeed('Project not tracked in Vigthoria Community Repository');
727
+ console.log(chalk.yellow('\n📦 Current project is not synced with Vigthoria Community Repository.\n'));
577
728
  console.log(chalk.gray('Use `vigthoria repo push` to upload it.\n'));
578
729
  return;
579
730
  }
@@ -650,7 +801,7 @@ export class RepoCommand {
650
801
  const { confirm } = await inquirer.prompt([{
651
802
  type: 'confirm',
652
803
  name: 'confirm',
653
- message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Repo?`),
804
+ message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Community Repository?`),
654
805
  default: false
655
806
  }]);
656
807
  if (!confirm) {
@@ -667,7 +818,7 @@ export class RepoCommand {
667
818
  const error = await response.json();
668
819
  throw new Error(error.error || 'Failed to delete project');
669
820
  }
670
- spinner.succeed(chalk.green('Project deleted from Vigthoria Repo'));
821
+ spinner.succeed(chalk.green('Project deleted from Vigthoria Community Repository'));
671
822
  console.log(chalk.gray('\nNote: Your local files are not affected.\n'));
672
823
  }
673
824
  catch (error) {
@@ -704,7 +855,7 @@ export class RepoCommand {
704
855
  })
705
856
  });
706
857
  if (!response.ok) {
707
- const err = await response.json().catch(async () => ({ error: await response.text() }));
858
+ const err = await readResponsePayload(response);
708
859
  throw new Error(err.error || `Engine import failed (${response.status})`);
709
860
  }
710
861
  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
  }