@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.
- package/dist/index.js +450 -0
- package/package.json +8 -4
- package/.env.example +0 -14
- package/src/commands/login.ts +0 -60
- package/src/commands/scan.ts +0 -219
- package/src/commands/upload.ts +0 -241
- package/src/index.ts +0 -36
- package/src/wasm/license.ts +0 -181
- package/src/wasm/loader.ts +0 -409
- package/tsconfig.json +0 -19
- package/tsup.config.ts +0 -11
package/src/commands/scan.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/upload.ts
DELETED
|
@@ -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();
|
package/src/wasm/license.ts
DELETED
|
@@ -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
|
-
}
|