vigthoria-cli 1.6.1 → 1.6.4

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 (46) hide show
  1. package/README.md +52 -1
  2. package/dist/commands/chat.d.ts +31 -45
  3. package/dist/commands/chat.d.ts.map +1 -1
  4. package/dist/commands/chat.js +374 -855
  5. package/dist/commands/chat.js.map +1 -1
  6. package/dist/commands/repo.d.ts +10 -0
  7. package/dist/commands/repo.d.ts.map +1 -1
  8. package/dist/commands/repo.js +215 -97
  9. package/dist/commands/repo.js.map +1 -1
  10. package/dist/index.js +32 -4
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/api.d.ts +8 -0
  13. package/dist/utils/api.d.ts.map +1 -1
  14. package/dist/utils/api.js +183 -42
  15. package/dist/utils/api.js.map +1 -1
  16. package/dist/utils/config.d.ts.map +1 -1
  17. package/dist/utils/config.js +2 -1
  18. package/dist/utils/config.js.map +1 -1
  19. package/dist/utils/tools.d.ts +3 -0
  20. package/dist/utils/tools.d.ts.map +1 -1
  21. package/dist/utils/tools.js +252 -14
  22. package/dist/utils/tools.js.map +1 -1
  23. package/package.json +13 -2
  24. package/install.ps1 +0 -290
  25. package/install.sh +0 -307
  26. package/src/commands/auth.ts +0 -226
  27. package/src/commands/chat.ts +0 -1101
  28. package/src/commands/config.ts +0 -306
  29. package/src/commands/deploy.ts +0 -609
  30. package/src/commands/edit.ts +0 -310
  31. package/src/commands/explain.ts +0 -115
  32. package/src/commands/generate.ts +0 -222
  33. package/src/commands/hub.ts +0 -382
  34. package/src/commands/repo.ts +0 -742
  35. package/src/commands/review.ts +0 -186
  36. package/src/index.ts +0 -601
  37. package/src/types/marked-terminal.d.ts +0 -31
  38. package/src/utils/api.ts +0 -526
  39. package/src/utils/config.ts +0 -241
  40. package/src/utils/files.ts +0 -273
  41. package/src/utils/logger.ts +0 -130
  42. package/src/utils/session.ts +0 -179
  43. package/src/utils/tools.ts +0 -1964
  44. package/test-parse.js +0 -105
  45. package/test-parse2.js +0 -35
  46. package/tsconfig.json +0 -20
