@veestack-tools/cli 3.0.2 → 3.0.3

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,219 +0,0 @@
1
- import { glob } from 'glob';
2
- import { readFileSync, statSync, readdirSync } from 'fs';
3
- import { join, resolve } from 'path';
4
- import type { Snapshot, FileNode, DependencyNode } from '@veestack-tools/types';
5
- import crypto from 'crypto';
6
- import ora from 'ora';
7
-
8
- export async function scanCommand(options: {
9
- path: string;
10
- output: string;
11
- }): Promise<void> {
12
- const spinner = ora({
13
- text: 'Scanning project...',
14
- spinner: 'dots',
15
- stream: process.stdout
16
- }).start();
17
-
18
- try {
19
- const snapshot = await generateSnapshot(options.path);
20
-
21
- spinner.succeed(`Scanned ${snapshot.metadata.total_files} files`);
22
-
23
- // Save to file
24
- const fs = await import('fs/promises');
25
- await fs.writeFile(options.output, JSON.stringify(snapshot, null, 2));
26
-
27
- console.log(`\n✅ Snapshot saved to ${options.output}`);
28
- } catch (error) {
29
- spinner.fail('Scan failed');
30
- console.error(error);
31
- process.exit(1);
32
- }
33
- }
34
-
35
- async function generateSnapshot(projectPath: string): Promise<Snapshot> {
36
- const files = await scanFiles(projectPath);
37
- const dependencies = await scanDependencies(projectPath);
38
-
39
- const metadata = {
40
- total_files: files.length,
41
- total_dependencies: dependencies.length,
42
- total_size_bytes: files.reduce((sum, f) => sum + f.size_bytes, 0),
43
- total_lines: files.reduce((sum, f) => sum + (f.estimated_lines || 0), 0),
44
- max_directory_depth: Math.max(...files.map((f) => f.depth), 0),
45
- language_breakdown: calculateLanguageBreakdown(files),
46
- };
47
-
48
- return {
49
- snapshot_version: '1.0.0',
50
- engine_target_version: '1.0.0',
51
- project_id: crypto.randomUUID(),
52
- generated_at: new Date().toISOString(),
53
- root_path_hash: hashString(projectPath),
54
- project_root: resolve(projectPath), // Use absolute path for content scanning
55
- metadata,
56
- files,
57
- dependencies,
58
- };
59
- }
60
-
61
- async function scanFiles(projectPath: string): Promise<FileNode[]> {
62
- const files: FileNode[] = [];
63
- const patterns = ['**/*'];
64
-
65
- const filePaths = await glob(patterns, {
66
- cwd: projectPath,
67
- ignore: [
68
- '**/node_modules/**',
69
- '**/.git/**',
70
- '**/dist/**',
71
- '**/build/**',
72
- '**/.next/**',
73
- '**/out/**',
74
- '**/coverage/**',
75
- ],
76
- absolute: false,
77
- });
78
-
79
- for (const filePath of filePaths) {
80
- const fullPath = join(projectPath, filePath);
81
- const stats = statSync(fullPath);
82
-
83
- if (stats.isFile()) {
84
- const fileNode: FileNode = {
85
- id: crypto.randomUUID(),
86
- path: filePath, // Actual relative path for rule evaluation
87
- path_hash: hashString(filePath),
88
- depth: filePath.split(/[\\/]/).length,
89
- size_bytes: stats.size,
90
- estimated_lines: Math.floor(stats.size / 50), // Rough estimate
91
- extension: filePath.split('.').pop() || '',
92
- is_binary: isBinaryFile(filePath),
93
- };
94
-
95
- files.push(fileNode);
96
- }
97
- }
98
-
99
- // Sort deterministically by path_hash
100
- return files.sort((a, b) => a.path_hash.localeCompare(b.path_hash));
101
- }
102
-
103
- async function scanDependencies(projectPath: string): Promise<DependencyNode[]> {
104
- const dependencies: DependencyNode[] = [];
105
-
106
- try {
107
- const packageJsonPath = join(projectPath, 'package.json');
108
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
109
-
110
- const processDeps = (
111
- deps: Record<string, string>,
112
- category: 'dev' | 'prod' | 'peer'
113
- ) => {
114
- for (const [name, version] of Object.entries(deps)) {
115
- const versionMatch = version.match(/^(\d+)\.(\d+)/);
116
- const major = versionMatch ? parseInt(versionMatch[1]) : 0;
117
- const minor = versionMatch ? parseInt(versionMatch[2]) : 0;
118
-
119
- dependencies.push({
120
- id: crypto.randomUUID(),
121
- name_hash: hashString(name),
122
- major_version: major,
123
- minor_version: minor,
124
- category,
125
- });
126
- }
127
- };
128
-
129
- if (packageJson.dependencies) {
130
- processDeps(packageJson.dependencies, 'prod');
131
- }
132
-
133
- if (packageJson.devDependencies) {
134
- processDeps(packageJson.devDependencies, 'dev');
135
- }
136
-
137
- if (packageJson.peerDependencies) {
138
- processDeps(packageJson.peerDependencies, 'peer');
139
- }
140
- } catch (error) {
141
- // No package.json found
142
- }
143
-
144
- // Sort deterministically by name_hash
145
- return dependencies.sort((a, b) => a.name_hash.localeCompare(b.name_hash));
146
- }
147
-
148
- function calculateLanguageBreakdown(files: FileNode[]): Record<string, number> {
149
- const breakdown: Record<string, number> = {};
150
-
151
- for (const file of files) {
152
- const ext = file.extension.toLowerCase();
153
- const lang = getLanguageFromExtension(ext);
154
- breakdown[lang] = (breakdown[lang] || 0) + 1;
155
- }
156
-
157
- return breakdown;
158
- }
159
-
160
- function getLanguageFromExtension(ext: string): string {
161
- const map: Record<string, string> = {
162
- ts: 'TypeScript',
163
- js: 'JavaScript',
164
- tsx: 'TypeScript',
165
- jsx: 'JavaScript',
166
- py: 'Python',
167
- go: 'Go',
168
- rs: 'Rust',
169
- java: 'Java',
170
- kt: 'Kotlin',
171
- rb: 'Ruby',
172
- php: 'PHP',
173
- cs: 'C#',
174
- cpp: 'C++',
175
- c: 'C',
176
- h: 'C',
177
- json: 'JSON',
178
- yaml: 'YAML',
179
- yml: 'YAML',
180
- md: 'Markdown',
181
- html: 'HTML',
182
- css: 'CSS',
183
- scss: 'SCSS',
184
- sql: 'SQL',
185
- sh: 'Shell',
186
- };
187
-
188
- return map[ext] || 'Other';
189
- }
190
-
191
- function isBinaryFile(filePath: string): boolean {
192
- const binaryExtensions = [
193
- '.exe',
194
- '.dll',
195
- '.so',
196
- '.dylib',
197
- '.bin',
198
- '.png',
199
- '.jpg',
200
- '.jpeg',
201
- '.gif',
202
- '.pdf',
203
- '.zip',
204
- '.tar',
205
- '.gz',
206
- '.7z',
207
- '.woff',
208
- '.woff2',
209
- '.ttf',
210
- '.eot',
211
- ];
212
-
213
- const ext = filePath.split('.').pop()?.toLowerCase();
214
- return ext ? binaryExtensions.includes('.' + ext) : false;
215
- }
216
-
217
- function hashString(str: string): string {
218
- return crypto.createHash('sha256').update(str).digest('hex');
219
- }
@@ -1,241 +0,0 @@
1
- import { readFileSync } from 'fs';
2
- import ora from 'ora';
3
- import chalk from 'chalk';
4
- import readline from 'readline';
5
-
6
- const supabaseUrl = process.env.SUPABASE_URL || 'https://qhonrrojtqklvlkvfswb.supabase.co';
7
- const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || ''; // Must be set in environment
8
-
9
- // Fetch projects from Supabase
10
- async function fetchProjects(): Promise<any[]> {
11
- try {
12
- const res = await fetch(`${supabaseUrl}/rest/v1/projects?select=id,name,created_at&order=created_at.desc`, {
13
- headers: { 'apikey': supabaseAnonKey, 'Authorization': `Bearer ${supabaseAnonKey}` },
14
- });
15
- if (res.ok) {
16
- const data = await res.json() as any[];
17
- return data;
18
- }
19
- return [];
20
- } catch {
21
- return [];
22
- }
23
- }
24
-
25
- // Create new project in Supabase
26
- async function createProject(name: string): Promise<string | null> {
27
- try {
28
- const res = await fetch(`${supabaseUrl}/rest/v1/projects`, {
29
- method: 'POST',
30
- headers: {
31
- 'Content-Type': 'application/json',
32
- 'apikey': supabaseAnonKey,
33
- 'Authorization': `Bearer ${supabaseAnonKey}`,
34
- 'Prefer': 'return=representation',
35
- },
36
- body: JSON.stringify({
37
- name,
38
- user_id: '00000000-0000-0000-0000-000000000001', // Default user
39
- }),
40
- });
41
- if (res.ok) {
42
- const data = await res.json() as any[];
43
- return data[0]?.id || null;
44
- }
45
- return null;
46
- } catch {
47
- return null;
48
- }
49
- }
50
-
51
- // Interactive prompt
52
- function askQuestion(question: string): Promise<string> {
53
- const rl = readline.createInterface({
54
- input: process.stdin,
55
- output: process.stdout,
56
- });
57
- return new Promise((resolve) => {
58
- rl.question(question, (answer) => {
59
- rl.close();
60
- resolve(answer);
61
- });
62
- });
63
- }
64
-
65
- // Select or create project
66
- async function selectOrCreateProject(): Promise<string | null> {
67
- console.log(chalk.blue('\n📁 Loading projects...\n'));
68
-
69
- const projects = await fetchProjects();
70
-
71
- if (projects.length === 0) {
72
- console.log(chalk.yellow('No projects found.\n'));
73
- const answer = await askQuestion('Do you want to create a new project? (y/n): ');
74
- if (answer.toLowerCase() === 'y') {
75
- const name = await askQuestion('Enter project name: ');
76
- if (name.trim()) {
77
- const projectId = await createProject(name.trim());
78
- if (projectId) {
79
- console.log(chalk.green(`\n✅ Project "${name}" created successfully!`));
80
- return projectId;
81
- }
82
- }
83
- }
84
- return null;
85
- }
86
-
87
- // Display projects
88
- console.log(chalk.white('Select a project:\n'));
89
- console.log(chalk.gray('0. [+] Create new project\n'));
90
-
91
- projects.forEach((p, i) => {
92
- console.log(chalk.white(`${i + 1}. ${p.name}`));
93
- console.log(chalk.gray(` ID: ${p.id}`));
94
- console.log(chalk.gray(` Created: ${new Date(p.created_at).toLocaleDateString()}\n`));
95
- });
96
-
97
- const answer = await askQuestion('Enter number (0-' + projects.length + '): ');
98
- const choice = parseInt(answer, 10);
99
-
100
- if (choice === 0) {
101
- // Create new project
102
- const name = await askQuestion('\nEnter project name: ');
103
- if (name.trim()) {
104
- const projectId = await createProject(name.trim());
105
- if (projectId) {
106
- console.log(chalk.green(`\n✅ Project "${name}" created successfully!`));
107
- return projectId;
108
- }
109
- }
110
- return null;
111
- } else if (choice > 0 && choice <= projects.length) {
112
- // Select existing project
113
- const selected = projects[choice - 1];
114
- console.log(chalk.green(`\n✅ Selected project: "${selected.name}"`));
115
- return selected.id;
116
- } else {
117
- console.log(chalk.red('\n❌ Invalid selection'));
118
- return null;
119
- }
120
- }
121
-
122
- export async function uploadCommand(options: {
123
- file: string;
124
- projectId?: string;
125
- apiKey?: string;
126
- }): Promise<void> {
127
- const spinner = ora('Preparing upload...').start();
128
-
129
- try {
130
- const fs = await import('fs/promises');
131
- const snapshotData = await fs.readFile(options.file, 'utf-8');
132
- const snapshot = JSON.parse(snapshotData);
133
-
134
- // Determine project ID
135
- let projectId: string | undefined = options.projectId;
136
-
137
- if (!projectId) {
138
- spinner.stop();
139
- const selectedProjectId = await selectOrCreateProject();
140
- if (!selectedProjectId) {
141
- console.log(chalk.red('\n❌ No project selected. Exiting.'));
142
- process.exit(1);
143
- }
144
- projectId = selectedProjectId;
145
- spinner.start('Uploading snapshot...');
146
- }
147
-
148
- // 1. Create snapshot
149
- spinner.text = 'Creating snapshot...';
150
- const snapshotRes = await fetch(`${supabaseUrl}/rest/v1/snapshots`, {
151
- method: 'POST',
152
- headers: {
153
- 'Content-Type': 'application/json',
154
- 'apikey': supabaseAnonKey,
155
- 'Authorization': `Bearer ${supabaseAnonKey}`,
156
- 'Prefer': 'return=representation',
157
- },
158
- body: JSON.stringify({
159
- project_id: projectId,
160
- file_count: snapshot.metadata.total_files,
161
- size_mb: snapshot.metadata.total_size_bytes / (1024 * 1024),
162
- storage_path: 'local://' + projectId,
163
- }),
164
- });
165
-
166
- if (!snapshotRes.ok) {
167
- const error = await snapshotRes.json() as any;
168
- spinner.fail('Failed to create snapshot');
169
- console.error(chalk.red(`Error: ${error.message || error.details || 'Unknown error'}`));
170
- process.exit(1);
171
- }
172
-
173
- const snapshotData2 = await snapshotRes.json() as any[];
174
- const snapshotId = snapshotData2[0]?.id || snapshotData2[0]?.id;
175
-
176
- // 2. Run analysis locally
177
- spinner.text = 'Analyzing snapshot...';
178
- const { CoreEngine } = await import('@veestack-tools/engine');
179
- const { RULES } = await import('@veestack-tools/engine');
180
-
181
- const engine = new CoreEngine();
182
- RULES.forEach((rule: any) => engine.registerRule(rule));
183
- const result = await engine.analyze(snapshot);
184
-
185
- if (!result.success) {
186
- spinner.fail('Analysis failed');
187
- console.error(chalk.red('Error:', result.error));
188
- process.exit(1);
189
- }
190
-
191
- // 3. Create report
192
- spinner.text = 'Creating report...';
193
- const reportRes = await fetch(`${supabaseUrl}/rest/v1/reports`, {
194
- method: 'POST',
195
- headers: {
196
- 'Content-Type': 'application/json',
197
- 'apikey': supabaseAnonKey,
198
- 'Authorization': `Bearer ${supabaseAnonKey}`,
199
- 'Prefer': 'return=representation',
200
- },
201
- body: JSON.stringify({
202
- snapshot_id: snapshotId,
203
- score: result.report.total_score,
204
- issues_count: result.report.summary.total_findings,
205
- critical_count: result.report.summary.critical_count,
206
- high_count: result.report.summary.high_count,
207
- medium_count: result.report.summary.medium_count,
208
- low_count: result.report.summary.low_count,
209
- execution_time_ms: 0,
210
- report_json: result.report,
211
- }),
212
- });
213
-
214
- if (!reportRes.ok) {
215
- const error = await reportRes.json() as any;
216
- spinner.fail('Failed to create report');
217
- console.error(chalk.red(`Error: ${error.message || error.details || 'Unknown error'}`));
218
- process.exit(1);
219
- }
220
-
221
- const reportData = await reportRes.json() as any[];
222
- const reportId = reportData[0]?.id || reportData[0]?.id;
223
-
224
- spinner.succeed('Analysis complete');
225
-
226
- console.log(chalk.green('\n✅ Report generated and saved to VeeStack'));
227
- console.log(chalk.gray('Project ID:'), projectId);
228
- console.log(chalk.gray('Snapshot ID:'), snapshotId);
229
- console.log(chalk.gray('Report ID:'), reportId);
230
- console.log(chalk.bold('\n📊 Score:'), result.report.total_score);
231
- console.log(chalk.gray('Severity:'), result.report.severity_band);
232
- console.log(chalk.gray('Findings:'), result.report.summary.total_findings);
233
-
234
- console.log(chalk.blue('\n🔗 View your report at:'));
235
- console.log(chalk.underline(`http://localhost:3001/reports/${reportId}?project=${projectId}`));
236
- } catch (error) {
237
- spinner.fail('Upload failed');
238
- console.error(error);
239
- process.exit(1);
240
- }
241
- }
package/src/index.ts DELETED
@@ -1,36 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import { scanCommand } from './commands/scan';
5
- import { uploadCommand } from './commands/upload';
6
- import { loginCommand } from './commands/login';
7
-
8
- const program = new Command();
9
-
10
- program
11
- .name('veestack')
12
- .description('VeeStack CLI - Technical Stack Visibility Tool')
13
- .version('3.0.1');
14
-
15
- program
16
- .command('scan')
17
- .description('Scan a project directory and generate analysis')
18
- .option('-p, --path <path>', 'Path to project directory', '.')
19
- .option('-o, --output <path>', 'Output file path', 'snapshot.json')
20
- .option('--ci', 'CI mode (no interactive prompts)')
21
- .action(scanCommand);
22
-
23
- program
24
- .command('upload')
25
- .description('Upload a snapshot to VeeStack server')
26
- .option('-f, --file <path>', 'Snapshot file path', 'snapshot.json')
27
- .option('-p, --project-id <id>', 'Project ID')
28
- .action(uploadCommand);
29
-
30
- program
31
- .command('login')
32
- .description('Authenticate with VeeStack server')
33
- .option('-k, --key <apiKey>', 'API key')
34
- .action(loginCommand);
35
-
36
- program.parse();
@@ -1,181 +0,0 @@
1
- import crypto from 'crypto';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { WasmEngineLoader, type WasmSnapshot, type WasmFileInfo, type WasmDependency, type WasmMetadata } from '../wasm/loader.js';
5
-
6
- const LICENSE_FILE = '.veestack/license.key';
7
- const SESSION_FILE = '.veestack/session.json';
8
-
9
- export interface LicenseData {
10
- key: string;
11
- hardware_id: string;
12
- issued_at: string;
13
- expires_at: string;
14
- tier: 'free' | 'pro' | 'enterprise';
15
- features: string[];
16
- }
17
-
18
- export class LicenseManager {
19
- private projectPath: string;
20
- private licenseData: LicenseData | null = null;
21
-
22
- constructor(projectPath: string = '.') {
23
- this.projectPath = projectPath;
24
- }
25
-
26
- /**
27
- * Check if valid license exists
28
- */
29
- async hasValidLicense(): Promise<boolean> {
30
- const licensePath = path.join(this.projectPath, LICENSE_FILE);
31
-
32
- if (!fs.existsSync(licensePath)) {
33
- return false;
34
- }
35
-
36
- try {
37
- const data = fs.readFileSync(licensePath, 'utf-8');
38
- this.licenseData = JSON.parse(data);
39
-
40
- // Validate hardware binding
41
- const currentHardwareId = this.generateHardwareId();
42
- if (this.licenseData?.hardware_id !== currentHardwareId) {
43
- console.warn('⚠️ License bound to different machine');
44
- return false;
45
- }
46
-
47
- // Check expiration
48
- if (this.licenseData?.expires_at) {
49
- const expiry = new Date(this.licenseData.expires_at);
50
- if (expiry < new Date()) {
51
- console.warn('⚠️ License has expired');
52
- return false;
53
- }
54
- }
55
-
56
- return true;
57
- } catch (error) {
58
- console.error('❌ Error reading license:', error);
59
- return false;
60
- }
61
- }
62
-
63
- /**
64
- * Get license key
65
- */
66
- getLicenseKey(): string | null {
67
- return this.licenseData?.key || null;
68
- }
69
-
70
- /**
71
- * Get license tier
72
- */
73
- getLicenseTier(): string {
74
- return this.licenseData?.tier || 'free';
75
- }
76
-
77
- /**
78
- * Validate license with remote server
79
- */
80
- async validateWithServer(apiUrl: string, apiKey: string): Promise<boolean> {
81
- if (!this.licenseData) return false;
82
-
83
- try {
84
- const response = await fetch(`${apiUrl}/functions/v1/validate-license`, {
85
- method: 'POST',
86
- headers: {
87
- 'Content-Type': 'application/json',
88
- 'Authorization': `Bearer ${apiKey}`
89
- },
90
- body: JSON.stringify({
91
- license_key: this.licenseData.key,
92
- hardware_id: this.licenseData.hardware_id
93
- })
94
- });
95
-
96
- if (!response.ok) return false;
97
-
98
- const result = await response.json() as { valid: boolean };
99
- return result.valid === true;
100
- } catch (error) {
101
- console.error('❌ License validation error:', error);
102
- return false;
103
- }
104
- }
105
-
106
- /**
107
- * Generate hardware fingerprint
108
- */
109
- generateHardwareId(): string {
110
- const components = [
111
- process.platform,
112
- process.arch,
113
- process.version,
114
- require('os').hostname(),
115
- require('os').userInfo().username,
116
- 'veestack-v1'
117
- ];
118
-
119
- const combined = components.join('|');
120
- return crypto.createHash('sha256').update(combined).digest('hex');
121
- }
122
-
123
- /**
124
- * Save license to file
125
- */
126
- async saveLicense(licenseData: LicenseData): Promise<void> {
127
- const licenseDir = path.dirname(path.join(this.projectPath, LICENSE_FILE));
128
-
129
- if (!fs.existsSync(licenseDir)) {
130
- fs.mkdirSync(licenseDir, { recursive: true });
131
- }
132
-
133
- fs.writeFileSync(
134
- path.join(this.projectPath, LICENSE_FILE),
135
- JSON.stringify(licenseData, null, 2)
136
- );
137
-
138
- this.licenseData = licenseData;
139
- }
140
-
141
- /**
142
- * Get or create session
143
- */
144
- getOrCreateSession(): { sessionId: string; isNew: boolean } {
145
- const sessionPath = path.join(this.projectPath, SESSION_FILE);
146
-
147
- if (fs.existsSync(sessionPath)) {
148
- try {
149
- const data = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
150
-
151
- // Check if session is still valid (24 hours)
152
- const created = new Date(data.created_at);
153
- const now = new Date();
154
- const hoursDiff = (now.getTime() - created.getTime()) / (1000 * 60 * 60);
155
-
156
- if (hoursDiff < 24) {
157
- return { sessionId: data.session_id, isNew: false };
158
- }
159
- } catch {
160
- // Invalid session file, create new
161
- }
162
- }
163
-
164
- // Create new session
165
- const sessionId = crypto.randomUUID();
166
- const sessionData = {
167
- session_id: sessionId,
168
- created_at: new Date().toISOString(),
169
- hardware_id: this.generateHardwareId()
170
- };
171
-
172
- const sessionDir = path.dirname(sessionPath);
173
- if (!fs.existsSync(sessionDir)) {
174
- fs.mkdirSync(sessionDir, { recursive: true });
175
- }
176
-
177
- fs.writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
178
-
179
- return { sessionId, isNew: true };
180
- }
181
- }