@veestack-tools/cli 3.0.1 → 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 ADDED
@@ -0,0 +1,450 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/scan.ts
7
+ import { glob } from "glob";
8
+ import { readFileSync, statSync } from "fs";
9
+ import { join, resolve } from "path";
10
+ import crypto from "crypto";
11
+ import ora from "ora";
12
+ async function scanCommand(options) {
13
+ const spinner = ora({
14
+ text: "Scanning project...",
15
+ spinner: "dots",
16
+ stream: process.stdout
17
+ }).start();
18
+ try {
19
+ const snapshot = await generateSnapshot(options.path);
20
+ spinner.succeed(`Scanned ${snapshot.metadata.total_files} files`);
21
+ const fs = await import("fs/promises");
22
+ await fs.writeFile(options.output, JSON.stringify(snapshot, null, 2));
23
+ console.log(`
24
+ \u2705 Snapshot saved to ${options.output}`);
25
+ } catch (error) {
26
+ spinner.fail("Scan failed");
27
+ console.error(error);
28
+ process.exit(1);
29
+ }
30
+ }
31
+ async function generateSnapshot(projectPath) {
32
+ const files = await scanFiles(projectPath);
33
+ const dependencies = await scanDependencies(projectPath);
34
+ const metadata = {
35
+ total_files: files.length,
36
+ total_dependencies: dependencies.length,
37
+ total_size_bytes: files.reduce((sum, f) => sum + f.size_bytes, 0),
38
+ total_lines: files.reduce((sum, f) => sum + (f.estimated_lines || 0), 0),
39
+ max_directory_depth: Math.max(...files.map((f) => f.depth), 0),
40
+ language_breakdown: calculateLanguageBreakdown(files)
41
+ };
42
+ return {
43
+ snapshot_version: "1.0.0",
44
+ engine_target_version: "1.0.0",
45
+ project_id: crypto.randomUUID(),
46
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
47
+ root_path_hash: hashString(projectPath),
48
+ project_root: resolve(projectPath),
49
+ // Use absolute path for content scanning
50
+ metadata,
51
+ files,
52
+ dependencies
53
+ };
54
+ }
55
+ async function scanFiles(projectPath) {
56
+ const files = [];
57
+ const patterns = ["**/*"];
58
+ const filePaths = await glob(patterns, {
59
+ cwd: projectPath,
60
+ ignore: [
61
+ "**/node_modules/**",
62
+ "**/.git/**",
63
+ "**/dist/**",
64
+ "**/build/**",
65
+ "**/.next/**",
66
+ "**/out/**",
67
+ "**/coverage/**"
68
+ ],
69
+ absolute: false
70
+ });
71
+ for (const filePath of filePaths) {
72
+ const fullPath = join(projectPath, filePath);
73
+ const stats = statSync(fullPath);
74
+ if (stats.isFile()) {
75
+ const fileNode = {
76
+ id: crypto.randomUUID(),
77
+ path: filePath,
78
+ // Actual relative path for rule evaluation
79
+ path_hash: hashString(filePath),
80
+ depth: filePath.split(/[\\/]/).length,
81
+ size_bytes: stats.size,
82
+ estimated_lines: Math.floor(stats.size / 50),
83
+ // Rough estimate
84
+ extension: filePath.split(".").pop() || "",
85
+ is_binary: isBinaryFile(filePath)
86
+ };
87
+ files.push(fileNode);
88
+ }
89
+ }
90
+ return files.sort((a, b) => a.path_hash.localeCompare(b.path_hash));
91
+ }
92
+ async function scanDependencies(projectPath) {
93
+ const dependencies = [];
94
+ try {
95
+ const packageJsonPath = join(projectPath, "package.json");
96
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
97
+ const processDeps = (deps, category) => {
98
+ for (const [name, version] of Object.entries(deps)) {
99
+ const versionMatch = version.match(/^(\d+)\.(\d+)/);
100
+ const major = versionMatch ? parseInt(versionMatch[1]) : 0;
101
+ const minor = versionMatch ? parseInt(versionMatch[2]) : 0;
102
+ dependencies.push({
103
+ id: crypto.randomUUID(),
104
+ name_hash: hashString(name),
105
+ major_version: major,
106
+ minor_version: minor,
107
+ category
108
+ });
109
+ }
110
+ };
111
+ if (packageJson.dependencies) {
112
+ processDeps(packageJson.dependencies, "prod");
113
+ }
114
+ if (packageJson.devDependencies) {
115
+ processDeps(packageJson.devDependencies, "dev");
116
+ }
117
+ if (packageJson.peerDependencies) {
118
+ processDeps(packageJson.peerDependencies, "peer");
119
+ }
120
+ } catch (error) {
121
+ }
122
+ return dependencies.sort((a, b) => a.name_hash.localeCompare(b.name_hash));
123
+ }
124
+ function calculateLanguageBreakdown(files) {
125
+ const breakdown = {};
126
+ for (const file of files) {
127
+ const ext = file.extension.toLowerCase();
128
+ const lang = getLanguageFromExtension(ext);
129
+ breakdown[lang] = (breakdown[lang] || 0) + 1;
130
+ }
131
+ return breakdown;
132
+ }
133
+ function getLanguageFromExtension(ext) {
134
+ const map = {
135
+ ts: "TypeScript",
136
+ js: "JavaScript",
137
+ tsx: "TypeScript",
138
+ jsx: "JavaScript",
139
+ py: "Python",
140
+ go: "Go",
141
+ rs: "Rust",
142
+ java: "Java",
143
+ kt: "Kotlin",
144
+ rb: "Ruby",
145
+ php: "PHP",
146
+ cs: "C#",
147
+ cpp: "C++",
148
+ c: "C",
149
+ h: "C",
150
+ json: "JSON",
151
+ yaml: "YAML",
152
+ yml: "YAML",
153
+ md: "Markdown",
154
+ html: "HTML",
155
+ css: "CSS",
156
+ scss: "SCSS",
157
+ sql: "SQL",
158
+ sh: "Shell"
159
+ };
160
+ return map[ext] || "Other";
161
+ }
162
+ function isBinaryFile(filePath) {
163
+ const binaryExtensions = [
164
+ ".exe",
165
+ ".dll",
166
+ ".so",
167
+ ".dylib",
168
+ ".bin",
169
+ ".png",
170
+ ".jpg",
171
+ ".jpeg",
172
+ ".gif",
173
+ ".pdf",
174
+ ".zip",
175
+ ".tar",
176
+ ".gz",
177
+ ".7z",
178
+ ".woff",
179
+ ".woff2",
180
+ ".ttf",
181
+ ".eot"
182
+ ];
183
+ const ext = filePath.split(".").pop()?.toLowerCase();
184
+ return ext ? binaryExtensions.includes("." + ext) : false;
185
+ }
186
+ function hashString(str) {
187
+ return crypto.createHash("sha256").update(str).digest("hex");
188
+ }
189
+
190
+ // src/commands/upload.ts
191
+ import ora2 from "ora";
192
+ import chalk from "chalk";
193
+ import readline from "readline";
194
+ var supabaseUrl = process.env.SUPABASE_URL || "https://qhonrrojtqklvlkvfswb.supabase.co";
195
+ var supabaseAnonKey = process.env.SUPABASE_ANON_KEY || "";
196
+ async function fetchProjects() {
197
+ try {
198
+ const res = await fetch(`${supabaseUrl}/rest/v1/projects?select=id,name,created_at&order=created_at.desc`, {
199
+ headers: { "apikey": supabaseAnonKey, "Authorization": `Bearer ${supabaseAnonKey}` }
200
+ });
201
+ if (res.ok) {
202
+ const data = await res.json();
203
+ return data;
204
+ }
205
+ return [];
206
+ } catch {
207
+ return [];
208
+ }
209
+ }
210
+ async function createProject(name) {
211
+ try {
212
+ const res = await fetch(`${supabaseUrl}/rest/v1/projects`, {
213
+ method: "POST",
214
+ headers: {
215
+ "Content-Type": "application/json",
216
+ "apikey": supabaseAnonKey,
217
+ "Authorization": `Bearer ${supabaseAnonKey}`,
218
+ "Prefer": "return=representation"
219
+ },
220
+ body: JSON.stringify({
221
+ name,
222
+ user_id: "00000000-0000-0000-0000-000000000001"
223
+ // Default user
224
+ })
225
+ });
226
+ if (res.ok) {
227
+ const data = await res.json();
228
+ return data[0]?.id || null;
229
+ }
230
+ return null;
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ function askQuestion(question) {
236
+ const rl = readline.createInterface({
237
+ input: process.stdin,
238
+ output: process.stdout
239
+ });
240
+ return new Promise((resolve2) => {
241
+ rl.question(question, (answer) => {
242
+ rl.close();
243
+ resolve2(answer);
244
+ });
245
+ });
246
+ }
247
+ async function selectOrCreateProject() {
248
+ console.log(chalk.blue("\n\u{1F4C1} Loading projects...\n"));
249
+ const projects = await fetchProjects();
250
+ if (projects.length === 0) {
251
+ console.log(chalk.yellow("No projects found.\n"));
252
+ const answer2 = await askQuestion("Do you want to create a new project? (y/n): ");
253
+ if (answer2.toLowerCase() === "y") {
254
+ const name = await askQuestion("Enter project name: ");
255
+ if (name.trim()) {
256
+ const projectId = await createProject(name.trim());
257
+ if (projectId) {
258
+ console.log(chalk.green(`
259
+ \u2705 Project "${name}" created successfully!`));
260
+ return projectId;
261
+ }
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+ console.log(chalk.white("Select a project:\n"));
267
+ console.log(chalk.gray("0. [+] Create new project\n"));
268
+ projects.forEach((p, i) => {
269
+ console.log(chalk.white(`${i + 1}. ${p.name}`));
270
+ console.log(chalk.gray(` ID: ${p.id}`));
271
+ console.log(chalk.gray(` Created: ${new Date(p.created_at).toLocaleDateString()}
272
+ `));
273
+ });
274
+ const answer = await askQuestion("Enter number (0-" + projects.length + "): ");
275
+ const choice = parseInt(answer, 10);
276
+ if (choice === 0) {
277
+ const name = await askQuestion("\nEnter project name: ");
278
+ if (name.trim()) {
279
+ const projectId = await createProject(name.trim());
280
+ if (projectId) {
281
+ console.log(chalk.green(`
282
+ \u2705 Project "${name}" created successfully!`));
283
+ return projectId;
284
+ }
285
+ }
286
+ return null;
287
+ } else if (choice > 0 && choice <= projects.length) {
288
+ const selected = projects[choice - 1];
289
+ console.log(chalk.green(`
290
+ \u2705 Selected project: "${selected.name}"`));
291
+ return selected.id;
292
+ } else {
293
+ console.log(chalk.red("\n\u274C Invalid selection"));
294
+ return null;
295
+ }
296
+ }
297
+ async function uploadCommand(options) {
298
+ const spinner = ora2("Preparing upload...").start();
299
+ try {
300
+ const fs = await import("fs/promises");
301
+ const snapshotData = await fs.readFile(options.file, "utf-8");
302
+ const snapshot = JSON.parse(snapshotData);
303
+ let projectId = options.projectId;
304
+ if (!projectId) {
305
+ spinner.stop();
306
+ const selectedProjectId = await selectOrCreateProject();
307
+ if (!selectedProjectId) {
308
+ console.log(chalk.red("\n\u274C No project selected. Exiting."));
309
+ process.exit(1);
310
+ }
311
+ projectId = selectedProjectId;
312
+ spinner.start("Uploading snapshot...");
313
+ }
314
+ spinner.text = "Creating snapshot...";
315
+ const snapshotRes = await fetch(`${supabaseUrl}/rest/v1/snapshots`, {
316
+ method: "POST",
317
+ headers: {
318
+ "Content-Type": "application/json",
319
+ "apikey": supabaseAnonKey,
320
+ "Authorization": `Bearer ${supabaseAnonKey}`,
321
+ "Prefer": "return=representation"
322
+ },
323
+ body: JSON.stringify({
324
+ project_id: projectId,
325
+ file_count: snapshot.metadata.total_files,
326
+ size_mb: snapshot.metadata.total_size_bytes / (1024 * 1024),
327
+ storage_path: "local://" + projectId
328
+ })
329
+ });
330
+ if (!snapshotRes.ok) {
331
+ const error = await snapshotRes.json();
332
+ spinner.fail("Failed to create snapshot");
333
+ console.error(chalk.red(`Error: ${error.message || error.details || "Unknown error"}`));
334
+ process.exit(1);
335
+ }
336
+ const snapshotData2 = await snapshotRes.json();
337
+ const snapshotId = snapshotData2[0]?.id || snapshotData2[0]?.id;
338
+ spinner.text = "Analyzing snapshot...";
339
+ const { CoreEngine } = await import("@veestack-tools/engine");
340
+ const { RULES } = await import("@veestack-tools/engine");
341
+ const engine = new CoreEngine();
342
+ RULES.forEach((rule) => engine.registerRule(rule));
343
+ const result = await engine.analyze(snapshot);
344
+ if (!result.success) {
345
+ spinner.fail("Analysis failed");
346
+ console.error(chalk.red("Error:", result.error));
347
+ process.exit(1);
348
+ }
349
+ spinner.text = "Creating report...";
350
+ const reportRes = await fetch(`${supabaseUrl}/rest/v1/reports`, {
351
+ method: "POST",
352
+ headers: {
353
+ "Content-Type": "application/json",
354
+ "apikey": supabaseAnonKey,
355
+ "Authorization": `Bearer ${supabaseAnonKey}`,
356
+ "Prefer": "return=representation"
357
+ },
358
+ body: JSON.stringify({
359
+ snapshot_id: snapshotId,
360
+ score: result.report.total_score,
361
+ issues_count: result.report.summary.total_findings,
362
+ critical_count: result.report.summary.critical_count,
363
+ high_count: result.report.summary.high_count,
364
+ medium_count: result.report.summary.medium_count,
365
+ low_count: result.report.summary.low_count,
366
+ execution_time_ms: 0,
367
+ report_json: result.report
368
+ })
369
+ });
370
+ if (!reportRes.ok) {
371
+ const error = await reportRes.json();
372
+ spinner.fail("Failed to create report");
373
+ console.error(chalk.red(`Error: ${error.message || error.details || "Unknown error"}`));
374
+ process.exit(1);
375
+ }
376
+ const reportData = await reportRes.json();
377
+ const reportId = reportData[0]?.id || reportData[0]?.id;
378
+ spinner.succeed("Analysis complete");
379
+ console.log(chalk.green("\n\u2705 Report generated and saved to VeeStack"));
380
+ console.log(chalk.gray("Project ID:"), projectId);
381
+ console.log(chalk.gray("Snapshot ID:"), snapshotId);
382
+ console.log(chalk.gray("Report ID:"), reportId);
383
+ console.log(chalk.bold("\n\u{1F4CA} Score:"), result.report.total_score);
384
+ console.log(chalk.gray("Severity:"), result.report.severity_band);
385
+ console.log(chalk.gray("Findings:"), result.report.summary.total_findings);
386
+ console.log(chalk.blue("\n\u{1F517} View your report at:"));
387
+ console.log(chalk.underline(`http://localhost:3001/reports/${reportId}?project=${projectId}`));
388
+ } catch (error) {
389
+ spinner.fail("Upload failed");
390
+ console.error(error);
391
+ process.exit(1);
392
+ }
393
+ }
394
+
395
+ // src/commands/login.ts
396
+ import { writeFileSync } from "fs";
397
+ import { join as join2 } from "path";
398
+ import { homedir } from "os";
399
+ import ora3 from "ora";
400
+ import prompts from "prompts";
401
+ async function loginCommand(options) {
402
+ const spinner = ora3({
403
+ text: "Authenticating...",
404
+ spinner: "dots",
405
+ stream: process.stdout
406
+ });
407
+ try {
408
+ let apiKey = options.key;
409
+ if (!apiKey) {
410
+ const response = await prompts({
411
+ type: "password",
412
+ name: "key",
413
+ message: "Enter your VeeStack API key:",
414
+ validate: (value) => value.length > 0 || "API key is required"
415
+ });
416
+ if (!response.key) {
417
+ console.log("\u274C Login cancelled");
418
+ process.exit(1);
419
+ }
420
+ apiKey = response.key;
421
+ }
422
+ spinner.start("Validating API key...");
423
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
424
+ const configDir = join2(homedir(), ".veestack");
425
+ const configFile = join2(configDir, "config.json");
426
+ try {
427
+ const fs = await import("fs/promises");
428
+ await fs.mkdir(configDir, { recursive: true });
429
+ await fs.writeFile(configFile, JSON.stringify({ apiKey }, null, 2));
430
+ } catch {
431
+ const fs = await import("fs");
432
+ fs.mkdirSync(configDir, { recursive: true });
433
+ writeFileSync(configFile, JSON.stringify({ apiKey }, null, 2));
434
+ }
435
+ spinner.succeed("Authenticated successfully");
436
+ console.log("\u2705 API key saved to ~/.veestack/config.json");
437
+ } catch (error) {
438
+ spinner.fail("Authentication failed");
439
+ console.error(error);
440
+ process.exit(1);
441
+ }
442
+ }
443
+
444
+ // src/index.ts
445
+ var program = new Command();
446
+ program.name("veestack").description("VeeStack CLI - Technical Stack Visibility Tool").version("3.0.1");
447
+ program.command("scan").description("Scan a project directory and generate analysis").option("-p, --path <path>", "Path to project directory", ".").option("-o, --output <path>", "Output file path", "snapshot.json").option("--ci", "CI mode (no interactive prompts)").action(scanCommand);
448
+ program.command("upload").description("Upload a snapshot to VeeStack server").option("-f, --file <path>", "Snapshot file path", "snapshot.json").option("-p, --project-id <id>", "Project ID").action(uploadCommand);
449
+ program.command("login").description("Authenticate with VeeStack server").option("-k, --key <apiKey>", "API key").action(loginCommand);
450
+ program.parse();
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@veestack-tools/cli",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "type": "module",
5
+ "files": [
6
+ "dist/**/*",
7
+ "bin/**/*"
8
+ ],
5
9
  "bin": {
6
10
  "veestack": "bin/veestack.mjs"
7
11
  },
@@ -11,9 +15,9 @@
11
15
  "start": "node bin/veestack.mjs"
12
16
  },
13
17
  "dependencies": {
14
- "@veestack-tools/types": "workspace:*",
15
- "@veestack-tools/utils": "workspace:*",
16
- "@veestack-tools/engine": "workspace:*",
18
+ "@veestack-tools/types": "3.0.2",
19
+ "@veestack-tools/utils": "3.0.2",
20
+ "@veestack-tools/engine": "3.0.3",
17
21
  "commander": "^11.1.0",
18
22
  "chalk": "^5.3.0",
19
23
  "ora": "^8.0.1",
package/.env.example DELETED
@@ -1,14 +0,0 @@
1
- # VeeStack CLI Environment Variables
2
- # Copy this file to .env and fill in your actual values
3
-
4
- # Supabase Configuration (Required)
5
- SUPABASE_URL=https://your-project.supabase.co
6
- SUPABASE_ANON_KEY=your_anon_key_here
7
-
8
- # IMPORTANT SECURITY NOTES:
9
- # - CLI should use SUPABASE_ANON_KEY (not Service Role Key)
10
- # - User authentication is handled via OAuth browser flow
11
- # - Access token is stored securely after login
12
-
13
- # Optional: For admin/debug scripts only (never commit this)
14
- # SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here
@@ -1,60 +0,0 @@
1
- import { writeFileSync } from 'fs';
2
- import { join } from 'path';
3
- import { homedir } from 'os';
4
- import ora from 'ora';
5
- import prompts from 'prompts';
6
-
7
- export async function loginCommand(options: { key?: string }): Promise<void> {
8
- const spinner = ora({
9
- text: 'Authenticating...',
10
- spinner: 'dots',
11
- stream: process.stdout
12
- });
13
-
14
- try {
15
- let apiKey = options.key;
16
-
17
- if (!apiKey) {
18
- const response = await prompts({
19
- type: 'password',
20
- name: 'key',
21
- message: 'Enter your VeeStack API key:',
22
- validate: (value) => value.length > 0 || 'API key is required'
23
- });
24
-
25
- if (!response.key) {
26
- console.log('❌ Login cancelled');
27
- process.exit(1);
28
- }
29
-
30
- apiKey = response.key;
31
- }
32
-
33
- spinner.start('Validating API key...');
34
-
35
- // Mock validation - in production this would validate with the server
36
- await new Promise(resolve => setTimeout(resolve, 1000));
37
-
38
- // Save to config file
39
- const configDir = join(homedir(), '.veestack');
40
- const configFile = join(configDir, 'config.json');
41
-
42
- try {
43
- const fs = await import('fs/promises');
44
- await fs.mkdir(configDir, { recursive: true });
45
- await fs.writeFile(configFile, JSON.stringify({ apiKey }, null, 2));
46
- } catch {
47
- // Fallback to sync
48
- const fs = await import('fs');
49
- fs.mkdirSync(configDir, { recursive: true });
50
- writeFileSync(configFile, JSON.stringify({ apiKey }, null, 2));
51
- }
52
-
53
- spinner.succeed('Authenticated successfully');
54
- console.log('✅ API key saved to ~/.veestack/config.json');
55
- } catch (error) {
56
- spinner.fail('Authentication failed');
57
- console.error(error);
58
- process.exit(1);
59
- }
60
- }