@quantracode/vibecheck 0.0.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +64 -0
  2. package/dist/index.js +1110 -21
  3. package/package.json +5 -1
package/README.md CHANGED
@@ -63,6 +63,70 @@ vibecheck scan --emit-intent-map
63
63
 
64
64
  # Explain a scan report
65
65
  vibecheck explain ./scan.json
66
+
67
+ # Start the web viewer
68
+ vibecheck view
69
+
70
+ # Open viewer with specific artifact
71
+ vibecheck view -a ./scan.json
72
+ ```
73
+
74
+ ## View Command
75
+
76
+ The `view` command starts a local web viewer to explore scan results interactively.
77
+
78
+ ```bash
79
+ # Start viewer (auto-detects artifacts in current directory)
80
+ vibecheck view
81
+
82
+ # Specify artifact file explicitly
83
+ vibecheck view -a ./vibecheck-scan.json
84
+ vibecheck view --artifact ./scan-results.json
85
+
86
+ # Use a different port
87
+ vibecheck view --port 8080
88
+
89
+ # Don't auto-open browser
90
+ vibecheck view --no-open
91
+
92
+ # Force update the viewer to latest version
93
+ vibecheck view --update
94
+
95
+ # Clear cached viewer files
96
+ vibecheck view --clear-cache
97
+ ```
98
+
99
+ ### View Command Options
100
+
101
+ | Option | Description | Default |
102
+ |--------|-------------|---------|
103
+ | `-p, --port <port>` | Port to run the viewer on | `3000` |
104
+ | `-a, --artifact <path>` | Path to artifact file to open | Auto-detected |
105
+ | `--no-open` | Don't automatically open the browser | Opens browser |
106
+ | `--update` | Force update the viewer to latest version | - |
107
+ | `--clear-cache` | Clear the cached viewer and exit | - |
108
+
109
+ ### How It Works
110
+
111
+ 1. **Auto-download**: The viewer is automatically downloaded from npm on first run and cached in `~/.vibecheck/viewer/`
112
+ 2. **Auto-detect artifacts**: Looks for scan artifacts in common locations:
113
+ - `vibecheck-artifacts/artifact.json`
114
+ - `vibecheck-artifact.json`
115
+ - `.vibecheck/artifact.json`
116
+ - `scan-results.json`
117
+ 3. **Auto-load**: When an artifact is detected, it's automatically loaded into the viewer
118
+ 4. **Local-only**: The viewer runs entirely on your machine with no external connections
119
+
120
+ ### Workflow Example
121
+
122
+ ```bash
123
+ # 1. Run a scan
124
+ vibecheck scan --out vibecheck-artifacts/artifact.json
125
+
126
+ # 2. View results (artifact auto-detected)
127
+ vibecheck view
128
+
129
+ # Browser opens to http://localhost:3000 with results loaded
66
130
  ```
67
131
 
68
132
  ### Command Line Options
package/dist/index.js CHANGED
@@ -414,7 +414,7 @@ function readJsonSync(filePath) {
414
414
  }
415
415
 
416
416
  // src/utils/fingerprint.ts
417
- import crypto from "crypto";
417
+ import crypto2 from "crypto";
418
418
  function generateFingerprint(parts) {
419
419
  const data = [
420
420
  parts.ruleId,
@@ -423,14 +423,14 @@ function generateFingerprint(parts) {
423
423
  parts.route ?? "",
424
424
  parts.startLine?.toString() ?? ""
425
425
  ].join("::");
426
- return crypto.createHash("sha256").update(data).digest("hex").slice(0, 16);
426
+ return crypto2.createHash("sha256").update(data).digest("hex").slice(0, 16);
427
427
  }
428
428
  function generateFindingId(parts) {
429
429
  const fp = generateFingerprint(parts);
430
430
  return `${parts.ruleId.toLowerCase()}-${fp.slice(0, 8)}`;
431
431
  }
432
432
  function hashPath(dirPath) {
433
- return crypto.createHash("sha256").update(dirPath).digest("hex").slice(0, 16);
433
+ return crypto2.createHash("sha256").update(dirPath).digest("hex").slice(0, 16);
434
434
  }
435
435
 
436
436
  // src/utils/git-info.ts
@@ -3928,13 +3928,13 @@ var abusePack = {
3928
3928
  };
3929
3929
 
3930
3930
  // src/phase3/proof-trace-builder.ts
3931
- import crypto2 from "crypto";
3931
+ import crypto3 from "crypto";
3932
3932
  import path3 from "path";
3933
3933
  import { SyntaxKind as SyntaxKind2 } from "ts-morph";
3934
3934
  var MAX_TRACE_DEPTH = 2;
3935
3935
  function generateRouteId(routePath, method, file) {
3936
3936
  const normalized = `${method}:${routePath}:${file}`.toLowerCase();
3937
- return crypto2.createHash("sha256").update(normalized).digest("hex").slice(0, 12);
3937
+ return crypto3.createHash("sha256").update(normalized).digest("hex").slice(0, 12);
3938
3938
  }
3939
3939
  function filePathToRoutePath(filePath) {
3940
3940
  const normalized = filePath.replace(/\\/g, "/");
@@ -4296,7 +4296,7 @@ function buildAllProofTraces(ctx, routes) {
4296
4296
  }
4297
4297
 
4298
4298
  // src/phase3/intent-miner.ts
4299
- import crypto3 from "crypto";
4299
+ import crypto4 from "crypto";
4300
4300
  import path4 from "path";
4301
4301
  import { SyntaxKind as SyntaxKind3 } from "ts-morph";
4302
4302
  var INTENT_PATTERNS = [
@@ -4331,7 +4331,7 @@ var SECURITY_IMPORTS = [
4331
4331
  ];
4332
4332
  function generateIntentId(type, file, line, evidence) {
4333
4333
  const normalized = `${type}:${file}:${line}:${evidence.slice(0, 50)}`.toLowerCase();
4334
- return crypto3.createHash("sha256").update(normalized).digest("hex").slice(0, 12);
4334
+ return crypto4.createHash("sha256").update(normalized).digest("hex").slice(0, 12);
4335
4335
  }
4336
4336
  function mineIntentClaims(ctx, sourceFile, routes) {
4337
4337
  const claims = [];
@@ -4525,7 +4525,7 @@ function mineAllIntentClaims(ctx, routes) {
4525
4525
  }
4526
4526
 
4527
4527
  // src/phase3/scanners/comment-claim-unproven.ts
4528
- import crypto4 from "crypto";
4528
+ import crypto5 from "crypto";
4529
4529
  var RULE_ID20 = "VC-HALL-010";
4530
4530
  async function scanCommentClaimUnproven(ctx) {
4531
4531
  const findings = [];
@@ -4655,15 +4655,15 @@ function generateRemediation2(claim) {
4655
4655
  }
4656
4656
  }
4657
4657
  function generateFindingId2(claim) {
4658
- return `f-${crypto4.randomUUID().slice(0, 8)}`;
4658
+ return `f-${crypto5.randomUUID().slice(0, 8)}`;
4659
4659
  }
4660
4660
  function generateFingerprint2(claim) {
4661
4661
  const data = `${RULE_ID20}:${claim.location.file}:${claim.location.startLine}:${claim.type}`;
4662
- return `sha256:${crypto4.createHash("sha256").update(data).digest("hex")}`;
4662
+ return `sha256:${crypto5.createHash("sha256").update(data).digest("hex")}`;
4663
4663
  }
4664
4664
 
4665
4665
  // src/phase3/scanners/middleware-assumed-not-matching.ts
4666
- import crypto5 from "crypto";
4666
+ import crypto6 from "crypto";
4667
4667
  var RULE_ID21 = "VC-HALL-011";
4668
4668
  var MIDDLEWARE_EXPECTATION_SIGNALS = [
4669
4669
  // Comments
@@ -4799,15 +4799,15 @@ function generateRemediation3(route, currentMatchers) {
4799
4799
  return `Update the middleware matcher to include this route. Current matchers: ${JSON.stringify(currentMatchers)}. Consider adding "${suggestedPattern}" or add explicit auth in the handler.`;
4800
4800
  }
4801
4801
  function generateFindingId3(route) {
4802
- return `f-${crypto5.randomUUID().slice(0, 8)}`;
4802
+ return `f-${crypto6.randomUUID().slice(0, 8)}`;
4803
4803
  }
4804
4804
  function generateFingerprint3(route) {
4805
4805
  const data = `${RULE_ID21}:${route.file}:${route.method}:${route.path}`;
4806
- return `sha256:${crypto5.createHash("sha256").update(data).digest("hex")}`;
4806
+ return `sha256:${crypto6.createHash("sha256").update(data).digest("hex")}`;
4807
4807
  }
4808
4808
 
4809
4809
  // src/phase3/scanners/validation-claimed-missing.ts
4810
- import crypto6 from "crypto";
4810
+ import crypto7 from "crypto";
4811
4811
  import path5 from "path";
4812
4812
  var RULE_ID22 = "VC-HALL-012";
4813
4813
  async function scanValidationClaimedMissing(ctx) {
@@ -4908,7 +4908,7 @@ function createFinding(route, claim, issueType, description, additionalEvidence)
4908
4908
  });
4909
4909
  }
4910
4910
  return {
4911
- id: `f-${crypto6.randomUUID().slice(0, 8)}`,
4911
+ id: `f-${crypto7.randomUUID().slice(0, 8)}`,
4912
4912
  severity: "medium",
4913
4913
  confidence: 0.8,
4914
4914
  category: "hallucinations",
@@ -4961,7 +4961,7 @@ function checkUnusedValidationImports(ctx, routes, claims) {
4961
4961
  const fileRoutes = routes.filter((r) => r.file === claim.location.file);
4962
4962
  if (fileRoutes.length > 0) {
4963
4963
  findings.push({
4964
- id: `f-${crypto6.randomUUID().slice(0, 8)}`,
4964
+ id: `f-${crypto7.randomUUID().slice(0, 8)}`,
4965
4965
  severity: "medium",
4966
4966
  confidence: 0.7,
4967
4967
  category: "hallucinations",
@@ -4980,7 +4980,7 @@ function checkUnusedValidationImports(ctx, routes, claims) {
4980
4980
  remediation: {
4981
4981
  recommendedFix: "Define validation schemas using the imported library, or remove the unused import."
4982
4982
  },
4983
- fingerprint: `sha256:${crypto6.createHash("sha256").update(`${RULE_ID22}:${claim.location.file}:import_unused`).digest("hex")}`
4983
+ fingerprint: `sha256:${crypto7.createHash("sha256").update(`${RULE_ID22}:${claim.location.file}:import_unused`).digest("hex")}`
4984
4984
  });
4985
4985
  }
4986
4986
  }
@@ -4989,11 +4989,11 @@ function checkUnusedValidationImports(ctx, routes, claims) {
4989
4989
  }
4990
4990
  function generateFingerprint4(route, issueType) {
4991
4991
  const data = `${RULE_ID22}:${route.file}:${route.method}:${issueType}`;
4992
- return `sha256:${crypto6.createHash("sha256").update(data).digest("hex")}`;
4992
+ return `sha256:${crypto7.createHash("sha256").update(data).digest("hex")}`;
4993
4993
  }
4994
4994
 
4995
4995
  // src/phase3/scanners/auth-by-ui-server-gap.ts
4996
- import crypto7 from "crypto";
4996
+ import crypto8 from "crypto";
4997
4997
  import path6 from "path";
4998
4998
  var RULE_ID23 = "VC-AUTH-010";
4999
4999
  var CLIENT_AUTH_PATTERNS = [
@@ -5049,7 +5049,7 @@ async function scanAuthByUiServerGap(ctx) {
5049
5049
  if (matchingUnprotected) {
5050
5050
  const clientAuthLocation = findClientAuthLocation(sourceFile, fullText);
5051
5051
  findings.push({
5052
- id: `f-${crypto7.randomUUID().slice(0, 8)}`,
5052
+ id: `f-${crypto8.randomUUID().slice(0, 8)}`,
5053
5053
  severity: "critical",
5054
5054
  confidence: 0.85,
5055
5055
  category: "auth",
@@ -5185,7 +5185,7 @@ Client-side auth checks are for UX only and must never be the sole protection me
5185
5185
  }
5186
5186
  function generateFingerprint5(componentFile, route) {
5187
5187
  const data = `${RULE_ID23}:${componentFile}:${route.file}:${route.method}`;
5188
- return `sha256:${crypto7.createHash("sha256").update(data).digest("hex")}`;
5188
+ return `sha256:${crypto8.createHash("sha256").update(data).digest("hex")}`;
5189
5189
  }
5190
5190
 
5191
5191
  // src/phase3/scanners/index.ts
@@ -7899,6 +7899,1093 @@ Examples:
7899
7899
  });
7900
7900
  }
7901
7901
 
7902
+ // src/commands/view.ts
7903
+ import { existsSync as existsSync3 } from "fs";
7904
+ import { resolve as resolve2, join as join3 } from "path";
7905
+
7906
+ // src/utils/static-server.ts
7907
+ import { createServer } from "http";
7908
+ import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
7909
+ import { join, extname } from "path";
7910
+ var MIME_TYPES = {
7911
+ ".html": "text/html; charset=utf-8",
7912
+ ".js": "application/javascript; charset=utf-8",
7913
+ ".mjs": "application/javascript; charset=utf-8",
7914
+ ".css": "text/css; charset=utf-8",
7915
+ ".json": "application/json; charset=utf-8",
7916
+ ".png": "image/png",
7917
+ ".jpg": "image/jpeg",
7918
+ ".jpeg": "image/jpeg",
7919
+ ".gif": "image/gif",
7920
+ ".svg": "image/svg+xml",
7921
+ ".ico": "image/x-icon",
7922
+ ".woff": "font/woff",
7923
+ ".woff2": "font/woff2",
7924
+ ".ttf": "font/ttf",
7925
+ ".eot": "application/vnd.ms-fontobject",
7926
+ ".txt": "text/plain; charset=utf-8",
7927
+ ".xml": "application/xml",
7928
+ ".webp": "image/webp",
7929
+ ".webm": "video/webm",
7930
+ ".mp4": "video/mp4",
7931
+ ".map": "application/json"
7932
+ };
7933
+ function getMimeType(filePath) {
7934
+ const ext = extname(filePath).toLowerCase();
7935
+ return MIME_TYPES[ext] || "application/octet-stream";
7936
+ }
7937
+ function resolveFilePath(staticDir, urlPath) {
7938
+ let decodedPath;
7939
+ try {
7940
+ decodedPath = decodeURIComponent(urlPath);
7941
+ } catch {
7942
+ return null;
7943
+ }
7944
+ const queryIndex = decodedPath.indexOf("?");
7945
+ if (queryIndex !== -1) {
7946
+ decodedPath = decodedPath.substring(0, queryIndex);
7947
+ }
7948
+ let filePath = join(staticDir, decodedPath === "/" ? "index.html" : decodedPath);
7949
+ const normalizedPath = join(filePath);
7950
+ if (!normalizedPath.startsWith(staticDir)) {
7951
+ return null;
7952
+ }
7953
+ if (existsSync(filePath)) {
7954
+ const stat = statSync(filePath);
7955
+ if (stat.isDirectory()) {
7956
+ filePath = join(filePath, "index.html");
7957
+ if (!existsSync(filePath)) {
7958
+ return null;
7959
+ }
7960
+ }
7961
+ return filePath;
7962
+ }
7963
+ if (existsSync(filePath + ".html")) {
7964
+ return filePath + ".html";
7965
+ }
7966
+ const indexPath = join(staticDir, "index.html");
7967
+ if (existsSync(indexPath)) {
7968
+ return indexPath;
7969
+ }
7970
+ return null;
7971
+ }
7972
+ function handleRequest(staticDir, req, res) {
7973
+ const urlPath = req.url || "/";
7974
+ const filePath = resolveFilePath(staticDir, urlPath);
7975
+ if (!filePath) {
7976
+ res.writeHead(404, { "Content-Type": "text/plain" });
7977
+ res.end("Not Found");
7978
+ return;
7979
+ }
7980
+ try {
7981
+ const content = readFileSync2(filePath);
7982
+ const contentType = getMimeType(filePath);
7983
+ res.writeHead(200, {
7984
+ "Content-Type": contentType,
7985
+ "Content-Length": content.length,
7986
+ "Cache-Control": "no-cache",
7987
+ "X-Content-Type-Options": "nosniff"
7988
+ });
7989
+ res.end(content);
7990
+ } catch (err) {
7991
+ res.writeHead(500, { "Content-Type": "text/plain" });
7992
+ res.end("Internal Server Error");
7993
+ }
7994
+ }
7995
+ async function startStaticServer(options) {
7996
+ const { staticDir, port, host = "127.0.0.1", artifactPath } = options;
7997
+ return new Promise((resolve3, reject) => {
7998
+ const server = createServer((req, res) => {
7999
+ const urlPath = req.url || "/";
8000
+ if (urlPath === "/__vibecheck__/artifact" || urlPath === "/__vibecheck__/artifact.json") {
8001
+ if (artifactPath && existsSync(artifactPath)) {
8002
+ try {
8003
+ const content = readFileSync2(artifactPath);
8004
+ res.writeHead(200, {
8005
+ "Content-Type": "application/json",
8006
+ "Content-Length": content.length,
8007
+ "Cache-Control": "no-cache",
8008
+ "Access-Control-Allow-Origin": "*"
8009
+ });
8010
+ res.end(content);
8011
+ return;
8012
+ } catch {
8013
+ res.writeHead(500, { "Content-Type": "application/json" });
8014
+ res.end(JSON.stringify({ error: "Failed to read artifact" }));
8015
+ return;
8016
+ }
8017
+ } else {
8018
+ res.writeHead(404, { "Content-Type": "application/json" });
8019
+ res.end(JSON.stringify({ error: "No artifact available" }));
8020
+ return;
8021
+ }
8022
+ }
8023
+ if (req.method === "OPTIONS" && urlPath.startsWith("/__vibecheck__/")) {
8024
+ res.writeHead(204, {
8025
+ "Access-Control-Allow-Origin": "*",
8026
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
8027
+ "Access-Control-Allow-Headers": "Content-Type"
8028
+ });
8029
+ res.end();
8030
+ return;
8031
+ }
8032
+ handleRequest(staticDir, req, res);
8033
+ });
8034
+ server.on("error", (err) => {
8035
+ if (err.code === "EADDRINUSE") {
8036
+ reject(new Error(`Port ${port} is already in use`));
8037
+ } else {
8038
+ reject(err);
8039
+ }
8040
+ });
8041
+ server.listen(port, host, () => {
8042
+ const url = `http://${host}:${port}`;
8043
+ resolve3({
8044
+ server,
8045
+ url,
8046
+ stop: () => new Promise((res) => {
8047
+ server.close(() => res());
8048
+ })
8049
+ });
8050
+ });
8051
+ });
8052
+ }
8053
+ async function findAvailablePort(startPort, maxAttempts = 10) {
8054
+ for (let i = 0; i < maxAttempts; i++) {
8055
+ const port = startPort + i;
8056
+ const available = await isPortAvailable(port);
8057
+ if (available) {
8058
+ return port;
8059
+ }
8060
+ }
8061
+ throw new Error(
8062
+ `Could not find available port in range ${startPort}-${startPort + maxAttempts - 1}`
8063
+ );
8064
+ }
8065
+ function isPortAvailable(port) {
8066
+ return new Promise((resolve3) => {
8067
+ const server = createServer();
8068
+ server.once("error", (err) => {
8069
+ if (err.code === "EADDRINUSE") {
8070
+ resolve3(false);
8071
+ } else {
8072
+ resolve3(false);
8073
+ }
8074
+ });
8075
+ server.once("listening", () => {
8076
+ server.close(() => resolve3(true));
8077
+ });
8078
+ server.listen(port, "127.0.0.1");
8079
+ });
8080
+ }
8081
+
8082
+ // src/utils/viewer-cache.ts
8083
+ import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync3, readFileSync as readFileSync3, rmSync } from "fs";
8084
+ import { join as join2 } from "path";
8085
+ import { homedir } from "os";
8086
+ import { execSync as execSync2 } from "child_process";
8087
+ import { extract } from "tar";
8088
+ var CACHE_DIR = join2(homedir(), ".vibecheck");
8089
+ var VIEWER_DIR = join2(CACHE_DIR, "viewer");
8090
+ var VIEWER_DIST_DIR = join2(VIEWER_DIR, "dist");
8091
+ var VERSION_FILE = join2(VIEWER_DIR, ".version");
8092
+ var VIEWER_PACKAGE = "@quantracode/vibecheck-viewer";
8093
+ function getInstalledVersion() {
8094
+ if (!existsSync2(VERSION_FILE)) {
8095
+ return null;
8096
+ }
8097
+ try {
8098
+ return readFileSync3(VERSION_FILE, "utf-8").trim();
8099
+ } catch {
8100
+ return null;
8101
+ }
8102
+ }
8103
+ function setInstalledVersion(version) {
8104
+ writeFileSync3(VERSION_FILE, version, "utf-8");
8105
+ }
8106
+ async function getLatestVersion() {
8107
+ try {
8108
+ const result = execSync2(`npm view ${VIEWER_PACKAGE} version`, {
8109
+ encoding: "utf-8",
8110
+ stdio: ["pipe", "pipe", "pipe"]
8111
+ });
8112
+ return result.trim();
8113
+ } catch {
8114
+ throw new Error(
8115
+ `Failed to fetch latest version of ${VIEWER_PACKAGE}. Check your network connection.`
8116
+ );
8117
+ }
8118
+ }
8119
+ async function downloadPackage(version) {
8120
+ const tarballUrl = execSync2(
8121
+ `npm view ${VIEWER_PACKAGE}@${version} dist.tarball`,
8122
+ {
8123
+ encoding: "utf-8",
8124
+ stdio: ["pipe", "pipe", "pipe"]
8125
+ }
8126
+ ).trim();
8127
+ if (existsSync2(VIEWER_DIR)) {
8128
+ rmSync(VIEWER_DIR, { recursive: true, force: true });
8129
+ }
8130
+ mkdirSync(VIEWER_DIR, { recursive: true });
8131
+ const response = await fetch(tarballUrl);
8132
+ if (!response.ok) {
8133
+ throw new Error(`Failed to download viewer: ${response.statusText}`);
8134
+ }
8135
+ const tarPath = join2(CACHE_DIR, "viewer.tgz");
8136
+ const buffer = Buffer.from(await response.arrayBuffer());
8137
+ writeFileSync3(tarPath, buffer);
8138
+ await extract({
8139
+ file: tarPath,
8140
+ cwd: VIEWER_DIR,
8141
+ strip: 1
8142
+ // Remove the 'package' directory prefix
8143
+ });
8144
+ rmSync(tarPath, { force: true });
8145
+ }
8146
+ async function ensureViewer(forceUpdate = false) {
8147
+ mkdirSync(CACHE_DIR, { recursive: true });
8148
+ const installedVersion = getInstalledVersion();
8149
+ let latestVersion;
8150
+ try {
8151
+ latestVersion = await getLatestVersion();
8152
+ } catch (error) {
8153
+ if (installedVersion && existsSync2(join2(VIEWER_DIST_DIR, "index.html"))) {
8154
+ return {
8155
+ path: VIEWER_DIST_DIR,
8156
+ version: installedVersion
8157
+ };
8158
+ }
8159
+ throw error;
8160
+ }
8161
+ const needsUpdate = forceUpdate || !installedVersion || installedVersion !== latestVersion || !existsSync2(join2(VIEWER_DIST_DIR, "index.html"));
8162
+ if (needsUpdate) {
8163
+ console.log(
8164
+ installedVersion ? `Updating viewer from ${installedVersion} to ${latestVersion}...` : `Installing viewer v${latestVersion}...`
8165
+ );
8166
+ await downloadPackage(latestVersion);
8167
+ setInstalledVersion(latestVersion);
8168
+ console.log("Viewer installed successfully.\n");
8169
+ }
8170
+ return {
8171
+ path: VIEWER_DIST_DIR,
8172
+ version: latestVersion
8173
+ };
8174
+ }
8175
+ function clearViewerCache() {
8176
+ if (existsSync2(VIEWER_DIR)) {
8177
+ rmSync(VIEWER_DIR, { recursive: true, force: true });
8178
+ console.log("Viewer cache cleared.");
8179
+ } else {
8180
+ console.log("No viewer cache to clear.");
8181
+ }
8182
+ }
8183
+
8184
+ // src/utils/open-browser.ts
8185
+ import { spawn } from "child_process";
8186
+ function openBrowser(url) {
8187
+ const platform = process.platform;
8188
+ let command;
8189
+ let args;
8190
+ switch (platform) {
8191
+ case "win32":
8192
+ command = "cmd";
8193
+ args = ["/c", "start", "", url.replace(/&/g, "^&")];
8194
+ break;
8195
+ case "darwin":
8196
+ command = "open";
8197
+ args = [url];
8198
+ break;
8199
+ default:
8200
+ command = "xdg-open";
8201
+ args = [url];
8202
+ break;
8203
+ }
8204
+ try {
8205
+ const child = spawn(command, args, {
8206
+ detached: true,
8207
+ stdio: "ignore",
8208
+ shell: platform === "win32"
8209
+ });
8210
+ child.unref();
8211
+ } catch {
8212
+ console.log(`Could not automatically open browser.`);
8213
+ console.log(`Please open ${url} manually.`);
8214
+ }
8215
+ }
8216
+
8217
+ // src/commands/view.ts
8218
+ var DEFAULT_PORT = 3e3;
8219
+ function printBanner(url, hasArtifact) {
8220
+ const width = 76;
8221
+ console.log(`
8222
+ \x1B[36m \u256D${"\u2500".repeat(width)}\u256E\x1B[0m`);
8223
+ console.log(
8224
+ `\x1B[36m \u2502\x1B[0m \x1B[1mVIBECHECK VIEWER\x1B[0m${" ".repeat(width - 19)}\x1B[36m\u2502\x1B[0m`
8225
+ );
8226
+ console.log(`\x1B[36m \u2570${"\u2500".repeat(width)}\u256F\x1B[0m
8227
+ `);
8228
+ console.log(` \x1B[32m\u25CF\x1B[0m Server running at: \x1B[1m\x1B[36m${url}\x1B[0m
8229
+ `);
8230
+ if (hasArtifact) {
8231
+ console.log(` \x1B[32m\u25CF\x1B[0m Scan artifact will be loaded automatically.
8232
+ `);
8233
+ } else {
8234
+ console.log(` \x1B[90mDrag and drop a scan artifact (JSON) onto the page to view results.\x1B[0m`);
8235
+ console.log(` \x1B[90mOr run: vibecheck scan --out artifact.json\x1B[0m
8236
+ `);
8237
+ }
8238
+ console.log(` \x1B[90mPress Ctrl+C to stop the server.\x1B[0m
8239
+ `);
8240
+ }
8241
+ async function runViewCommand(options) {
8242
+ if (options.clearCache) {
8243
+ clearViewerCache();
8244
+ return;
8245
+ }
8246
+ const requestedPort = parseInt(options.port, 10);
8247
+ if (isNaN(requestedPort) || requestedPort < 1 || requestedPort > 65535) {
8248
+ console.error(`\x1B[31mError: Invalid port number: ${options.port}\x1B[0m`);
8249
+ console.error("Port must be a number between 1 and 65535.");
8250
+ process.exit(1);
8251
+ }
8252
+ const portAvailable = await isPortAvailable(requestedPort);
8253
+ let port = requestedPort;
8254
+ if (!portAvailable) {
8255
+ console.log(`\x1B[33mPort ${requestedPort} is already in use.\x1B[0m`);
8256
+ try {
8257
+ port = await findAvailablePort(requestedPort + 1);
8258
+ console.log(`Using port ${port} instead.
8259
+ `);
8260
+ } catch (error) {
8261
+ console.error(
8262
+ `\x1B[31mError: Could not find an available port.\x1B[0m`
8263
+ );
8264
+ console.error(`
8265
+ Try specifying a different port:`);
8266
+ console.error(` vibecheck view --port 8080
8267
+ `);
8268
+ console.error(`Or stop the process using port ${requestedPort}:`);
8269
+ if (process.platform === "win32") {
8270
+ console.error(` netstat -ano | findstr :${requestedPort}`);
8271
+ console.error(` taskkill /PID <pid> /F`);
8272
+ } else {
8273
+ console.error(` lsof -i :${requestedPort}`);
8274
+ console.error(` kill <pid>`);
8275
+ }
8276
+ process.exit(1);
8277
+ }
8278
+ }
8279
+ let viewerInfo;
8280
+ try {
8281
+ viewerInfo = await ensureViewer(options.update);
8282
+ } catch (error) {
8283
+ console.error(`\x1B[31mError: Failed to install viewer.\x1B[0m`);
8284
+ if (error instanceof Error) {
8285
+ console.error(error.message);
8286
+ }
8287
+ console.error(`
8288
+ Check your network connection and try again.`);
8289
+ console.error(`You may also try: vibecheck view --clear-cache
8290
+ `);
8291
+ process.exit(1);
8292
+ }
8293
+ if (!existsSync3(join3(viewerInfo.path, "index.html"))) {
8294
+ console.error(`\x1B[31mError: Viewer files not found.\x1B[0m`);
8295
+ console.error(`Try reinstalling: vibecheck view --update
8296
+ `);
8297
+ process.exit(1);
8298
+ }
8299
+ let artifactPath;
8300
+ if (options.artifact) {
8301
+ artifactPath = resolve2(options.artifact);
8302
+ if (!existsSync3(artifactPath)) {
8303
+ console.log(
8304
+ `\x1B[33mWarning: Artifact file not found: ${options.artifact}\x1B[0m
8305
+ `
8306
+ );
8307
+ artifactPath = void 0;
8308
+ }
8309
+ } else {
8310
+ const commonPaths = [
8311
+ "vibecheck-artifacts/artifact.json",
8312
+ "vibecheck-artifact.json",
8313
+ ".vibecheck/artifact.json",
8314
+ "scan-results.json"
8315
+ ];
8316
+ for (const p of commonPaths) {
8317
+ const fullPath = resolve2(p);
8318
+ if (existsSync3(fullPath)) {
8319
+ artifactPath = fullPath;
8320
+ console.log(`\x1B[90mAuto-detected artifact: ${p}\x1B[0m`);
8321
+ break;
8322
+ }
8323
+ }
8324
+ }
8325
+ let serverResult;
8326
+ try {
8327
+ serverResult = await startStaticServer({
8328
+ staticDir: viewerInfo.path,
8329
+ port,
8330
+ artifactPath
8331
+ });
8332
+ } catch (error) {
8333
+ console.error(`\x1B[31mError: Failed to start server.\x1B[0m`);
8334
+ if (error instanceof Error) {
8335
+ console.error(error.message);
8336
+ }
8337
+ process.exit(1);
8338
+ }
8339
+ const url = serverResult.url;
8340
+ printBanner(url, !!artifactPath);
8341
+ if (options.open) {
8342
+ setTimeout(() => {
8343
+ openBrowser(url);
8344
+ }, 300);
8345
+ }
8346
+ const shutdown = async () => {
8347
+ console.log("\n\x1B[90mShutting down viewer...\x1B[0m");
8348
+ await serverResult.stop();
8349
+ process.exit(0);
8350
+ };
8351
+ process.on("SIGINT", shutdown);
8352
+ process.on("SIGTERM", shutdown);
8353
+ await new Promise(() => {
8354
+ });
8355
+ }
8356
+ function registerViewCommand(program2) {
8357
+ program2.command("view").description("Start the VibeCheck web viewer to explore scan results").option(
8358
+ "-p, --port <port>",
8359
+ "Port to run the viewer on",
8360
+ String(DEFAULT_PORT)
8361
+ ).option("--no-open", "Don't automatically open the browser").option("--update", "Force update the viewer to latest version").option("--clear-cache", "Clear the cached viewer and exit").option(
8362
+ "-a, --artifact <path>",
8363
+ "Path to artifact file to open (optional)"
8364
+ ).addHelpText(
8365
+ "after",
8366
+ `
8367
+ Examples:
8368
+ $ vibecheck view Start viewer on default port (3000)
8369
+ $ vibecheck view --port 8080 Start viewer on port 8080
8370
+ $ vibecheck view --no-open Start without opening browser
8371
+ $ vibecheck view --update Update viewer to latest version
8372
+ $ vibecheck view --clear-cache Clear cached viewer files
8373
+
8374
+ Workflow:
8375
+ 1. Run a scan: vibecheck scan --out scan.json
8376
+ 2. Start viewer: vibecheck view
8377
+ 3. Drop scan.json onto the page to view results
8378
+ `
8379
+ ).action(runViewCommand);
8380
+ }
8381
+
8382
+ // src/commands/license.ts
8383
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync2, unlinkSync } from "fs";
8384
+ import { homedir as homedir2 } from "os";
8385
+ import { join as join4 } from "path";
8386
+ import chalk from "chalk";
8387
+ import readline from "readline";
8388
+
8389
+ // ../license/dist/types.js
8390
+ var PLAN_FEATURES = {
8391
+ free: [],
8392
+ pro: [
8393
+ "baseline",
8394
+ "policy_customization",
8395
+ "abuse_classification",
8396
+ "architecture_maps",
8397
+ "signed_export"
8398
+ ],
8399
+ enterprise: [
8400
+ "baseline",
8401
+ "policy_customization",
8402
+ "abuse_classification",
8403
+ "architecture_maps",
8404
+ "signed_export",
8405
+ "sso",
8406
+ "audit_logs",
8407
+ "custom_rules"
8408
+ ]
8409
+ };
8410
+ var PLAN_NAMES = {
8411
+ free: "Free",
8412
+ pro: "Pro",
8413
+ enterprise: "Enterprise"
8414
+ };
8415
+ function isDemoLicense(licenseId) {
8416
+ return licenseId.startsWith("demo-") || licenseId.startsWith("trial-");
8417
+ }
8418
+
8419
+ // ../license/dist/constants.js
8420
+ var VIBECHECK_PUBLIC_KEY_B64 = "MCowBQYDK2VwAyEAN2JawZEm3mmUmYXAg+uPjh9XSLMmfwdg2Hrq3W8ueoU=";
8421
+ function isDemoModeAllowed() {
8422
+ if (typeof window !== "undefined") {
8423
+ return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
8424
+ }
8425
+ if (typeof process !== "undefined") {
8426
+ return process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test" || process.env.VIBECHECK_ALLOW_DEMO === "true";
8427
+ }
8428
+ return false;
8429
+ }
8430
+
8431
+ // ../license/dist/verify.js
8432
+ async function importPublicKey(keyB64) {
8433
+ const keyData = Uint8Array.from(atob(keyB64), (c) => c.charCodeAt(0));
8434
+ return crypto.subtle.importKey("spki", keyData, { name: "Ed25519" }, false, ["verify"]);
8435
+ }
8436
+ async function verifySignature(payload, signatureB64, publicKey) {
8437
+ try {
8438
+ const signature = Uint8Array.from(atob(signatureB64), (c) => c.charCodeAt(0));
8439
+ const data = new TextEncoder().encode(payload);
8440
+ return crypto.subtle.verify({ name: "Ed25519" }, publicKey, signature, data);
8441
+ } catch {
8442
+ return false;
8443
+ }
8444
+ }
8445
+ function parseLicenseKey(licenseKey) {
8446
+ try {
8447
+ const parts = licenseKey.trim().split(".");
8448
+ if (parts.length !== 2)
8449
+ return null;
8450
+ const [payloadB64, signature] = parts;
8451
+ const payloadJson = atob(payloadB64);
8452
+ const payload = JSON.parse(payloadJson);
8453
+ if (!payload.id || !payload.plan || !payload.email || !payload.issuedAt) {
8454
+ return null;
8455
+ }
8456
+ return { payload, signature };
8457
+ } catch {
8458
+ return null;
8459
+ }
8460
+ }
8461
+ async function validateLicense(licenseKey, options = {}) {
8462
+ const license = parseLicenseKey(licenseKey);
8463
+ if (!license) {
8464
+ return { valid: false, license: null, error: "Invalid license format" };
8465
+ }
8466
+ const isDemo = isDemoLicense(license.payload.id);
8467
+ if (isDemo && !isDemoModeAllowed()) {
8468
+ return {
8469
+ valid: false,
8470
+ license: null,
8471
+ error: "Demo licenses are not valid in production",
8472
+ isDemo: true
8473
+ };
8474
+ }
8475
+ if (license.payload.expiresAt) {
8476
+ const expiryDate = new Date(license.payload.expiresAt);
8477
+ if (expiryDate < /* @__PURE__ */ new Date()) {
8478
+ return {
8479
+ valid: false,
8480
+ license,
8481
+ error: "License has expired",
8482
+ isDemo
8483
+ };
8484
+ }
8485
+ }
8486
+ if (isDemo) {
8487
+ console.info("[License] Demo license detected - valid in development mode");
8488
+ return { valid: true, license, error: void 0, isDemo: true };
8489
+ }
8490
+ if (options.skipSignature) {
8491
+ return { valid: true, license, error: void 0, isDemo: false };
8492
+ }
8493
+ try {
8494
+ const publicKeyB64 = options.publicKey ?? VIBECHECK_PUBLIC_KEY_B64;
8495
+ const publicKey = await importPublicKey(publicKeyB64);
8496
+ const payloadB64 = licenseKey.split(".")[0];
8497
+ const isValid = await verifySignature(atob(payloadB64), license.signature, publicKey);
8498
+ if (!isValid) {
8499
+ return {
8500
+ valid: false,
8501
+ license: null,
8502
+ error: "Invalid license signature",
8503
+ isDemo: false
8504
+ };
8505
+ }
8506
+ } catch (err) {
8507
+ const message = err instanceof Error ? err.message : "Unknown cryptographic error";
8508
+ return {
8509
+ valid: false,
8510
+ license: null,
8511
+ error: `License verification failed: ${message}`,
8512
+ isDemo: false
8513
+ };
8514
+ }
8515
+ return { valid: true, license, error: void 0, isDemo: false };
8516
+ }
8517
+ function getDaysRemaining(license) {
8518
+ if (!license.payload.expiresAt)
8519
+ return null;
8520
+ const expiryDate = new Date(license.payload.expiresAt);
8521
+ const now = /* @__PURE__ */ new Date();
8522
+ const diff = expiryDate.getTime() - now.getTime();
8523
+ return Math.ceil(diff / (1e3 * 60 * 60 * 24));
8524
+ }
8525
+
8526
+ // ../license/dist/issue.js
8527
+ import { createPrivateKey, createPublicKey, sign, generateKeyPairSync } from "crypto";
8528
+ import { randomUUID } from "crypto";
8529
+ function generateKeyPair() {
8530
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
8531
+ publicKeyEncoding: { type: "spki", format: "der" },
8532
+ privateKeyEncoding: { type: "pkcs8", format: "der" }
8533
+ });
8534
+ return {
8535
+ publicKey: Buffer.from(publicKey).toString("base64"),
8536
+ privateKey: Buffer.from(privateKey).toString("base64")
8537
+ };
8538
+ }
8539
+ function signPayload(payloadJson, privateKeyB64) {
8540
+ const privateKeyDer = Buffer.from(privateKeyB64, "base64");
8541
+ const privateKey = createPrivateKey({
8542
+ key: privateKeyDer,
8543
+ format: "der",
8544
+ type: "pkcs8"
8545
+ });
8546
+ const signature = sign(null, Buffer.from(payloadJson), privateKey);
8547
+ return signature.toString("base64");
8548
+ }
8549
+ function createLicense(options, privateKeyB64) {
8550
+ const now = /* @__PURE__ */ new Date();
8551
+ const planFeatures = PLAN_FEATURES[options.plan] ?? [];
8552
+ const additionalFeatures = options.features ?? [];
8553
+ const allFeatures = [.../* @__PURE__ */ new Set([...planFeatures, ...additionalFeatures])];
8554
+ const payload = {
8555
+ id: options.id ?? randomUUID(),
8556
+ plan: options.plan,
8557
+ name: options.name,
8558
+ email: options.email,
8559
+ issuedAt: now.toISOString(),
8560
+ expiresAt: options.expiresAt?.toISOString() ?? null,
8561
+ features: allFeatures,
8562
+ ...options.seats && { seats: options.seats }
8563
+ };
8564
+ const payloadJson = JSON.stringify(payload);
8565
+ const payloadB64 = Buffer.from(payloadJson).toString("base64");
8566
+ const signature = signPayload(payloadJson, privateKeyB64);
8567
+ return `${payloadB64}.${signature}`;
8568
+ }
8569
+ function createDemoLicense(plan = "pro", daysValid = 30) {
8570
+ const now = /* @__PURE__ */ new Date();
8571
+ const expiresAt = new Date(now.getTime() + daysValid * 24 * 60 * 60 * 1e3);
8572
+ const payload = {
8573
+ id: `demo-${Date.now()}`,
8574
+ plan,
8575
+ name: "Demo User",
8576
+ email: "demo@vibecheck.dev",
8577
+ issuedAt: now.toISOString(),
8578
+ expiresAt: expiresAt.toISOString(),
8579
+ features: PLAN_FEATURES[plan] ?? []
8580
+ };
8581
+ const payloadJson = JSON.stringify(payload);
8582
+ const payloadB64 = Buffer.from(payloadJson).toString("base64");
8583
+ const demoSignature = Buffer.from(`demo-signature-${payload.id}`).toString("base64");
8584
+ return `${payloadB64}.${demoSignature}`;
8585
+ }
8586
+ function derivePublicKey(privateKeyB64) {
8587
+ const privateKeyDer = Buffer.from(privateKeyB64, "base64");
8588
+ const privateKey = createPrivateKey({
8589
+ key: privateKeyDer,
8590
+ format: "der",
8591
+ type: "pkcs8"
8592
+ });
8593
+ const publicKey = createPublicKey(privateKey);
8594
+ const publicKeyDer = publicKey.export({ type: "spki", format: "der" });
8595
+ return Buffer.from(publicKeyDer).toString("base64");
8596
+ }
8597
+ function inspectLicense(licenseKey) {
8598
+ try {
8599
+ const parts = licenseKey.trim().split(".");
8600
+ if (parts.length !== 2)
8601
+ return null;
8602
+ const [payloadB64, signature] = parts;
8603
+ const payloadJson = Buffer.from(payloadB64, "base64").toString("utf-8");
8604
+ const payload = JSON.parse(payloadJson);
8605
+ return { payload, signature };
8606
+ } catch {
8607
+ return null;
8608
+ }
8609
+ }
8610
+
8611
+ // src/commands/license.ts
8612
+ var CONFIG_DIR = join4(homedir2(), ".vibecheck");
8613
+ var LICENSE_FILE = join4(CONFIG_DIR, "license.key");
8614
+ function ensureConfigDir() {
8615
+ if (!existsSync4(CONFIG_DIR)) {
8616
+ mkdirSync2(CONFIG_DIR, { recursive: true });
8617
+ }
8618
+ }
8619
+ function getStoredLicenseKey() {
8620
+ try {
8621
+ if (existsSync4(LICENSE_FILE)) {
8622
+ return readFileSync4(LICENSE_FILE, "utf-8").trim();
8623
+ }
8624
+ } catch {
8625
+ }
8626
+ return null;
8627
+ }
8628
+ function storeLicenseKey(licenseKey) {
8629
+ ensureConfigDir();
8630
+ writeFileSync4(LICENSE_FILE, licenseKey, "utf-8");
8631
+ }
8632
+ function clearLicenseKey() {
8633
+ try {
8634
+ if (existsSync4(LICENSE_FILE)) {
8635
+ unlinkSync(LICENSE_FILE);
8636
+ return true;
8637
+ }
8638
+ } catch {
8639
+ }
8640
+ return false;
8641
+ }
8642
+ function promptInput(question) {
8643
+ const rl = readline.createInterface({
8644
+ input: process.stdin,
8645
+ output: process.stdout
8646
+ });
8647
+ return new Promise((resolve3) => {
8648
+ rl.question(question, (answer) => {
8649
+ rl.close();
8650
+ resolve3(answer);
8651
+ });
8652
+ });
8653
+ }
8654
+ function registerLicenseCommand(program2) {
8655
+ const license = program2.command("license").description("License management commands");
8656
+ license.command("set").description("Activate a license key (validates locally, saves to config)").argument("[license-key]", "The license key (prompts if not provided)").action(async (licenseKey) => {
8657
+ await setLicenseAction(licenseKey);
8658
+ });
8659
+ license.command("status").description("Show current license status").action(async () => {
8660
+ await statusLicenseAction();
8661
+ });
8662
+ license.command("clear").description("Remove stored license").action(async () => {
8663
+ await clearLicenseAction();
8664
+ });
8665
+ license.command("create").description("Create a new signed license").option("-e, --email <email>", "License holder email (optional)").option("-n, --name <name>", "License holder name (optional)").option("-p, --plan <plan>", "Plan type: pro", "pro").option("-k, --key <path>", "Path to private key file").option("--key-env <var>", "Environment variable containing private key", "VIBECHECK_PRIVATE_KEY").option("--expires <days>", "Days until expiry (0 for perpetual)", "90").option("--id <id>", "Custom license ID").option("--features <list>", "Comma-separated additional features").action(async (options) => {
8666
+ await createLicenseAction(options);
8667
+ });
8668
+ license.command("demo").description("Generate a demo license for development/testing").option("-p, --plan <plan>", "Plan type: pro", "pro").option("-d, --days <days>", "Days until expiry", "30").action(async (options) => {
8669
+ await createDemoAction(options);
8670
+ });
8671
+ license.command("verify").description("Verify a license key").argument("<license-key>", "The license key to verify").option("-k, --key <key>", "Public key to verify against (base64)").action(async (licenseKey, options) => {
8672
+ await verifyLicenseAction(licenseKey, options);
8673
+ });
8674
+ license.command("inspect").description("Inspect a license key (without verification)").argument("<license-key>", "The license key to inspect").action(async (licenseKey) => {
8675
+ await inspectLicenseAction(licenseKey);
8676
+ });
8677
+ license.command("keygen").description("Generate a new Ed25519 key pair for license signing").action(async () => {
8678
+ await keygenAction();
8679
+ });
8680
+ }
8681
+ async function setLicenseAction(licenseKey) {
8682
+ console.log();
8683
+ if (!licenseKey) {
8684
+ licenseKey = await promptInput(chalk.cyan("Enter your license key: "));
8685
+ }
8686
+ licenseKey = licenseKey.trim();
8687
+ if (!licenseKey) {
8688
+ console.log(chalk.red("Error: No license key provided"));
8689
+ process.exit(1);
8690
+ }
8691
+ console.log(chalk.gray("Validating license..."));
8692
+ console.log();
8693
+ try {
8694
+ const result = await validateLicense(licenseKey);
8695
+ if (result.valid && result.license) {
8696
+ storeLicenseKey(licenseKey);
8697
+ const { payload } = result.license;
8698
+ const isDemo = isDemoLicense(payload.id);
8699
+ const daysRemaining = getDaysRemaining(result.license);
8700
+ if (isDemo) {
8701
+ console.log(chalk.yellow.bold("\u26A0 Trial License Activated"));
8702
+ console.log(chalk.gray(" This license is for development/testing only."));
8703
+ } else {
8704
+ console.log(chalk.green.bold("\u2713 License Activated"));
8705
+ }
8706
+ console.log();
8707
+ console.log(chalk.gray("Details:"));
8708
+ console.log(` ${chalk.gray("Plan:")} ${chalk.cyan(PLAN_NAMES[payload.plan])}`);
8709
+ if (payload.name) {
8710
+ console.log(` ${chalk.gray("Name:")} ${payload.name}`);
8711
+ }
8712
+ console.log(` ${chalk.gray("Expires:")} ${payload.expiresAt ?? chalk.green("Never")}`);
8713
+ if (daysRemaining !== null) {
8714
+ const daysColor = daysRemaining <= 14 ? chalk.yellow : chalk.gray;
8715
+ console.log(` ${chalk.gray("Remaining:")} ${daysColor(`${daysRemaining} days`)}`);
8716
+ }
8717
+ console.log(` ${chalk.gray("Features:")} ${payload.features.join(", ") || "(none)"}`);
8718
+ console.log();
8719
+ console.log(chalk.gray(`License saved to: ${LICENSE_FILE}`));
8720
+ } else {
8721
+ console.log(chalk.red.bold("\u2717 Invalid License"));
8722
+ console.log(chalk.red(` ${result.error}`));
8723
+ process.exit(1);
8724
+ }
8725
+ } catch (err) {
8726
+ console.error(chalk.red("Error validating license:"), err instanceof Error ? err.message : err);
8727
+ process.exit(1);
8728
+ }
8729
+ console.log();
8730
+ }
8731
+ async function statusLicenseAction() {
8732
+ console.log();
8733
+ const licenseKey = getStoredLicenseKey();
8734
+ if (!licenseKey) {
8735
+ console.log(chalk.gray("No license configured."));
8736
+ console.log();
8737
+ console.log(chalk.gray("Plan:") + " " + chalk.white("Free"));
8738
+ console.log();
8739
+ console.log(chalk.gray("To activate a license:"));
8740
+ console.log(chalk.cyan(" vibecheck license set <license-key>"));
8741
+ console.log();
8742
+ console.log(chalk.gray("Get a license at:") + " " + chalk.blue("https://vibecheck.dev/pricing"));
8743
+ console.log();
8744
+ return;
8745
+ }
8746
+ try {
8747
+ const result = await validateLicense(licenseKey);
8748
+ if (result.valid && result.license) {
8749
+ const { payload } = result.license;
8750
+ const isDemo = isDemoLicense(payload.id);
8751
+ const daysRemaining = getDaysRemaining(result.license);
8752
+ if (isDemo) {
8753
+ console.log(chalk.yellow.bold("Trial License"));
8754
+ console.log(chalk.gray("(Development/testing only)"));
8755
+ } else {
8756
+ console.log(chalk.green.bold("License Active"));
8757
+ }
8758
+ console.log();
8759
+ console.log(`${chalk.gray("Plan:")} ${chalk.cyan(PLAN_NAMES[payload.plan])}`);
8760
+ console.log(`${chalk.gray("ID:")} ${payload.id}`);
8761
+ if (payload.name) {
8762
+ console.log(`${chalk.gray("Name:")} ${payload.name}`);
8763
+ }
8764
+ console.log(`${chalk.gray("Issued:")} ${payload.issuedAt}`);
8765
+ console.log(`${chalk.gray("Expires:")} ${payload.expiresAt ?? chalk.green("Never")}`);
8766
+ if (daysRemaining !== null) {
8767
+ if (daysRemaining <= 0) {
8768
+ console.log(`${chalk.gray("Status:")} ${chalk.red("Expired")}`);
8769
+ } else if (daysRemaining <= 14) {
8770
+ console.log(`${chalk.gray("Remaining:")} ${chalk.yellow(`${daysRemaining} days`)} ${chalk.gray("(expiring soon)")}`);
8771
+ } else {
8772
+ console.log(`${chalk.gray("Remaining:")} ${chalk.green(`${daysRemaining} days`)}`);
8773
+ }
8774
+ }
8775
+ console.log(`${chalk.gray("Features:")} ${payload.features.join(", ") || "(none)"}`);
8776
+ console.log();
8777
+ if (daysRemaining !== null && daysRemaining <= 14 && !isDemo) {
8778
+ console.log(chalk.yellow("License expiring soon. Generate a new key from the Pro Portal:"));
8779
+ console.log(chalk.blue(" https://vibecheck.dev/portal"));
8780
+ console.log();
8781
+ }
8782
+ } else {
8783
+ console.log(chalk.red.bold("Stored License Invalid"));
8784
+ console.log(chalk.red(` ${result.error}`));
8785
+ console.log();
8786
+ console.log(chalk.gray("Clear the invalid license with:"));
8787
+ console.log(chalk.cyan(" vibecheck license clear"));
8788
+ console.log();
8789
+ }
8790
+ } catch (err) {
8791
+ console.error(chalk.red("Error checking license:"), err instanceof Error ? err.message : err);
8792
+ process.exit(1);
8793
+ }
8794
+ }
8795
+ async function clearLicenseAction() {
8796
+ console.log();
8797
+ const cleared = clearLicenseKey();
8798
+ if (cleared) {
8799
+ console.log(chalk.green("\u2713 License removed"));
8800
+ console.log(chalk.gray(` Deleted: ${LICENSE_FILE}`));
8801
+ } else {
8802
+ console.log(chalk.gray("No license to remove."));
8803
+ }
8804
+ console.log();
8805
+ }
8806
+ async function createLicenseAction(options) {
8807
+ const plan = options.plan;
8808
+ if (plan !== "pro") {
8809
+ console.error(chalk.red("Error: Plan must be 'pro'"));
8810
+ process.exit(1);
8811
+ }
8812
+ let privateKey;
8813
+ if (options.key) {
8814
+ if (!existsSync4(options.key)) {
8815
+ console.error(chalk.red(`Error: Key file not found: ${options.key}`));
8816
+ process.exit(1);
8817
+ }
8818
+ privateKey = readFileSync4(options.key, "utf-8").trim();
8819
+ } else if (process.env[options.keyEnv]) {
8820
+ privateKey = process.env[options.keyEnv];
8821
+ }
8822
+ if (!privateKey) {
8823
+ console.error(chalk.red("Error: No private key provided"));
8824
+ console.error(chalk.gray(" Use --key <path> or set VIBECHECK_PRIVATE_KEY environment variable"));
8825
+ process.exit(1);
8826
+ }
8827
+ const expiresDays = parseInt(options.expires, 10);
8828
+ const expiresAt = expiresDays > 0 ? new Date(Date.now() + expiresDays * 24 * 60 * 60 * 1e3) : null;
8829
+ const features = options.features?.split(",").map((f) => f.trim());
8830
+ try {
8831
+ const licenseKey = createLicense(
8832
+ {
8833
+ id: options.id,
8834
+ plan,
8835
+ name: options.name,
8836
+ email: options.email,
8837
+ expiresAt,
8838
+ features
8839
+ },
8840
+ privateKey
8841
+ );
8842
+ const license = inspectLicense(licenseKey);
8843
+ console.log();
8844
+ console.log(chalk.green.bold("\u2713 License created successfully"));
8845
+ console.log();
8846
+ if (license) {
8847
+ console.log(chalk.gray("Details:"));
8848
+ console.log(` ${chalk.gray("ID:")} ${license.payload.id}`);
8849
+ console.log(` ${chalk.gray("Plan:")} ${chalk.cyan(license.payload.plan)}`);
8850
+ if (license.payload.name) {
8851
+ console.log(` ${chalk.gray("Name:")} ${license.payload.name}`);
8852
+ }
8853
+ if (license.payload.email) {
8854
+ console.log(` ${chalk.gray("Email:")} ${license.payload.email}`);
8855
+ }
8856
+ console.log(` ${chalk.gray("Issued:")} ${license.payload.issuedAt}`);
8857
+ console.log(` ${chalk.gray("Expires:")} ${license.payload.expiresAt ?? chalk.green("Never (perpetual)")}`);
8858
+ console.log(` ${chalk.gray("Features:")} ${license.payload.features.join(", ")}`);
8859
+ }
8860
+ console.log();
8861
+ console.log(chalk.gray("License Key:"));
8862
+ console.log(chalk.yellow(licenseKey));
8863
+ console.log();
8864
+ } catch (err) {
8865
+ console.error(chalk.red("Error creating license:"), err instanceof Error ? err.message : err);
8866
+ process.exit(1);
8867
+ }
8868
+ }
8869
+ async function createDemoAction(options) {
8870
+ const plan = options.plan;
8871
+ if (plan !== "pro") {
8872
+ console.error(chalk.red("Error: Plan must be 'pro'"));
8873
+ process.exit(1);
8874
+ }
8875
+ const days = parseInt(options.days, 10);
8876
+ const licenseKey = createDemoLicense(plan, days);
8877
+ const license = inspectLicense(licenseKey);
8878
+ console.log();
8879
+ console.log(chalk.yellow.bold("\u26A0 Demo License Generated"));
8880
+ console.log(chalk.gray(" This license is for development/testing only."));
8881
+ console.log(chalk.gray(" It will not work in production environments."));
8882
+ console.log();
8883
+ if (license) {
8884
+ console.log(chalk.gray("Details:"));
8885
+ console.log(` ${chalk.gray("ID:")} ${license.payload.id}`);
8886
+ console.log(` ${chalk.gray("Plan:")} ${chalk.cyan(license.payload.plan)}`);
8887
+ console.log(` ${chalk.gray("Expires:")} ${license.payload.expiresAt}`);
8888
+ }
8889
+ console.log();
8890
+ console.log(chalk.gray("License Key:"));
8891
+ console.log(chalk.yellow(licenseKey));
8892
+ console.log();
8893
+ }
8894
+ async function verifyLicenseAction(licenseKey, options) {
8895
+ console.log();
8896
+ console.log(chalk.gray("Verifying license..."));
8897
+ try {
8898
+ const result = await validateLicense(licenseKey, {
8899
+ publicKey: options.key
8900
+ });
8901
+ if (result.valid) {
8902
+ console.log();
8903
+ console.log(chalk.green.bold("\u2713 License is valid"));
8904
+ if (result.isDemo) {
8905
+ console.log(chalk.yellow(" (Demo license - valid in development only)"));
8906
+ }
8907
+ if (result.license) {
8908
+ const { payload } = result.license;
8909
+ console.log();
8910
+ console.log(chalk.gray("Details:"));
8911
+ console.log(` ${chalk.gray("ID:")} ${payload.id}`);
8912
+ console.log(` ${chalk.gray("Plan:")} ${chalk.cyan(payload.plan)}`);
8913
+ if (payload.name) {
8914
+ console.log(` ${chalk.gray("Name:")} ${payload.name}`);
8915
+ }
8916
+ if (payload.email) {
8917
+ console.log(` ${chalk.gray("Email:")} ${payload.email}`);
8918
+ }
8919
+ console.log(` ${chalk.gray("Issued:")} ${payload.issuedAt}`);
8920
+ console.log(` ${chalk.gray("Expires:")} ${payload.expiresAt ?? chalk.green("Never")}`);
8921
+ console.log(` ${chalk.gray("Features:")} ${payload.features.join(", ") || "(none)"}`);
8922
+ }
8923
+ } else {
8924
+ console.log();
8925
+ console.log(chalk.red.bold("\u2717 License is invalid"));
8926
+ console.log(chalk.red(` ${result.error}`));
8927
+ process.exit(1);
8928
+ }
8929
+ } catch (err) {
8930
+ console.error(chalk.red("Error verifying license:"), err instanceof Error ? err.message : err);
8931
+ process.exit(1);
8932
+ }
8933
+ console.log();
8934
+ }
8935
+ async function inspectLicenseAction(licenseKey) {
8936
+ const license = inspectLicense(licenseKey);
8937
+ if (!license) {
8938
+ console.log();
8939
+ console.log(chalk.red.bold("\u2717 Invalid license format"));
8940
+ console.log(chalk.gray(" Could not parse the license key."));
8941
+ process.exit(1);
8942
+ }
8943
+ console.log();
8944
+ console.log(chalk.blue.bold("License Contents"));
8945
+ console.log(chalk.gray("(Not verified - use 'vibecheck license verify' for verification)"));
8946
+ console.log();
8947
+ const { payload } = license;
8948
+ console.log(`${chalk.gray("ID:")} ${payload.id}`);
8949
+ console.log(`${chalk.gray("Plan:")} ${chalk.cyan(payload.plan)}`);
8950
+ if (payload.name) {
8951
+ console.log(`${chalk.gray("Name:")} ${payload.name}`);
8952
+ }
8953
+ if (payload.email) {
8954
+ console.log(`${chalk.gray("Email:")} ${payload.email}`);
8955
+ }
8956
+ console.log(`${chalk.gray("Issued:")} ${payload.issuedAt}`);
8957
+ console.log(`${chalk.gray("Expires:")} ${payload.expiresAt ?? chalk.green("Never (perpetual)")}`);
8958
+ console.log(`${chalk.gray("Features:")} ${payload.features.join(", ") || "(none)"}`);
8959
+ console.log();
8960
+ console.log(`${chalk.gray("Signature:")} ${license.signature.slice(0, 20)}...`);
8961
+ console.log();
8962
+ }
8963
+ async function keygenAction() {
8964
+ console.log();
8965
+ console.log(chalk.blue.bold("Generating Ed25519 Key Pair"));
8966
+ console.log();
8967
+ const { publicKey, privateKey } = generateKeyPair();
8968
+ const derivedPublic = derivePublicKey(privateKey);
8969
+ if (derivedPublic !== publicKey) {
8970
+ console.error(chalk.red("Error: Key verification failed"));
8971
+ process.exit(1);
8972
+ }
8973
+ console.log(chalk.green("\u2713 Keys generated and verified"));
8974
+ console.log();
8975
+ console.log(chalk.gray("PUBLIC KEY") + chalk.gray(" (safe to embed in code):"));
8976
+ console.log(chalk.cyan(publicKey));
8977
+ console.log();
8978
+ console.log(chalk.gray("PRIVATE KEY") + chalk.red.bold(" (\u26A0\uFE0F KEEP SECRET):"));
8979
+ console.log(chalk.yellow(privateKey));
8980
+ console.log();
8981
+ console.log(chalk.gray("\u2500".repeat(60)));
8982
+ console.log(chalk.gray("Next steps:"));
8983
+ console.log(chalk.gray("1. Save the private key securely (password manager, HSM)"));
8984
+ console.log(chalk.gray("2. Update VIBECHECK_PUBLIC_KEY_B64 in packages/license/src/constants.ts"));
8985
+ console.log(chalk.gray("3. NEVER commit the private key to version control"));
8986
+ console.log();
8987
+ }
8988
+
7902
8989
  // src/index.ts