@@ -1,241 +0,0 @@
1
- /**
2
- * Configuration Manager for Vigthoria CLI
3
- */
4
-
5
- import Conf from 'conf';
6
- import * as path from 'path';
7
- import * as os from 'os';
8
-
9
- export interface VigthoriaCLIConfig {
10
- apiUrl: string;
11
- modelsApiUrl: string; // Direct Vigthoria Models API
12
- wsUrl: string;
13
- authToken: string | null;
14
- refreshToken: string | null;
15
- userId: string | null;
16
- email: string | null;
17
- subscription: {
18
- plan: string | null;
19
- status: string | null;
20
- expiresAt: string | null;
21
- };
22
- preferences: {
23
- defaultModel: string;
24
- theme: 'dark' | 'light';
25
- autoApplyFixes: boolean;
26
- showDiffs: boolean;
27
- contextLines: number;
28
- maxTokens: number;
29
- };
30
- project: {
31
- rootPath: string | null;
32
- ignorePatterns: string[];
33
- };
34
- }
35
-
36
- const defaultConfig: VigthoriaCLIConfig = {
37
- apiUrl: 'https://coder.vigthoria.io',
38
- modelsApiUrl: 'https://api.vigthoria.io', // Direct AI Models API - no proxying through Coder
39
- wsUrl: 'wss://coder.vigthoria.io/ws',
40
- authToken: null,
41
- refreshToken: null,
42
- userId: null,
43
- email: null,
44
- subscription: {
45
- plan: null,
46
- status: null,
47
- expiresAt: null,
48
- },
49
- preferences: {
50
- defaultModel: 'vigthoria-code',
51
- theme: 'dark',
52
- autoApplyFixes: false,
53
- showDiffs: true,
54
- contextLines: 3,
55
- maxTokens: 4096,
56
- },
57
- project: {
58
- rootPath: null,
59
- ignorePatterns: [
60
- 'node_modules',
61
- '.git',
62
- 'dist',
63
- 'build',
64
- '__pycache__',
65
- '.venv',
66
- 'venv',
67
- '.env',
68
- '*.log',
69
- ],
70
- },
71
- };
72
-
73
- export class Config {
74
- private store: Conf<VigthoriaCLIConfig>;
75
-
76
- constructor() {
77
- this.store = new Conf<VigthoriaCLIConfig>({
78
- projectName: 'vigthoria-cli',
79
- defaults: defaultConfig,
80
- schema: {
81
- apiUrl: { type: 'string' },
82
- modelsApiUrl: { type: 'string' },
83
- wsUrl: { type: 'string' },
84
- authToken: { type: ['string', 'null'] },
85
- refreshToken: { type: ['string', 'null'] },
86
- userId: { type: ['string', 'null'] },
87
- email: { type: ['string', 'null'] },
88
- subscription: {
89
- type: 'object',
90
- properties: {
91
- plan: { type: ['string', 'null'] },
92
- status: { type: ['string', 'null'] },
93
- expiresAt: { type: ['string', 'null'] },
94
- },
95
- },
96
- preferences: {
97
- type: 'object',
98
- properties: {
99
- defaultModel: { type: 'string' },
100
- theme: { type: 'string', enum: ['dark', 'light'] },
101
- autoApplyFixes: { type: 'boolean' },
102
- showDiffs: { type: 'boolean' },
103
- contextLines: { type: 'number' },
104
- maxTokens: { type: 'number' },
105
- },
106
- },
107
- project: {
108
- type: 'object',
109
- properties: {
110
- rootPath: { type: ['string', 'null'] },
111
- ignorePatterns: { type: 'array', items: { type: 'string' } },
112
- },
113
- },
114
- },
115
- });
116
- }
117
-
118
- get<K extends keyof VigthoriaCLIConfig>(key: K): VigthoriaCLIConfig[K] {
119
- return this.store.get(key);
120
- }
121
-
122
- set<K extends keyof VigthoriaCLIConfig>(key: K, value: VigthoriaCLIConfig[K]): void {
123
- this.store.set(key, value);
124
- }
125
-
126
- getAll(): VigthoriaCLIConfig {
127
- return this.store.store;
128
- }
129
-
130
- reset(): void {
131
- this.store.clear();
132
- }
133
-
134
- isAuthenticated(): boolean {
135
- return this.store.get('authToken') !== null;
136
- }
137
-
138
- hasValidSubscription(): boolean {
139
- const sub = this.store.get('subscription');
140
- if (!sub.status || sub.status !== 'active') return false;
141
- if (sub.expiresAt) {
142
- const expires = new Date(sub.expiresAt);
143
- return expires > new Date();
144
- }
145
- return true;
146
- }
147
-
148
- getConfigPath(): string {
149
- return this.store.path;
150
- }
151
-
152
- // Auth helpers
153
- setAuth(data: {
154
- token: string;
155
- refreshToken?: string;
156
- userId: string;
157
- email: string;
158
- }): void {
159
- this.store.set('authToken', data.token);
160
- if (data.refreshToken) {
161
- this.store.set('refreshToken', data.refreshToken);
162
- }
163
- this.store.set('userId', data.userId);
164
- this.store.set('email', data.email);
165
- }
166
-
167
- clearAuth(): void {
168
- this.store.set('authToken', null);
169
- this.store.set('refreshToken', null);
170
- this.store.set('userId', null);
171
- this.store.set('email', null);
172
- this.store.set('subscription', {
173
- plan: null,
174
- status: null,
175
- expiresAt: null,
176
- });
177
- }
178
-
179
- setSubscription(data: {
180
- plan: string;
181
- status: string;
182
- expiresAt?: string;
183
- }): void {
184
- this.store.set('subscription', {
185
- plan: data.plan,
186
- status: data.status,
187
- expiresAt: data.expiresAt || null,
188
- });
189
- }
190
-
191
- // Model selection - Vigthoria Models (internal: qwen3-coder, deepseek via OpenRouter)
192
- getAvailableModels(): { id: string; name: string; description: string; tier: string }[] {
193
- const sub = this.store.get('subscription');
194
- const plan = (sub.plan || '').toLowerCase();
195
-
196
- // ═══════════════════════════════════════════════════════════════
197
- // VIGTHORIA LOCAL - Self-hosted models (fast, no API costs)
198
- // ═══════════════════════════════════════════════════════════════
199
- const models: { id: string; name: string; description: string; tier: string }[] = [
200
- { id: 'code', name: 'Vigthoria v3 Code 30B', description: '🏠 30B coding powerhouse - default', tier: 'local' },
201
- { id: 'code-30b', name: 'Vigthoria v3 Code 30B', description: '🏠 Same as code', tier: 'local' },
202
- { id: 'code-8b', name: 'Vigthoria v2 Code 8B', description: '🏠 Lighter 8B model', tier: 'local' },
203
- { id: 'balanced', name: 'Vigthoria Balanced 4B', description: '🏠 General purpose', tier: 'local' },
204
- { id: 'fast', name: 'Vigthoria Fast 1.7B', description: '🏠 Quick responses', tier: 'local' },
205
- { id: 'creative', name: 'Vigthoria Creative 9B', description: '🏠 Creative writing', tier: 'local' },
206
- ];
207
-
208
- // ═══════════════════════════════════════════════════════════════
209
- // VIGTHORIA CLOUD - Premium cloud models (Pro subscription)
210
- // For complex multi-file tasks, large refactoring, architecture
211
- // ═══════════════════════════════════════════════════════════════
212
- const proPlansList = ['developer', 'pro', 'ultra', 'enterprise', 'master_admin', 'admin'];
213
- if (proPlansList.includes(plan)) {
214
- models.push(
215
- { id: 'cloud', name: 'Vigthoria Cloud 671B', description: '☁️ 671B cloud model - complex tasks', tier: 'cloud' },
216
- { id: 'cloud-reason', name: 'Vigthoria Cloud Reason', description: '☁️ Reasoning expert model', tier: 'cloud' },
217
- { id: 'ultra', name: 'Vigthoria Ultra', description: '☁️ Maximum capability', tier: 'cloud' },
218
- );
219
- }
220
-
221
- return models;
222
- }
223
-
224
- // Check if a model is a "Cloud" tier model
225
- isCloudModel(modelId: string): boolean {
226
- const cloudModels = ['cloud', 'cloud-reason', 'ultra', 'agent'];
227
- return cloudModels.includes(modelId) || modelId.includes('cloud');
228
- }
229
-
230
- // Check if task is complex enough to suggest Cloud upgrade
231
- isComplexTask(prompt: string): boolean {
232
- const complexIndicators = [
233
- /refactor/i, /architect/i, /redesign/i, /migrate/i,
234
- /multi.?file/i, /multiple files/i, /entire.*project/i,
235
- /all files/i, /whole.*codebase/i, /implement.*feature/i,
236
- /create.*system/i, /build.*from.*scratch/i,
237
- /analyze.*project/i, /review.*codebase/i,
238
- ];
239
- return complexIndicators.some(pattern => pattern.test(prompt));
240
- }
241
- }
@@ -1,273 +0,0 @@
1
- /**
2
- * File utilities for Vigthoria CLI
3
- */
4
-
5
- import * as fs from 'fs';
6
- import * as path from 'path';
7
- import { glob } from 'glob';
8
-
9
- export interface FileInfo {
10
- path: string;
11
- relativePath: string;
12
- content: string;
13
- language: string;
14
- lines: number;
15
- }
16
-
17
- export class FileUtils {
18
- private projectRoot: string;
19
- private ignorePatterns: string[];
20
-
21
- constructor(projectRoot: string, ignorePatterns: string[] = []) {
22
- // Resolve symlinks and normalize path for consistent behavior
23
- try {
24
- this.projectRoot = fs.realpathSync(projectRoot);
25
- } catch {
26
- // If realpath fails (e.g., path doesn't exist), use the original
27
- this.projectRoot = path.resolve(projectRoot);
28
- }
29
- this.ignorePatterns = ignorePatterns;
30
- }
31
-
32
- // Read file with metadata
33
- readFile(filePath: string): FileInfo | null {
34
- try {
35
- let absolutePath = path.isAbsolute(filePath)
36
- ? filePath
37
- : path.join(this.projectRoot, filePath);
38
-
39
- // Resolve symlinks for consistent path handling
40
- try {
41
- absolutePath = fs.realpathSync(absolutePath);
42
- } catch {
43
- // Keep original path if realpath fails
44
- }
45
-
46
- // Check if it's a regular file (not a directory or special file)
47
- const stats = fs.statSync(absolutePath);
48
- if (!stats.isFile()) {
49
- return null;
50
- }
51
-
52
- const content = fs.readFileSync(absolutePath, 'utf-8');
53
- const relativePath = path.relative(this.projectRoot, absolutePath);
54
-
55
- return {
56
- path: absolutePath,
57
- relativePath,
58
- content,
59
- language: this.detectLanguage(absolutePath),
60
- lines: content.split('\n').length,
61
- };
62
- } catch (error) {
63
- return null;
64
- }
65
- }
66
-
67
- // Read specific lines
68
- readLines(filePath: string, start: number, end: number): string | null {
69
- const file = this.readFile(filePath);
70
- if (!file) return null;
71
-
72
- const lines = file.content.split('\n');
73
- return lines.slice(start - 1, end).join('\n');
74
- }
75
-
76
- // Write file
77
- writeFile(filePath: string, content: string): boolean {
78
- try {
79
- const absolutePath = path.isAbsolute(filePath)
80
- ? filePath
81
- : path.join(this.projectRoot, filePath);
82
-
83
- // Create directory if needed
84
- const dir = path.dirname(absolutePath);
85
- if (!fs.existsSync(dir)) {
86
- fs.mkdirSync(dir, { recursive: true });
87
- }
88
-
89
- fs.writeFileSync(absolutePath, content, 'utf-8');
90
- return true;
91
- } catch {
92
- return false;
93
- }
94
- }
95
-
96
- // Backup file
97
- backupFile(filePath: string): string | null {
98
- const file = this.readFile(filePath);
99
- if (!file) return null;
100
-
101
- const backupPath = `${file.path}.bak.${Date.now()}`;
102
- if (this.writeFile(backupPath, file.content)) {
103
- return backupPath;
104
- }
105
- return null;
106
- }
107
-
108
- // List project files
109
- async listFiles(pattern: string = '**/*'): Promise<string[]> {
110
- try {
111
- // Check if project root exists
112
- if (!fs.existsSync(this.projectRoot)) {
113
- return [];
114
- }
115
-
116
- const files = await glob(pattern, {
117
- cwd: this.projectRoot,
118
- ignore: this.ignorePatterns,
119
- nodir: true,
120
- });
121
-
122
- return files;
123
- } catch (error) {
124
- // Return empty array on error (e.g., permission issues on Windows)
125
- return [];
126
- }
127
- }
128
-
129
- // Detect language from file extension
130
- detectLanguage(filePath: string): string {
131
- const ext = path.extname(filePath).toLowerCase();
132
- const languageMap: Record<string, string> = {
133
- '.ts': 'typescript',
134
- '.tsx': 'typescript',
135
- '.js': 'javascript',
136
- '.jsx': 'javascript',
137
- '.py': 'python',
138
- '.rs': 'rust',
139
- '.go': 'go',
140
- '.java': 'java',
141
- '.c': 'c',
142
- '.cpp': 'cpp',
143
- '.h': 'c',
144
- '.hpp': 'cpp',
145
- '.cs': 'csharp',
146
- '.rb': 'ruby',
147
- '.php': 'php',
148
- '.swift': 'swift',
149
- '.kt': 'kotlin',
150
- '.scala': 'scala',
151
- '.sql': 'sql',
152
- '.html': 'html',
153
- '.css': 'css',
154
- '.scss': 'scss',
155
- '.less': 'less',
156
- '.json': 'json',
157
- '.yaml': 'yaml',
158
- '.yml': 'yaml',
159
- '.xml': 'xml',
160
- '.md': 'markdown',
161
- '.sh': 'bash',
162
- '.bash': 'bash',
163
- '.zsh': 'zsh',
164
- '.dockerfile': 'dockerfile',
165
- '.vue': 'vue',
166
- '.svelte': 'svelte',
167
- };
168
-
169
- return languageMap[ext] || 'text';
170
- }
171
-
172
- // Get project context
173
- async getProjectContext(maxFiles: number = 20): Promise<{
174
- type: string;
175
- files: string[];
176
- dependencies: Record<string, string>;
177
- }> {
178
- const context = {
179
- type: 'unknown',
180
- files: [] as string[],
181
- dependencies: {} as Record<string, string>,
182
- };
183
-
184
- // Detect project type
185
- if (fs.existsSync(path.join(this.projectRoot, 'package.json'))) {
186
- context.type = 'node';
187
- const pkg = JSON.parse(fs.readFileSync(
188
- path.join(this.projectRoot, 'package.json'),
189
- 'utf-8'
190
- ));
191
- context.dependencies = {
192
- ...pkg.dependencies,
193
- ...pkg.devDependencies,
194
- };
195
- } else if (fs.existsSync(path.join(this.projectRoot, 'requirements.txt'))) {
196
- context.type = 'python';
197
- } else if (fs.existsSync(path.join(this.projectRoot, 'Cargo.toml'))) {
198
- context.type = 'rust';
199
- } else if (fs.existsSync(path.join(this.projectRoot, 'go.mod'))) {
200
- context.type = 'go';
201
- } else if (fs.existsSync(path.join(this.projectRoot, 'pom.xml'))) {
202
- context.type = 'java-maven';
203
- } else if (fs.existsSync(path.join(this.projectRoot, 'build.gradle'))) {
204
- context.type = 'java-gradle';
205
- } else if (
206
- fs.existsSync(path.join(this.projectRoot, 'core', 'core.services.yml')) ||
207
- fs.existsSync(path.join(this.projectRoot, 'web', 'core', 'core.services.yml')) ||
208
- fs.existsSync(path.join(this.projectRoot, 'sites', 'default', 'settings.php'))
209
- ) {
210
- // Drupal 8/9/10/11 detection
211
- context.type = 'drupal';
212
- // Try to read composer.json for Drupal version info
213
- const composerPath = fs.existsSync(path.join(this.projectRoot, 'composer.json'))
214
- ? path.join(this.projectRoot, 'composer.json')
215
- : fs.existsSync(path.join(this.projectRoot, 'web', 'composer.json'))
216
- ? path.join(this.projectRoot, 'web', 'composer.json')
217
- : null;
218
- if (composerPath) {
219
- try {
220
- const composer = JSON.parse(fs.readFileSync(composerPath, 'utf-8'));
221
- context.dependencies = composer.require || {};
222
- } catch { /* ignore parse errors */ }
223
- }
224
- } else if (fs.existsSync(path.join(this.projectRoot, 'wp-config.php'))) {
225
- context.type = 'wordpress';
226
- } else if (fs.existsSync(path.join(this.projectRoot, 'artisan'))) {
227
- context.type = 'laravel';
228
- } else if (fs.existsSync(path.join(this.projectRoot, 'composer.json'))) {
229
- context.type = 'php-composer';
230
- try {
231
- const composer = JSON.parse(fs.readFileSync(
232
- path.join(this.projectRoot, 'composer.json'),
233
- 'utf-8'
234
- ));
235
- context.dependencies = composer.require || {};
236
- } catch { /* ignore parse errors */ }
237
- }
238
-
239
- // Get relevant files
240
- const allFiles = await this.listFiles();
241
- context.files = allFiles.slice(0, maxFiles);
242
-
243
- return context;
244
- }
245
-
246
- // Create diff
247
- createDiff(original: string, modified: string): { added: string[]; removed: string[] } {
248
- const originalLines = original.split('\n');
249
- const modifiedLines = modified.split('\n');
250
-
251
- const added: string[] = [];
252
- const removed: string[] = [];
253
-
254
- // Simple diff - for display purposes
255
- const maxLen = Math.max(originalLines.length, modifiedLines.length);
256
-
257
- for (let i = 0; i < maxLen; i++) {
258
- const origLine = originalLines[i];
259
- const modLine = modifiedLines[i];
260
-
261
- if (origLine !== modLine) {
262
- if (origLine !== undefined) {
263
- removed.push(`${i + 1}: ${origLine}`);
264
- }
265
- if (modLine !== undefined) {
266
- added.push(`${i + 1}: ${modLine}`);
267
- }
268
- }
269
- }
270
-
271
- return { added, removed };
272
- }
273
- }
@@ -1,130 +0,0 @@
1
- /**
2
- * Logger utility for Vigthoria CLI
3
- */
4
-
5
- import chalk from 'chalk';
6
-
7
- export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
8
-
9
- export class Logger {
10
- private verbose: boolean = false;
11
-
12
- setVerbose(verbose: boolean): void {
13
- this.verbose = verbose;
14
- }
15
-
16
- debug(...args: unknown[]): void {
17
- if (this.verbose) {
18
- console.log(chalk.gray('[DEBUG]'), ...args);
19
- }
20
- }
21
-
22
- info(...args: unknown[]): void {
23
- console.log(chalk.blue('ℹ'), ...args);
24
- }
25
-
26
- warn(...args: unknown[]): void {
27
- console.log(chalk.yellow('⚠'), ...args);
28
- }
29
-
30
- error(...args: unknown[]): void {
31
- console.error(chalk.red('✗'), ...args);
32
- }
33
-
34
- success(...args: unknown[]): void {
35
- console.log(chalk.green('✓'), ...args);
36
- }
37
-
38
- // AI response formatting
39
- ai(message: string): void {
40
- console.log(chalk.cyan('🤖'), message);
41
- }
42
-
43
- // User input formatting
44
- user(message: string): void {
45
- console.log(chalk.white('👤'), message);
46
- }
47
-
48
- // Code block
49
- code(code: string, language?: string): void {
50
- console.log(chalk.gray('─'.repeat(60)));
51
- console.log(chalk.yellow(code));
52
- console.log(chalk.gray('─'.repeat(60)));
53
- }
54
-
55
- // Diff output
56
- diff(added: string[], removed: string[]): void {
57
- removed.forEach(line => console.log(chalk.red(`- ${line}`)));
58
- added.forEach(line => console.log(chalk.green(`+ ${line}`)));
59
- }
60
-
61
- // Section header
62
- section(title: string): void {
63
- console.log();
64
- console.log(chalk.bold.cyan(`═══ ${title} ═══`));
65
- console.log();
66
- }
67
-
68
- // Progress
69
- progress(message: string): void {
70
- process.stdout.write(chalk.gray(`${message}\r`));
71
- }
72
-
73
- // Clear line
74
- clearLine(): void {
75
- process.stdout.write('\r\x1b[K');
76
- }
77
-
78
- // Box output
79
- box(content: string, title?: string): void {
80
- const lines = content.split('\n');
81
- // Strip ANSI codes when calculating length for proper alignment
82
- const stripAnsi = (str: string) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
83
- const maxLen = Math.max(...lines.map(l => stripAnsi(l).length), title?.length || 0);
84
- const width = maxLen + 4;
85
-
86
- console.log(chalk.cyan('┌' + '─'.repeat(width - 2) + '┐'));
87
- if (title) {
88
- console.log(chalk.cyan('│ ') + chalk.bold.white(title.padEnd(width - 4)) + chalk.cyan(' │'));
89
- console.log(chalk.cyan('├' + '─'.repeat(width - 2) + '┤'));
90
- }
91
- lines.forEach(line => {
92
- // Calculate visible length and pad accordingly
93
- const visibleLen = stripAnsi(line).length;
94
- const padding = ' '.repeat(Math.max(0, width - 4 - visibleLen));
95
- console.log(chalk.cyan('│ ') + line + padding + chalk.cyan(' │'));
96
- });
97
- console.log(chalk.cyan('└' + '─'.repeat(width - 2) + '┘'));
98
- }
99
-
100
- // Table output
101
- table(headers: string[], rows: string[][]): void {
102
- const colWidths = headers.map((h, i) => {
103
- const maxData = Math.max(...rows.map(r => (r[i] || '').length));
104
- return Math.max(h.length, maxData);
105
- });
106
-
107
- // Header
108
- console.log(
109
- chalk.gray('│ ') +
110
- headers.map((h, i) => chalk.bold(h.padEnd(colWidths[i]))).join(chalk.gray(' │ ')) +
111
- chalk.gray(' │')
112
- );
113
-
114
- // Separator
115
- console.log(
116
- chalk.gray('├─') +
117
- colWidths.map(w => '─'.repeat(w)).join(chalk.gray('─┼─')) +
118
- chalk.gray('─┤')
119
- );
120
-
121
- // Rows
122
- rows.forEach(row => {
123
- console.log(
124
- chalk.gray('│ ') +
125
- row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(chalk.gray(' │ ')) +
126
- chalk.gray(' │')
127
- );
128
- });
129
- }
130
- }