@veestack-tools/cli 3.0.2 → 3.0.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.
- package/dist/index.js +563 -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/dist/index.js
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
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 as readFileSync2, statSync } from "fs";
|
|
9
|
+
import { join as join2, resolve } from "path";
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
import chalk2 from "chalk";
|
|
13
|
+
|
|
14
|
+
// src/utils/auth.ts
|
|
15
|
+
import { readFileSync, existsSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
var CONFIG_DIR = join(homedir(), ".veestack");
|
|
20
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
21
|
+
function getAuthConfig() {
|
|
22
|
+
try {
|
|
23
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
27
|
+
return config;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isAuthenticated() {
|
|
33
|
+
const config = getAuthConfig();
|
|
34
|
+
return config !== null && config.apiKey !== void 0 && config.apiKey.length > 0;
|
|
35
|
+
}
|
|
36
|
+
function getSubscriptionTier() {
|
|
37
|
+
const config = getAuthConfig();
|
|
38
|
+
if (!config || !config.subscription) {
|
|
39
|
+
return "free";
|
|
40
|
+
}
|
|
41
|
+
return config.subscription.tier;
|
|
42
|
+
}
|
|
43
|
+
function isSubscriptionActive() {
|
|
44
|
+
const config = getAuthConfig();
|
|
45
|
+
if (!config || !config.subscription) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (config.subscription.tier === "free") {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (config.subscription.expiresAt) {
|
|
52
|
+
const expiresAt = new Date(config.subscription.expiresAt);
|
|
53
|
+
const now = /* @__PURE__ */ new Date();
|
|
54
|
+
return expiresAt > now && config.subscription.isActive;
|
|
55
|
+
}
|
|
56
|
+
return config.subscription.isActive;
|
|
57
|
+
}
|
|
58
|
+
function requireAuth() {
|
|
59
|
+
const config = getAuthConfig();
|
|
60
|
+
if (!config || !config.apiKey) {
|
|
61
|
+
console.log(chalk.red("\u274C Authentication required"));
|
|
62
|
+
console.log(chalk.yellow("\nPlease login first:"));
|
|
63
|
+
console.log(chalk.cyan(" veestack login"));
|
|
64
|
+
console.log(chalk.gray("\nOr get an API key from: https://veestack.tools/dashboard"));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/commands/scan.ts
|
|
71
|
+
async function scanCommand(options) {
|
|
72
|
+
const authConfig = requireAuth();
|
|
73
|
+
const tier = getSubscriptionTier();
|
|
74
|
+
console.log(chalk2.blue("\n\u{1F510} Authentication verified"));
|
|
75
|
+
console.log(chalk2.gray(`Subscription tier: ${tier === "pro" ? "Pro" : "Free"}`));
|
|
76
|
+
console.log(chalk2.gray("\u2500".repeat(40) + "\n"));
|
|
77
|
+
const spinner = ora({
|
|
78
|
+
text: "Scanning project...",
|
|
79
|
+
spinner: "dots",
|
|
80
|
+
stream: process.stdout
|
|
81
|
+
}).start();
|
|
82
|
+
try {
|
|
83
|
+
const snapshot = await generateSnapshot(options.path);
|
|
84
|
+
spinner.succeed(`Scanned ${snapshot.metadata.total_files} files`);
|
|
85
|
+
const fs = await import("fs/promises");
|
|
86
|
+
await fs.writeFile(options.output, JSON.stringify(snapshot, null, 2));
|
|
87
|
+
console.log(`
|
|
88
|
+
\u2705 Snapshot saved to ${options.output}`);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
spinner.fail("Scan failed");
|
|
91
|
+
console.error(error);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function generateSnapshot(projectPath) {
|
|
96
|
+
const files = await scanFiles(projectPath);
|
|
97
|
+
const dependencies = await scanDependencies(projectPath);
|
|
98
|
+
const metadata = {
|
|
99
|
+
total_files: files.length,
|
|
100
|
+
total_dependencies: dependencies.length,
|
|
101
|
+
total_size_bytes: files.reduce((sum, f) => sum + f.size_bytes, 0),
|
|
102
|
+
total_lines: files.reduce((sum, f) => sum + (f.estimated_lines || 0), 0),
|
|
103
|
+
max_directory_depth: Math.max(...files.map((f) => f.depth), 0),
|
|
104
|
+
language_breakdown: calculateLanguageBreakdown(files)
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
snapshot_version: "1.0.0",
|
|
108
|
+
engine_target_version: "1.0.0",
|
|
109
|
+
project_id: crypto.randomUUID(),
|
|
110
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
111
|
+
root_path_hash: hashString(projectPath),
|
|
112
|
+
project_root: resolve(projectPath),
|
|
113
|
+
// Use absolute path for content scanning
|
|
114
|
+
metadata,
|
|
115
|
+
files,
|
|
116
|
+
dependencies
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function scanFiles(projectPath) {
|
|
120
|
+
const files = [];
|
|
121
|
+
const patterns = ["**/*"];
|
|
122
|
+
const filePaths = await glob(patterns, {
|
|
123
|
+
cwd: projectPath,
|
|
124
|
+
ignore: [
|
|
125
|
+
"**/node_modules/**",
|
|
126
|
+
"**/.git/**",
|
|
127
|
+
"**/dist/**",
|
|
128
|
+
"**/build/**",
|
|
129
|
+
"**/.next/**",
|
|
130
|
+
"**/out/**",
|
|
131
|
+
"**/coverage/**"
|
|
132
|
+
],
|
|
133
|
+
absolute: false
|
|
134
|
+
});
|
|
135
|
+
for (const filePath of filePaths) {
|
|
136
|
+
const fullPath = join2(projectPath, filePath);
|
|
137
|
+
const stats = statSync(fullPath);
|
|
138
|
+
if (stats.isFile()) {
|
|
139
|
+
const fileNode = {
|
|
140
|
+
id: crypto.randomUUID(),
|
|
141
|
+
path: filePath,
|
|
142
|
+
// Actual relative path for rule evaluation
|
|
143
|
+
path_hash: hashString(filePath),
|
|
144
|
+
depth: filePath.split(/[\\/]/).length,
|
|
145
|
+
size_bytes: stats.size,
|
|
146
|
+
estimated_lines: Math.floor(stats.size / 50),
|
|
147
|
+
// Rough estimate
|
|
148
|
+
extension: filePath.split(".").pop() || "",
|
|
149
|
+
is_binary: isBinaryFile(filePath)
|
|
150
|
+
};
|
|
151
|
+
files.push(fileNode);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return files.sort((a, b) => a.path_hash.localeCompare(b.path_hash));
|
|
155
|
+
}
|
|
156
|
+
async function scanDependencies(projectPath) {
|
|
157
|
+
const dependencies = [];
|
|
158
|
+
try {
|
|
159
|
+
const packageJsonPath = join2(projectPath, "package.json");
|
|
160
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
161
|
+
const processDeps = (deps, category) => {
|
|
162
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
163
|
+
const versionMatch = version.match(/^(\d+)\.(\d+)/);
|
|
164
|
+
const major = versionMatch ? parseInt(versionMatch[1]) : 0;
|
|
165
|
+
const minor = versionMatch ? parseInt(versionMatch[2]) : 0;
|
|
166
|
+
dependencies.push({
|
|
167
|
+
id: crypto.randomUUID(),
|
|
168
|
+
name_hash: hashString(name),
|
|
169
|
+
major_version: major,
|
|
170
|
+
minor_version: minor,
|
|
171
|
+
category
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
if (packageJson.dependencies) {
|
|
176
|
+
processDeps(packageJson.dependencies, "prod");
|
|
177
|
+
}
|
|
178
|
+
if (packageJson.devDependencies) {
|
|
179
|
+
processDeps(packageJson.devDependencies, "dev");
|
|
180
|
+
}
|
|
181
|
+
if (packageJson.peerDependencies) {
|
|
182
|
+
processDeps(packageJson.peerDependencies, "peer");
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
}
|
|
186
|
+
return dependencies.sort((a, b) => a.name_hash.localeCompare(b.name_hash));
|
|
187
|
+
}
|
|
188
|
+
function calculateLanguageBreakdown(files) {
|
|
189
|
+
const breakdown = {};
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
const ext = file.extension.toLowerCase();
|
|
192
|
+
const lang = getLanguageFromExtension(ext);
|
|
193
|
+
breakdown[lang] = (breakdown[lang] || 0) + 1;
|
|
194
|
+
}
|
|
195
|
+
return breakdown;
|
|
196
|
+
}
|
|
197
|
+
function getLanguageFromExtension(ext) {
|
|
198
|
+
const map = {
|
|
199
|
+
ts: "TypeScript",
|
|
200
|
+
js: "JavaScript",
|
|
201
|
+
tsx: "TypeScript",
|
|
202
|
+
jsx: "JavaScript",
|
|
203
|
+
py: "Python",
|
|
204
|
+
go: "Go",
|
|
205
|
+
rs: "Rust",
|
|
206
|
+
java: "Java",
|
|
207
|
+
kt: "Kotlin",
|
|
208
|
+
rb: "Ruby",
|
|
209
|
+
php: "PHP",
|
|
210
|
+
cs: "C#",
|
|
211
|
+
cpp: "C++",
|
|
212
|
+
c: "C",
|
|
213
|
+
h: "C",
|
|
214
|
+
json: "JSON",
|
|
215
|
+
yaml: "YAML",
|
|
216
|
+
yml: "YAML",
|
|
217
|
+
md: "Markdown",
|
|
218
|
+
html: "HTML",
|
|
219
|
+
css: "CSS",
|
|
220
|
+
scss: "SCSS",
|
|
221
|
+
sql: "SQL",
|
|
222
|
+
sh: "Shell"
|
|
223
|
+
};
|
|
224
|
+
return map[ext] || "Other";
|
|
225
|
+
}
|
|
226
|
+
function isBinaryFile(filePath) {
|
|
227
|
+
const binaryExtensions = [
|
|
228
|
+
".exe",
|
|
229
|
+
".dll",
|
|
230
|
+
".so",
|
|
231
|
+
".dylib",
|
|
232
|
+
".bin",
|
|
233
|
+
".png",
|
|
234
|
+
".jpg",
|
|
235
|
+
".jpeg",
|
|
236
|
+
".gif",
|
|
237
|
+
".pdf",
|
|
238
|
+
".zip",
|
|
239
|
+
".tar",
|
|
240
|
+
".gz",
|
|
241
|
+
".7z",
|
|
242
|
+
".woff",
|
|
243
|
+
".woff2",
|
|
244
|
+
".ttf",
|
|
245
|
+
".eot"
|
|
246
|
+
];
|
|
247
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
248
|
+
return ext ? binaryExtensions.includes("." + ext) : false;
|
|
249
|
+
}
|
|
250
|
+
function hashString(str) {
|
|
251
|
+
return crypto.createHash("sha256").update(str).digest("hex");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/commands/upload.ts
|
|
255
|
+
import ora2 from "ora";
|
|
256
|
+
import chalk3 from "chalk";
|
|
257
|
+
import readline from "readline";
|
|
258
|
+
var supabaseUrl = process.env.SUPABASE_URL || "https://qhonrrojtqklvlkvfswb.supabase.co";
|
|
259
|
+
var supabaseAnonKey = process.env.SUPABASE_ANON_KEY || "";
|
|
260
|
+
async function fetchProjects() {
|
|
261
|
+
try {
|
|
262
|
+
const res = await fetch(`${supabaseUrl}/rest/v1/projects?select=id,name,created_at&order=created_at.desc`, {
|
|
263
|
+
headers: { "apikey": supabaseAnonKey, "Authorization": `Bearer ${supabaseAnonKey}` }
|
|
264
|
+
});
|
|
265
|
+
if (res.ok) {
|
|
266
|
+
const data = await res.json();
|
|
267
|
+
return data;
|
|
268
|
+
}
|
|
269
|
+
return [];
|
|
270
|
+
} catch {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function createProject(name) {
|
|
275
|
+
try {
|
|
276
|
+
const res = await fetch(`${supabaseUrl}/rest/v1/projects`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
"apikey": supabaseAnonKey,
|
|
281
|
+
"Authorization": `Bearer ${supabaseAnonKey}`,
|
|
282
|
+
"Prefer": "return=representation"
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
name,
|
|
286
|
+
user_id: "00000000-0000-0000-0000-000000000001"
|
|
287
|
+
// Default user
|
|
288
|
+
})
|
|
289
|
+
});
|
|
290
|
+
if (res.ok) {
|
|
291
|
+
const data = await res.json();
|
|
292
|
+
return data[0]?.id || null;
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function askQuestion(question) {
|
|
300
|
+
const rl = readline.createInterface({
|
|
301
|
+
input: process.stdin,
|
|
302
|
+
output: process.stdout
|
|
303
|
+
});
|
|
304
|
+
return new Promise((resolve2) => {
|
|
305
|
+
rl.question(question, (answer) => {
|
|
306
|
+
rl.close();
|
|
307
|
+
resolve2(answer);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async function selectOrCreateProject() {
|
|
312
|
+
console.log(chalk3.blue("\n\u{1F4C1} Loading projects...\n"));
|
|
313
|
+
const projects = await fetchProjects();
|
|
314
|
+
if (projects.length === 0) {
|
|
315
|
+
console.log(chalk3.yellow("No projects found.\n"));
|
|
316
|
+
const answer2 = await askQuestion("Do you want to create a new project? (y/n): ");
|
|
317
|
+
if (answer2.toLowerCase() === "y") {
|
|
318
|
+
const name = await askQuestion("Enter project name: ");
|
|
319
|
+
if (name.trim()) {
|
|
320
|
+
const projectId = await createProject(name.trim());
|
|
321
|
+
if (projectId) {
|
|
322
|
+
console.log(chalk3.green(`
|
|
323
|
+
\u2705 Project "${name}" created successfully!`));
|
|
324
|
+
return projectId;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
console.log(chalk3.white("Select a project:\n"));
|
|
331
|
+
console.log(chalk3.gray("0. [+] Create new project\n"));
|
|
332
|
+
projects.forEach((p, i) => {
|
|
333
|
+
console.log(chalk3.white(`${i + 1}. ${p.name}`));
|
|
334
|
+
console.log(chalk3.gray(` ID: ${p.id}`));
|
|
335
|
+
console.log(chalk3.gray(` Created: ${new Date(p.created_at).toLocaleDateString()}
|
|
336
|
+
`));
|
|
337
|
+
});
|
|
338
|
+
const answer = await askQuestion("Enter number (0-" + projects.length + "): ");
|
|
339
|
+
const choice = parseInt(answer, 10);
|
|
340
|
+
if (choice === 0) {
|
|
341
|
+
const name = await askQuestion("\nEnter project name: ");
|
|
342
|
+
if (name.trim()) {
|
|
343
|
+
const projectId = await createProject(name.trim());
|
|
344
|
+
if (projectId) {
|
|
345
|
+
console.log(chalk3.green(`
|
|
346
|
+
\u2705 Project "${name}" created successfully!`));
|
|
347
|
+
return projectId;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
} else if (choice > 0 && choice <= projects.length) {
|
|
352
|
+
const selected = projects[choice - 1];
|
|
353
|
+
console.log(chalk3.green(`
|
|
354
|
+
\u2705 Selected project: "${selected.name}"`));
|
|
355
|
+
return selected.id;
|
|
356
|
+
} else {
|
|
357
|
+
console.log(chalk3.red("\n\u274C Invalid selection"));
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function uploadCommand(options) {
|
|
362
|
+
const authConfig = requireAuth();
|
|
363
|
+
const tier = getSubscriptionTier();
|
|
364
|
+
console.log(chalk3.blue("\n\u{1F510} Authentication verified"));
|
|
365
|
+
console.log(chalk3.gray(`Subscription tier: ${tier === "pro" ? "Pro" : "Free"}`));
|
|
366
|
+
console.log(chalk3.gray("\u2500".repeat(40) + "\n"));
|
|
367
|
+
const spinner = ora2("Preparing upload...").start();
|
|
368
|
+
try {
|
|
369
|
+
const fs = await import("fs/promises");
|
|
370
|
+
const snapshotData = await fs.readFile(options.file, "utf-8");
|
|
371
|
+
const snapshot = JSON.parse(snapshotData);
|
|
372
|
+
let projectId = options.projectId;
|
|
373
|
+
if (!projectId) {
|
|
374
|
+
spinner.stop();
|
|
375
|
+
const selectedProjectId = await selectOrCreateProject();
|
|
376
|
+
if (!selectedProjectId) {
|
|
377
|
+
console.log(chalk3.red("\n\u274C No project selected. Exiting."));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
projectId = selectedProjectId;
|
|
381
|
+
spinner.start("Uploading snapshot...");
|
|
382
|
+
}
|
|
383
|
+
spinner.text = "Creating snapshot...";
|
|
384
|
+
const snapshotRes = await fetch(`${supabaseUrl}/rest/v1/snapshots`, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
headers: {
|
|
387
|
+
"Content-Type": "application/json",
|
|
388
|
+
"apikey": supabaseAnonKey,
|
|
389
|
+
"Authorization": `Bearer ${supabaseAnonKey}`,
|
|
390
|
+
"Prefer": "return=representation"
|
|
391
|
+
},
|
|
392
|
+
body: JSON.stringify({
|
|
393
|
+
project_id: projectId,
|
|
394
|
+
file_count: snapshot.metadata.total_files,
|
|
395
|
+
size_mb: snapshot.metadata.total_size_bytes / (1024 * 1024),
|
|
396
|
+
storage_path: "local://" + projectId
|
|
397
|
+
})
|
|
398
|
+
});
|
|
399
|
+
if (!snapshotRes.ok) {
|
|
400
|
+
const error = await snapshotRes.json();
|
|
401
|
+
spinner.fail("Failed to create snapshot");
|
|
402
|
+
console.error(chalk3.red(`Error: ${error.message || error.details || "Unknown error"}`));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const snapshotData2 = await snapshotRes.json();
|
|
406
|
+
const snapshotId = snapshotData2[0]?.id || snapshotData2[0]?.id;
|
|
407
|
+
spinner.text = "Analyzing snapshot...";
|
|
408
|
+
const { CoreEngine } = await import("@veestack-tools/engine");
|
|
409
|
+
const { RULES } = await import("@veestack-tools/engine");
|
|
410
|
+
const engine = new CoreEngine();
|
|
411
|
+
RULES.forEach((rule) => engine.registerRule(rule));
|
|
412
|
+
const result = await engine.analyze(snapshot);
|
|
413
|
+
if (!result.success) {
|
|
414
|
+
spinner.fail("Analysis failed");
|
|
415
|
+
console.error(chalk3.red("Error:", result.error));
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
spinner.text = "Creating report...";
|
|
419
|
+
const reportRes = await fetch(`${supabaseUrl}/rest/v1/reports`, {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: {
|
|
422
|
+
"Content-Type": "application/json",
|
|
423
|
+
"apikey": supabaseAnonKey,
|
|
424
|
+
"Authorization": `Bearer ${supabaseAnonKey}`,
|
|
425
|
+
"Prefer": "return=representation"
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify({
|
|
428
|
+
snapshot_id: snapshotId,
|
|
429
|
+
score: result.report.total_score,
|
|
430
|
+
issues_count: result.report.summary.total_findings,
|
|
431
|
+
critical_count: result.report.summary.critical_count,
|
|
432
|
+
high_count: result.report.summary.high_count,
|
|
433
|
+
medium_count: result.report.summary.medium_count,
|
|
434
|
+
low_count: result.report.summary.low_count,
|
|
435
|
+
execution_time_ms: 0,
|
|
436
|
+
report_json: result.report
|
|
437
|
+
})
|
|
438
|
+
});
|
|
439
|
+
if (!reportRes.ok) {
|
|
440
|
+
const error = await reportRes.json();
|
|
441
|
+
spinner.fail("Failed to create report");
|
|
442
|
+
console.error(chalk3.red(`Error: ${error.message || error.details || "Unknown error"}`));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
const reportData = await reportRes.json();
|
|
446
|
+
const reportId = reportData[0]?.id || reportData[0]?.id;
|
|
447
|
+
spinner.succeed("Analysis complete");
|
|
448
|
+
console.log(chalk3.green("\n\u2705 Report generated and saved to VeeStack"));
|
|
449
|
+
console.log(chalk3.gray("Project ID:"), projectId);
|
|
450
|
+
console.log(chalk3.gray("Snapshot ID:"), snapshotId);
|
|
451
|
+
console.log(chalk3.gray("Report ID:"), reportId);
|
|
452
|
+
console.log(chalk3.bold("\n\u{1F4CA} Score:"), result.report.total_score);
|
|
453
|
+
console.log(chalk3.gray("Severity:"), result.report.severity_band);
|
|
454
|
+
console.log(chalk3.gray("Findings:"), result.report.summary.total_findings);
|
|
455
|
+
console.log(chalk3.blue("\n\u{1F517} View your report at:"));
|
|
456
|
+
console.log(chalk3.underline(`http://localhost:3001/reports/${reportId}?project=${projectId}`));
|
|
457
|
+
} catch (error) {
|
|
458
|
+
spinner.fail("Upload failed");
|
|
459
|
+
console.error(error);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/commands/login.ts
|
|
465
|
+
import { writeFileSync } from "fs";
|
|
466
|
+
import { join as join3 } from "path";
|
|
467
|
+
import { homedir as homedir2 } from "os";
|
|
468
|
+
import ora3 from "ora";
|
|
469
|
+
import prompts from "prompts";
|
|
470
|
+
async function loginCommand(options) {
|
|
471
|
+
const spinner = ora3({
|
|
472
|
+
text: "Authenticating...",
|
|
473
|
+
spinner: "dots",
|
|
474
|
+
stream: process.stdout
|
|
475
|
+
});
|
|
476
|
+
try {
|
|
477
|
+
let apiKey = options.key;
|
|
478
|
+
if (!apiKey) {
|
|
479
|
+
const response = await prompts({
|
|
480
|
+
type: "password",
|
|
481
|
+
name: "key",
|
|
482
|
+
message: "Enter your VeeStack API key:",
|
|
483
|
+
validate: (value) => value.length > 0 || "API key is required"
|
|
484
|
+
});
|
|
485
|
+
if (!response.key) {
|
|
486
|
+
console.log("\u274C Login cancelled");
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
apiKey = response.key;
|
|
490
|
+
}
|
|
491
|
+
spinner.start("Validating API key...");
|
|
492
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
493
|
+
const configDir = join3(homedir2(), ".veestack");
|
|
494
|
+
const configFile = join3(configDir, "config.json");
|
|
495
|
+
try {
|
|
496
|
+
const fs = await import("fs/promises");
|
|
497
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
498
|
+
await fs.writeFile(configFile, JSON.stringify({ apiKey }, null, 2));
|
|
499
|
+
} catch {
|
|
500
|
+
const fs = await import("fs");
|
|
501
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
502
|
+
writeFileSync(configFile, JSON.stringify({ apiKey }, null, 2));
|
|
503
|
+
}
|
|
504
|
+
spinner.succeed("Authenticated successfully");
|
|
505
|
+
console.log("\u2705 API key saved to ~/.veestack/config.json");
|
|
506
|
+
} catch (error) {
|
|
507
|
+
spinner.fail("Authentication failed");
|
|
508
|
+
console.error(error);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/commands/status.ts
|
|
514
|
+
import chalk4 from "chalk";
|
|
515
|
+
async function statusCommand() {
|
|
516
|
+
console.log(chalk4.blue("\n\u{1F4CA} VeeStack CLI Status\n"));
|
|
517
|
+
console.log(chalk4.gray("\u2500".repeat(40)));
|
|
518
|
+
const authenticated = isAuthenticated();
|
|
519
|
+
const tier = getSubscriptionTier();
|
|
520
|
+
const active = isSubscriptionActive();
|
|
521
|
+
if (!authenticated) {
|
|
522
|
+
console.log(chalk4.red("\n\u274C Not authenticated"));
|
|
523
|
+
console.log(chalk4.yellow("\nTo use VeeStack CLI, you need to login:"));
|
|
524
|
+
console.log(chalk4.cyan(" veestack login"));
|
|
525
|
+
console.log(chalk4.gray("\nGet your API key from: https://veestack.tools/dashboard"));
|
|
526
|
+
console.log(chalk4.gray("\u2500".repeat(40) + "\n"));
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
console.log(chalk4.green("\n\u2705 Authenticated"));
|
|
530
|
+
if (tier === "pro") {
|
|
531
|
+
console.log(chalk4.cyan("\u{1F48E} Subscription: Pro"));
|
|
532
|
+
if (!active) {
|
|
533
|
+
console.log(chalk4.red("\u26A0\uFE0F Status: Expired"));
|
|
534
|
+
console.log(chalk4.yellow("\nPlease renew your subscription:"));
|
|
535
|
+
console.log(chalk4.cyan(" https://veestack.tools/pricing"));
|
|
536
|
+
} else {
|
|
537
|
+
console.log(chalk4.green("\u2713 Status: Active"));
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
console.log(chalk4.blue("\u{1F4E6} Subscription: Free"));
|
|
541
|
+
console.log(chalk4.green("\u2713 Status: Active"));
|
|
542
|
+
console.log(chalk4.gray("\nUpgrade to Pro for advanced features:"));
|
|
543
|
+
console.log(chalk4.cyan(" https://veestack.tools/pricing"));
|
|
544
|
+
}
|
|
545
|
+
console.log(chalk4.gray("\u2500".repeat(40) + "\n"));
|
|
546
|
+
console.log(chalk4.white("Available commands:"));
|
|
547
|
+
console.log(chalk4.cyan(" veestack scan ") + chalk4.gray(" - Scan project (Free)"));
|
|
548
|
+
console.log(chalk4.cyan(" veestack upload ") + chalk4.gray(" - Upload to cloud (Free)"));
|
|
549
|
+
console.log(chalk4.cyan(" veestack status ") + chalk4.gray(" - Check status (Free)"));
|
|
550
|
+
if (tier === "pro") {
|
|
551
|
+
console.log(chalk4.cyan(" veestack analyze ") + chalk4.gray(" - Advanced analysis (Pro)"));
|
|
552
|
+
}
|
|
553
|
+
console.log(chalk4.gray("\n\u2500".repeat(40) + "\n"));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/index.ts
|
|
557
|
+
var program = new Command();
|
|
558
|
+
program.name("veestack").description("VeeStack CLI - Technical Stack Visibility Tool").version("3.0.3");
|
|
559
|
+
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);
|
|
560
|
+
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);
|
|
561
|
+
program.command("login").description("Authenticate with VeeStack server").option("-k, --key <apiKey>", "API key").action(loginCommand);
|
|
562
|
+
program.command("status").description("Check authentication and subscription status").action(statusCommand);
|
|
563
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veestack-tools/cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
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": "3.0.
|
|
15
|
-
"@veestack-tools/utils": "3.0.
|
|
16
|
-
"@veestack-tools/engine": "3.0.
|
|
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
|
package/src/commands/login.ts
DELETED
|
@@ -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
|
-
}
|