7903
8990
  var program = new Command();
7904
8991
  program.name("vibecheck").description("Security scanner for modern web applications").version("0.0.1");
@@ -7908,4 +8995,6 @@ registerDemoArtifactCommand(program);
7908
8995
  registerIntentCommand(program);
7909
8996
  registerEvaluateCommand(program);
7910
8997
  registerWaiversCommand(program);
8998
+ registerViewCommand(program);
8999
+ registerLicenseCommand(program);
7911
9000
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quantracode/vibecheck",
3
- "version": "0.0.2",
3
+ "version": "0.2.1",
4
4
  "description": "Security scanner for modern web applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,15 +55,19 @@
55
55
  "prepublishOnly": "pnpm run build && pnpm run pack:check"
56
56
  },
57
57
  "dependencies": {
58
+ "chalk": "^5.4.1",
58
59
  "commander": "^12.1.0",
59
60
  "fast-glob": "^3.3.2",
60
61
  "micromatch": "^4.0.8",
62
+ "tar": "^7.5.2",
61
63
  "ts-morph": "^24.0.0",
62
64
  "zod": "^3.24.1"
63
65
  },
64
66
  "devDependencies": {
67
+ "@vibecheck/license": "workspace:*",
65
68
  "@types/micromatch": "^4.0.9",
66
69
  "@types/node": "^22.10.2",
70
+ "@types/tar": "^6.1.13",
67
71
  "@vibecheck/policy": "workspace:*",
68
72
  "@vibecheck/schema": "workspace:*",
69
73
  "tsup": "^8.5.1",