@etalon/cli 1.0.5 → 1.1.0

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 (2) hide show
  1. package/dist/index.js +437 -124
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,13 +5,13 @@ import {
5
5
 
6
6
  // src/index.ts
7
7
  import { Command } from "commander";
8
- import ora from "ora";
9
- import chalk4 from "chalk";
10
- import { writeFileSync as writeFileSync2 } from "fs";
8
+ import ora2 from "ora";
9
+ import chalk6 from "chalk";
10
+ import { writeFileSync as writeFileSync3 } from "fs";
11
11
  import {
12
12
  normalizeUrl,
13
13
  VendorRegistry,
14
- auditProject,
14
+ auditProject as auditProject2,
15
15
  formatAuditSarif,
16
16
  generateBadgeSvg,
17
17
  calculateScore,
@@ -22,7 +22,7 @@ import {
22
22
  toTextSummary,
23
23
  generatePolicy,
24
24
  AutoFixEngine,
25
- applyContextScoring,
25
+ applyContextScoring as applyContextScoring2,
26
26
  reportFalsePositive,
27
27
  getFeedbackSummary,
28
28
  isTelemetryEnabled,
@@ -767,12 +767,281 @@ async function runInit(dir, options = {}) {
767
767
  console.log("");
768
768
  }
769
769
 
770
+ // src/commands/cloud.ts
771
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync } from "fs";
772
+ import { homedir } from "os";
773
+ import { join as join3 } from "path";
774
+ import chalk4 from "chalk";
775
+ var CONFIG_DIR = join3(homedir(), ".etalon");
776
+ var CONFIG_FILE = join3(CONFIG_DIR, "config.json");
777
+ var DEFAULT_API_URL = "https://etalon.nma.vc/api";
778
+ function saveCloudConfig(config) {
779
+ if (!existsSync3(CONFIG_DIR)) {
780
+ mkdirSync2(CONFIG_DIR, { recursive: true });
781
+ }
782
+ writeFileSync2(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
783
+ }
784
+ function loadCloudConfig() {
785
+ if (!existsSync3(CONFIG_FILE)) {
786
+ return null;
787
+ }
788
+ try {
789
+ return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
790
+ } catch {
791
+ return null;
792
+ }
793
+ }
794
+ function removeCloudConfig() {
795
+ if (existsSync3(CONFIG_FILE)) {
796
+ unlinkSync(CONFIG_FILE);
797
+ }
798
+ }
799
+ async function verifyApiKey(apiKey, apiUrl = DEFAULT_API_URL) {
800
+ try {
801
+ const response = await fetch(`${apiUrl}/auth/verify`, {
802
+ method: "POST",
803
+ headers: {
804
+ "Content-Type": "application/json",
805
+ Authorization: `Bearer ${apiKey}`
806
+ }
807
+ });
808
+ return response.ok;
809
+ } catch {
810
+ return false;
811
+ }
812
+ }
813
+ async function uploadScan(config, siteId, url, results, cliVersion) {
814
+ try {
815
+ const response = await fetch(`${config.apiUrl}/ingest`, {
816
+ method: "POST",
817
+ headers: {
818
+ "Content-Type": "application/json",
819
+ Authorization: `Bearer ${config.apiKey}`
820
+ },
821
+ body: JSON.stringify({
822
+ siteId,
823
+ url,
824
+ results,
825
+ cliVersion
826
+ })
827
+ });
828
+ const data = await response.json();
829
+ if (!response.ok) {
830
+ return { success: false, error: data.error || response.statusText };
831
+ }
832
+ return {
833
+ success: true,
834
+ scanId: data.scanId,
835
+ score: data.score,
836
+ grade: data.grade,
837
+ dashboardUrl: data.dashboardUrl
838
+ };
839
+ } catch (err) {
840
+ return { success: false, error: err instanceof Error ? err.message : "Network error" };
841
+ }
842
+ }
843
+ async function runLogin() {
844
+ console.log("");
845
+ console.log(chalk4.bold("\u{1F510} Login to ETALON Cloud"));
846
+ console.log(chalk4.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
847
+ console.log("");
848
+ console.log(chalk4.gray("Get your API key from: https://etalon.nma.vc/dashboard/api-keys"));
849
+ console.log("");
850
+ const apiKey = await readLine("Enter your API key: ");
851
+ if (!apiKey || apiKey.length < 10) {
852
+ console.log(chalk4.red("\u274C Invalid API key"));
853
+ process.exit(1);
854
+ }
855
+ const spinner = (await import("ora")).default;
856
+ const s = spinner("Verifying API key...").start();
857
+ const isValid = await verifyApiKey(apiKey);
858
+ if (!isValid) {
859
+ s.fail("API key verification failed");
860
+ console.log(chalk4.gray("\nMake sure you copied the full key from the dashboard."));
861
+ process.exit(1);
862
+ }
863
+ saveCloudConfig({ apiKey, apiUrl: DEFAULT_API_URL });
864
+ s.succeed("Successfully logged in!");
865
+ console.log("");
866
+ console.log(chalk4.gray("You can now use:"));
867
+ console.log(` ${chalk4.cyan("etalon scan <url> --upload --site <id>")} Upload scan results`);
868
+ console.log(` ${chalk4.cyan("etalon auth status")} Check login status`);
869
+ console.log(` ${chalk4.cyan("etalon auth logout")} Remove stored key`);
870
+ console.log("");
871
+ }
872
+ function runLogout() {
873
+ removeCloudConfig();
874
+ console.log(chalk4.green("\u2713 Logged out successfully"));
875
+ }
876
+ async function runStatus() {
877
+ const config = loadCloudConfig();
878
+ if (!config) {
879
+ console.log(chalk4.yellow("\u274C Not logged in"));
880
+ console.log(chalk4.gray("\nRun: etalon auth login"));
881
+ return;
882
+ }
883
+ const spinner = (await import("ora")).default;
884
+ const s = spinner("Checking API key...").start();
885
+ const isValid = await verifyApiKey(config.apiKey, config.apiUrl);
886
+ if (isValid) {
887
+ s.succeed("Logged in");
888
+ console.log(chalk4.gray(` API: ${config.apiUrl}`));
889
+ console.log(chalk4.gray(` Key: ${config.apiKey.slice(0, 12)}...`));
890
+ } else {
891
+ s.fail("API key expired or invalid");
892
+ console.log(chalk4.gray("\nRun: etalon auth login"));
893
+ }
894
+ }
895
+ function readLine(prompt) {
896
+ return new Promise((resolve) => {
897
+ process.stdout.write(prompt);
898
+ let data = "";
899
+ process.stdin.setEncoding("utf-8");
900
+ process.stdin.resume();
901
+ process.stdin.on("data", (chunk) => {
902
+ data += chunk;
903
+ if (data.includes("\n")) {
904
+ process.stdin.pause();
905
+ resolve(data.trim());
906
+ }
907
+ });
908
+ });
909
+ }
910
+ async function listSites(config) {
911
+ try {
912
+ const response = await fetch(`${config.apiUrl}/sites`, {
913
+ headers: {
914
+ Authorization: `Bearer ${config.apiKey}`
915
+ }
916
+ });
917
+ const data = await response.json();
918
+ if (!response.ok) {
919
+ return { success: false, error: data.error || response.statusText };
920
+ }
921
+ return { success: true, sites: data.sites };
922
+ } catch (err) {
923
+ return { success: false, error: err instanceof Error ? err.message : "Network error" };
924
+ }
925
+ }
926
+ async function runListSites() {
927
+ const config = loadCloudConfig();
928
+ if (!config) {
929
+ console.log(chalk4.yellow("\u274C Not logged in"));
930
+ console.log(chalk4.gray("\nRun: etalon auth login"));
931
+ return;
932
+ }
933
+ const spinner = (await import("ora")).default;
934
+ const s = spinner("Fetching sites...").start();
935
+ const result = await listSites(config);
936
+ if (!result.success || !result.sites) {
937
+ s.fail(`Failed: ${result.error}`);
938
+ return;
939
+ }
940
+ s.stop();
941
+ if (result.sites.length === 0) {
942
+ console.log(chalk4.yellow("No sites found."));
943
+ console.log(chalk4.gray("Add a site at: https://etalon.nma.vc/dashboard/sites"));
944
+ return;
945
+ }
946
+ console.log("");
947
+ console.log(chalk4.bold("\u{1F4CB} Your Sites"));
948
+ console.log(chalk4.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
949
+ console.log("");
950
+ for (const site of result.sites) {
951
+ const name = site.name || new URL(site.url).hostname;
952
+ const lastScan = site.last_scanned_at ? chalk4.gray(`Last scan: ${new Date(site.last_scanned_at).toLocaleDateString()}`) : chalk4.gray("Never scanned");
953
+ console.log(` ${chalk4.cyan(site.id)} ${chalk4.bold(name)}`);
954
+ console.log(` ${chalk4.dim(site.url)} ${lastScan}`);
955
+ console.log("");
956
+ }
957
+ console.log(chalk4.dim("Use the ID with: etalon scan <url> --upload --site <id>"));
958
+ console.log("");
959
+ }
960
+
961
+ // src/commands/push.ts
962
+ import chalk5 from "chalk";
963
+ import ora from "ora";
964
+ import { auditProject, applyContextScoring } from "@etalon/core";
965
+ var VERSION = "1.1.0";
966
+ async function runPush(url, dir, options) {
967
+ const siteId = options.site;
968
+ if (!siteId) {
969
+ console.log("");
970
+ console.log(chalk5.red("\u274C --site <id> is required"));
971
+ console.log(chalk5.gray(" Get your site ID from: https://etalon.nma.vc/dashboard/sites"));
972
+ console.log(chalk5.gray(" Or run: etalon sites"));
973
+ process.exit(1);
974
+ }
975
+ const config = loadCloudConfig();
976
+ if (!config) {
977
+ console.log("");
978
+ console.log(chalk5.red("\u274C Not logged in. Run: etalon auth login"));
979
+ process.exit(1);
980
+ }
981
+ console.log("");
982
+ console.log(chalk5.bold("\u{1F680} ETALON Push"));
983
+ console.log(chalk5.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
984
+ console.log("");
985
+ const scanSpinner = ora(`Scanning ${url}...`).start();
986
+ let scanReport;
987
+ try {
988
+ const normalizedUrl2 = url.startsWith("http") ? url : `https://${url}`;
989
+ const scanOptions = {
990
+ deep: options.deep ?? false,
991
+ timeout: parseInt(options.timeout ?? "30000", 10),
992
+ waitForNetworkIdle: false
993
+ };
994
+ scanReport = await scanSite(normalizedUrl2, scanOptions);
995
+ scanSpinner.succeed(`Scan complete \u2014 ${scanReport.summary.total} vendor(s) detected`);
996
+ } catch (err) {
997
+ scanSpinner.fail("Scan failed");
998
+ console.error(chalk5.red(` ${err instanceof Error ? err.message : String(err)}`));
999
+ process.exit(2);
1000
+ }
1001
+ let auditReport;
1002
+ const auditSpinner = ora(`Auditing ${dir}...`).start();
1003
+ try {
1004
+ auditReport = await auditProject(dir, {});
1005
+ const scoring = applyContextScoring(auditReport.findings, dir);
1006
+ auditReport.findings = scoring.adjustedFindings;
1007
+ auditSpinner.succeed(`Audit complete \u2014 ${auditReport.findings.length} finding(s)`);
1008
+ } catch {
1009
+ auditSpinner.warn("Audit skipped (no project found or not a codebase)");
1010
+ auditReport = null;
1011
+ }
1012
+ const uploadSpinner = ora("Uploading to ETALON Cloud...").start();
1013
+ const normalizedUrl = url.startsWith("http") ? url : `https://${url}`;
1014
+ const result = await uploadScan(config, siteId, normalizedUrl, scanReport, VERSION);
1015
+ if (result.success) {
1016
+ uploadSpinner.succeed(`Uploaded! Grade: ${chalk5.bold(result.grade)} (${result.score}/100)`);
1017
+ } else {
1018
+ uploadSpinner.fail(`Upload failed: ${result.error}`);
1019
+ }
1020
+ console.log("");
1021
+ console.log(chalk5.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1022
+ if (result.success) {
1023
+ console.log(chalk5.green("\u2713 Push complete"));
1024
+ console.log(chalk5.gray(` Dashboard: ${result.dashboardUrl}`));
1025
+ }
1026
+ if (scanReport.summary.highRisk > 0) {
1027
+ console.log(chalk5.yellow(` \u26A0 ${scanReport.summary.highRisk} high-risk tracker(s) detected`));
1028
+ }
1029
+ if (auditReport && auditReport.findings.length > 0) {
1030
+ const critical = auditReport.findings.filter((f) => f.severity === "critical").length;
1031
+ const high = auditReport.findings.filter((f) => f.severity === "high").length;
1032
+ if (critical + high > 0) {
1033
+ console.log(chalk5.yellow(` \u26A0 ${critical} critical, ${high} high severity code findings`));
1034
+ }
1035
+ }
1036
+ console.log("");
1037
+ }
1038
+
770
1039
  // src/index.ts
771
- var VERSION = "1.0.0";
1040
+ var VERSION2 = "1.1.0";
772
1041
  function showBanner() {
773
- const blue = chalk4.hex("#3B82F6");
774
- const dim = chalk4.hex("#64748B");
775
- const cyan = chalk4.hex("#06B6D4");
1042
+ const blue = chalk6.hex("#3B82F6");
1043
+ const dim = chalk6.hex("#64748B");
1044
+ const cyan = chalk6.hex("#06B6D4");
776
1045
  console.log("");
777
1046
  console.log(blue.bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557"));
778
1047
  console.log(blue.bold(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551"));
@@ -781,15 +1050,15 @@ function showBanner() {
781
1050
  console.log(blue.bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551"));
782
1051
  console.log(blue.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"));
783
1052
  console.log("");
784
- console.log(dim(` v${VERSION} `) + chalk4.white("Privacy audit tool for AI coding agents"));
1053
+ console.log(dim(` v${VERSION2} `) + chalk6.white("Privacy audit tool for AI coding agents"));
785
1054
  console.log(dim(" Open-source GDPR compliance scanner ") + cyan("etalon.nma.vc"));
786
1055
  console.log("");
787
1056
  }
788
1057
  var program = new Command();
789
- program.name("etalon").description("ETALON \u2014 Open-source privacy auditor. Scan websites for trackers and GDPR compliance.").version(VERSION).hook("preAction", () => {
1058
+ program.name("etalon").description("ETALON \u2014 Open-source privacy auditor. Scan websites for trackers and GDPR compliance.").version(VERSION2).hook("preAction", () => {
790
1059
  showBanner();
791
1060
  });
792
- program.command("scan").description("Scan a website for third-party trackers").argument("<url>", "URL to scan").option("-f, --format <format>", "Output format: text, json, sarif", "text").option("-d, --deep", "Deep scan: scroll page, interact with consent dialogs", false).option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--idle", "Wait for network idle (slower but more thorough)").option("--config <path>", "Path to etalon.yaml config file").action(async (url, options) => {
1061
+ program.command("scan").description("Scan a website for third-party trackers").argument("<url>", "URL to scan").option("-f, --format <format>", "Output format: text, json, sarif", "text").option("-d, --deep", "Deep scan: scroll page, interact with consent dialogs", false).option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--idle", "Wait for network idle (slower but more thorough)").option("--config <path>", "Path to etalon.yaml config file").option("--upload", "Upload results to ETALON Cloud").option("--site <id>", "Site ID for cloud upload (from dashboard)").action(async (url, options) => {
793
1062
  const normalizedUrl = normalizeUrl(url);
794
1063
  const format = options.format ?? "text";
795
1064
  const config = loadConfig(options.config);
@@ -807,7 +1076,7 @@ program.command("scan").description("Scan a website for third-party trackers").a
807
1076
  }
808
1077
  }
809
1078
  const showSpinner = format === "text";
810
- const spinner = showSpinner ? ora(`Scanning ${normalizedUrl}...`).start() : null;
1079
+ const spinner = showSpinner ? ora2(`Scanning ${normalizedUrl}...`).start() : null;
811
1080
  try {
812
1081
  const report = await scanSite(normalizedUrl, scanOptions);
813
1082
  spinner?.stop();
@@ -823,6 +1092,30 @@ program.command("scan").description("Scan a website for third-party trackers").a
823
1092
  console.log(formatText(report));
824
1093
  break;
825
1094
  }
1095
+ if (options.upload) {
1096
+ const siteId = options.site;
1097
+ if (!siteId) {
1098
+ console.log("");
1099
+ console.log(chalk6.red("\u274C --site <id> is required when using --upload"));
1100
+ console.log(chalk6.gray(" Get your site ID from: https://etalon.nma.vc/dashboard/sites"));
1101
+ process.exit(1);
1102
+ }
1103
+ const cloudConfig = loadCloudConfig();
1104
+ if (!cloudConfig) {
1105
+ console.log("");
1106
+ console.log(chalk6.red("\u274C Not logged in. Run: etalon auth login"));
1107
+ process.exit(1);
1108
+ }
1109
+ const uploadSpinner = ora2("Uploading to ETALON Cloud...").start();
1110
+ const result = await uploadScan(cloudConfig, siteId, normalizedUrl, report, VERSION2);
1111
+ if (result.success) {
1112
+ uploadSpinner.succeed(`Uploaded! Grade: ${chalk6.bold(result.grade)} (${result.score}/100)`);
1113
+ console.log(chalk6.gray(` View: ${result.dashboardUrl}`));
1114
+ console.log("");
1115
+ } else {
1116
+ uploadSpinner.fail(`Upload failed: ${result.error}`);
1117
+ }
1118
+ }
826
1119
  if (report.summary.highRisk > 0) {
827
1120
  process.exit(1);
828
1121
  }
@@ -846,39 +1139,39 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
846
1139
  const includeBlame = options.includeBlame;
847
1140
  const autoFix = options.fix;
848
1141
  const showSpinner = format === "text";
849
- const spinner = showSpinner ? ora(`Auditing ${dir}...`).start() : null;
1142
+ const spinner = showSpinner ? ora2(`Auditing ${dir}...`).start() : null;
850
1143
  try {
851
- const report = await auditProject(dir, {
1144
+ const report = await auditProject2(dir, {
852
1145
  severity: options.severity,
853
1146
  includeBlame
854
1147
  });
855
- const scoring = applyContextScoring(report.findings, dir);
1148
+ const scoring = applyContextScoring2(report.findings, dir);
856
1149
  report.findings = scoring.adjustedFindings;
857
1150
  if (scoring.adjustments.length > 0 && format === "text") {
858
1151
  spinner?.stop();
859
- console.log(chalk4.bold(`
1152
+ console.log(chalk6.bold(`
860
1153
  \u{1F3AF} Context: ${scoring.projectContext.industry} / ${scoring.projectContext.region}`));
861
- console.log(chalk4.dim(` ${scoring.adjustments.length} finding(s) had severity adjusted based on context`));
1154
+ console.log(chalk6.dim(` ${scoring.adjustments.length} finding(s) had severity adjusted based on context`));
862
1155
  for (const adj of scoring.adjustments.slice(0, 5)) {
863
- console.log(` ${chalk4.dim(adj.finding_rule)}: ${adj.original_severity} \u2192 ${chalk4.yellow(adj.adjusted_severity)} (${adj.reason})`);
1156
+ console.log(` ${chalk6.dim(adj.finding_rule)}: ${adj.original_severity} \u2192 ${chalk6.yellow(adj.adjusted_severity)} (${adj.reason})`);
864
1157
  }
865
1158
  if (scoring.adjustments.length > 5) {
866
- console.log(chalk4.dim(` ... and ${scoring.adjustments.length - 5} more`));
1159
+ console.log(chalk6.dim(` ... and ${scoring.adjustments.length - 5} more`));
867
1160
  }
868
1161
  }
869
1162
  spinner?.stop();
870
1163
  if (autoFix) {
871
1164
  const patches = generatePatches(report.findings, dir);
872
1165
  if (patches.length > 0) {
873
- console.log(chalk4.bold(`
1166
+ console.log(chalk6.bold(`
874
1167
  \u{1F527} ${patches.length} config fix(es):`));
875
1168
  for (const p of patches) {
876
- console.log(` ${chalk4.dim(p.file)}:${p.line} \u2014 ${p.description}`);
877
- console.log(` ${chalk4.red("- " + p.oldContent.trim())}`);
878
- console.log(` ${chalk4.green("+ " + p.newContent.trim())}`);
1169
+ console.log(` ${chalk6.dim(p.file)}:${p.line} \u2014 ${p.description}`);
1170
+ console.log(` ${chalk6.red("- " + p.oldContent.trim())}`);
1171
+ console.log(` ${chalk6.green("+ " + p.newContent.trim())}`);
879
1172
  }
880
1173
  const applied = applyPatches(patches, dir);
881
- console.log(chalk4.green(`
1174
+ console.log(chalk6.green(`
882
1175
  \u2713 Applied ${applied} config fix(es).`));
883
1176
  }
884
1177
  const engine = new AutoFixEngine();
@@ -903,23 +1196,23 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
903
1196
  collectCodeFiles(dir);
904
1197
  const suggestions = engine.scanFiles(codeFiles);
905
1198
  if (suggestions.length > 0) {
906
- console.log(chalk4.bold(`
1199
+ console.log(chalk6.bold(`
907
1200
  \u{1F6E1}\uFE0F ${suggestions.length} tracker consent fix(es) available:`));
908
1201
  for (const s of suggestions) {
909
- console.log(` ${chalk4.cyan(s.tracker_name)} in ${chalk4.dim(s.location.file)}:${s.location.line}`);
910
- console.log(` ${chalk4.dim(s.description)}`);
1202
+ console.log(` ${chalk6.cyan(s.tracker_name)} in ${chalk6.dim(s.location.file)}:${s.location.line}`);
1203
+ console.log(` ${chalk6.dim(s.description)}`);
911
1204
  }
912
1205
  const hookPath = engine.generateConsentHook(dir);
913
- console.log(chalk4.green(`
914
- \u2713 Generated consent hook: ${chalk4.cyan(hookPath)}`));
1206
+ console.log(chalk6.green(`
1207
+ \u2713 Generated consent hook: ${chalk6.cyan(hookPath)}`));
915
1208
  const result = engine.applyAllFixes(suggestions);
916
- console.log(chalk4.green(`\u2713 Applied ${result.applied} tracker consent fix(es).`));
1209
+ console.log(chalk6.green(`\u2713 Applied ${result.applied} tracker consent fix(es).`));
917
1210
  if (result.failed > 0) {
918
- console.log(chalk4.yellow(`\u26A0 ${result.failed} fix(es) could not be applied.`));
1211
+ console.log(chalk6.yellow(`\u26A0 ${result.failed} fix(es) could not be applied.`));
919
1212
  }
920
1213
  }
921
1214
  if (patches.length === 0 && suggestions.length === 0) {
922
- console.log(chalk4.yellow("\nNo auto-fixable issues found."));
1215
+ console.log(chalk6.yellow("\nNo auto-fixable issues found."));
923
1216
  }
924
1217
  }
925
1218
  recordAuditEvent({
@@ -941,8 +1234,8 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
941
1234
  case "html": {
942
1235
  const html = generateHtmlReport(report);
943
1236
  const outPath = "etalon-report.html";
944
- writeFileSync2(outPath, html, "utf-8");
945
- console.log(chalk4.green(`\u2713 Report written to ${chalk4.cyan(outPath)}`));
1237
+ writeFileSync3(outPath, html, "utf-8");
1238
+ console.log(chalk6.green(`\u2713 Report written to ${chalk6.cyan(outPath)}`));
946
1239
  break;
947
1240
  }
948
1241
  case "text":
@@ -993,7 +1286,7 @@ program.command("init").description("Set up ETALON in your project (config, CI,
993
1286
  program.command("consent-check").description("Test if trackers fire before/after rejecting cookies on a website").argument("<url>", "URL to check").option("-f, --format <format>", "Output format: text, json", "text").option("-t, --timeout <ms>", "Navigation timeout", "15000").action(async (url, options) => {
994
1287
  const normalizedUrl = normalizeUrl(url);
995
1288
  const format = options.format ?? "text";
996
- const spinner = format === "text" ? ora(`Checking consent on ${normalizedUrl}...`).start() : null;
1289
+ const spinner = format === "text" ? ora2(`Checking consent on ${normalizedUrl}...`).start() : null;
997
1290
  try {
998
1291
  const { checkConsent } = await import("./consent-checker-QRPTMQWN.js");
999
1292
  const result = await checkConsent(normalizedUrl, {
@@ -1004,33 +1297,33 @@ program.command("consent-check").description("Test if trackers fire before/after
1004
1297
  console.log(JSON.stringify(result, null, 2));
1005
1298
  } else {
1006
1299
  console.log("");
1007
- console.log(chalk4.bold("ETALON Consent Verification"));
1008
- console.log(chalk4.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1009
- console.log(`URL: ${chalk4.cyan(result.url)}`);
1010
- console.log(`Banner: ${result.bannerDetected ? chalk4.green("\u2713 Detected") : chalk4.red("\u2717 Not found")}${result.bannerType ? ` (${result.bannerType})` : ""}`);
1011
- console.log(`Reject: ${result.rejectClicked ? chalk4.green("\u2713 Clicked") : chalk4.yellow("\u2717 Could not reject")}`);
1300
+ console.log(chalk6.bold("ETALON Consent Verification"));
1301
+ console.log(chalk6.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1302
+ console.log(`URL: ${chalk6.cyan(result.url)}`);
1303
+ console.log(`Banner: ${result.bannerDetected ? chalk6.green("\u2713 Detected") : chalk6.red("\u2717 Not found")}${result.bannerType ? ` (${result.bannerType})` : ""}`);
1304
+ console.log(`Reject: ${result.rejectClicked ? chalk6.green("\u2713 Clicked") : chalk6.yellow("\u2717 Could not reject")}`);
1012
1305
  console.log("");
1013
1306
  if (result.preConsentTrackers.length > 0) {
1014
- console.log(chalk4.bold("\u{1F50D} Trackers before consent:"));
1307
+ console.log(chalk6.bold("\u{1F50D} Trackers before consent:"));
1015
1308
  for (const t of result.preConsentTrackers) {
1016
- console.log(` \u2022 ${t.name} (${chalk4.dim(t.matchedDomain)})`);
1309
+ console.log(` \u2022 ${t.name} (${chalk6.dim(t.matchedDomain)})`);
1017
1310
  }
1018
1311
  console.log("");
1019
1312
  }
1020
1313
  if (result.postRejectTrackers.length > 0) {
1021
- console.log(chalk4.bold("\u26A0\uFE0F Trackers after rejection:"));
1314
+ console.log(chalk6.bold("\u26A0\uFE0F Trackers after rejection:"));
1022
1315
  for (const t of result.postRejectTrackers) {
1023
- console.log(` \u2022 ${chalk4.red(t.name)} (${chalk4.dim(t.matchedDomain)})`);
1316
+ console.log(` \u2022 ${chalk6.red(t.name)} (${chalk6.dim(t.matchedDomain)})`);
1024
1317
  }
1025
1318
  console.log("");
1026
1319
  }
1027
1320
  if (result.violations.length > 0) {
1028
- console.log(chalk4.red.bold(`\u{1F534} ${result.violations.length} consent violation(s)`));
1321
+ console.log(chalk6.red.bold(`\u{1F534} ${result.violations.length} consent violation(s)`));
1029
1322
  for (const v of result.violations) {
1030
1323
  console.log(` ${v.phase === "before-interaction" ? "\u23F1" : "\u{1F534}"} ${v.message}`);
1031
1324
  }
1032
1325
  } else {
1033
- console.log(chalk4.green.bold("\u2713 No consent violations detected"));
1326
+ console.log(chalk6.green.bold("\u2713 No consent violations detected"));
1034
1327
  }
1035
1328
  console.log("");
1036
1329
  }
@@ -1047,7 +1340,7 @@ Error: ${error.message}`);
1047
1340
  program.command("policy-check").description("Cross-reference privacy policy text against actual detected trackers").argument("<url>", "URL to check").option("-f, --format <format>", "Output format: text, json", "text").option("-t, --timeout <ms>", "Navigation timeout", "30000").option("--policy-url <url>", "Directly specify the privacy policy URL").action(async (url, options) => {
1048
1341
  const normalizedUrl = normalizeUrl(url);
1049
1342
  const format = options.format ?? "text";
1050
- const spinner = format === "text" ? ora(`Analyzing privacy policy for ${normalizedUrl}...`).start() : null;
1343
+ const spinner = format === "text" ? ora2(`Analyzing privacy policy for ${normalizedUrl}...`).start() : null;
1051
1344
  try {
1052
1345
  const { checkPolicy } = await import("./policy-checker-ONMTI7X2.js");
1053
1346
  const result = await checkPolicy(normalizedUrl, {
@@ -1059,60 +1352,60 @@ program.command("policy-check").description("Cross-reference privacy policy text
1059
1352
  console.log(JSON.stringify(result, null, 2));
1060
1353
  } else {
1061
1354
  console.log("");
1062
- console.log(chalk4.bold("ETALON Policy vs. Reality Audit"));
1063
- console.log(chalk4.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1064
- console.log(`URL: ${chalk4.cyan(result.url)}`);
1065
- console.log(`Policy page: ${result.policyFound ? chalk4.green(result.policyUrl) : chalk4.red("\u2717 Not found")}`);
1355
+ console.log(chalk6.bold("ETALON Policy vs. Reality Audit"));
1356
+ console.log(chalk6.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1357
+ console.log(`URL: ${chalk6.cyan(result.url)}`);
1358
+ console.log(`Policy page: ${result.policyFound ? chalk6.green(result.policyUrl) : chalk6.red("\u2717 Not found")}`);
1066
1359
  console.log("");
1067
- console.log(`\u{1F4CB} Vendors mentioned in policy: ${chalk4.bold(String(result.mentionedVendors.length))}`);
1068
- console.log(`\u{1F50D} Vendors detected by scan: ${chalk4.bold(String(result.detectedVendors.length))}`);
1360
+ console.log(`\u{1F4CB} Vendors mentioned in policy: ${chalk6.bold(String(result.mentionedVendors.length))}`);
1361
+ console.log(`\u{1F50D} Vendors detected by scan: ${chalk6.bold(String(result.detectedVendors.length))}`);
1069
1362
  console.log("");
1070
1363
  if (!result.policyFound) {
1071
- console.log(chalk4.red.bold("\u26A0 No privacy policy page found \u2014 all detected trackers are undisclosed"));
1364
+ console.log(chalk6.red.bold("\u26A0 No privacy policy page found \u2014 all detected trackers are undisclosed"));
1072
1365
  console.log("");
1073
1366
  }
1074
1367
  if (result.undisclosed.length > 0) {
1075
- console.log(chalk4.red.bold(`\u{1F534} ${result.undisclosed.length} UNDISCLOSED (detected on site, not in policy):`));
1368
+ console.log(chalk6.red.bold(`\u{1F534} ${result.undisclosed.length} UNDISCLOSED (detected on site, not in policy):`));
1076
1369
  for (const m of result.undisclosed) {
1077
- const icon = m.severity === "critical" ? chalk4.red("\u2717 CRITICAL") : m.severity === "high" ? chalk4.yellow("\u2717 HIGH") : m.severity === "medium" ? chalk4.yellow("\u2717 MEDIUM") : chalk4.dim("\u2717 LOW");
1370
+ const icon = m.severity === "critical" ? chalk6.red("\u2717 CRITICAL") : m.severity === "high" ? chalk6.yellow("\u2717 HIGH") : m.severity === "medium" ? chalk6.yellow("\u2717 MEDIUM") : chalk6.dim("\u2717 LOW");
1078
1371
  console.log(` ${icon} ${m.vendorName} \u2014 ${m.message}`);
1079
1372
  }
1080
1373
  console.log("");
1081
1374
  }
1082
1375
  if (result.disclosed.length > 0) {
1083
- console.log(chalk4.green.bold(`\u2713 ${result.disclosed.length} DISCLOSED (in both policy and scan):`));
1376
+ console.log(chalk6.green.bold(`\u2713 ${result.disclosed.length} DISCLOSED (in both policy and scan):`));
1084
1377
  for (const m of result.disclosed) {
1085
- console.log(` ${chalk4.green("\u2713")} ${m.vendorName}`);
1378
+ console.log(` ${chalk6.green("\u2713")} ${m.vendorName}`);
1086
1379
  }
1087
1380
  console.log("");
1088
1381
  }
1089
1382
  if (result.overclaimed.length > 0) {
1090
- console.log(chalk4.dim(`\u2139 ${result.overclaimed.length} OVERCLAIMED (in policy, not detected):`));
1383
+ console.log(chalk6.dim(`\u2139 ${result.overclaimed.length} OVERCLAIMED (in policy, not detected):`));
1091
1384
  for (const m of result.overclaimed) {
1092
- console.log(` ${chalk4.dim("\u2013")} ${m.vendorName}`);
1385
+ console.log(` ${chalk6.dim("\u2013")} ${m.vendorName}`);
1093
1386
  }
1094
1387
  console.log("");
1095
1388
  }
1096
1389
  if (result.pass) {
1097
- console.log(chalk4.green.bold("\u2713 All detected trackers are disclosed in the privacy policy"));
1390
+ console.log(chalk6.green.bold("\u2713 All detected trackers are disclosed in the privacy policy"));
1098
1391
  } else {
1099
- console.log(chalk4.red.bold(`\u2717 ${result.undisclosed.length} tracker(s) not disclosed in privacy policy`));
1392
+ console.log(chalk6.red.bold(`\u2717 ${result.undisclosed.length} tracker(s) not disclosed in privacy policy`));
1100
1393
  }
1101
1394
  console.log("");
1102
1395
  if (result.disclosures.length > 0) {
1103
- console.log(chalk4.bold.cyan("\u{1F4DD} Add this to your privacy policy:"));
1104
- console.log(chalk4.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1396
+ console.log(chalk6.bold.cyan("\u{1F4DD} Add this to your privacy policy:"));
1397
+ console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1105
1398
  for (const d of result.disclosures) {
1106
1399
  console.log("");
1107
- console.log(chalk4.bold(` ${d.vendorName}`));
1400
+ console.log(chalk6.bold(` ${d.vendorName}`));
1108
1401
  console.log(` ${d.snippet}`);
1109
1402
  if (d.dpaUrl) {
1110
- console.log(chalk4.dim(` DPA: ${d.dpaUrl}`));
1403
+ console.log(chalk6.dim(` DPA: ${d.dpaUrl}`));
1111
1404
  }
1112
1405
  }
1113
1406
  console.log("");
1114
- console.log(chalk4.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1115
- console.log(chalk4.dim("Copy the text above into your privacy policy or CMS."));
1407
+ console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1408
+ console.log(chalk6.dim("Copy the text above into your privacy policy or CMS."));
1116
1409
  console.log("");
1117
1410
  }
1118
1411
  }
@@ -1127,10 +1420,10 @@ Error: ${error.message}`);
1127
1420
  }
1128
1421
  });
1129
1422
  program.command("generate-policy").description("Generate a GDPR privacy policy from code audit + network scan").argument("[dir]", "Project directory to audit", "./").requiredOption("--company <name>", "Your company/organization name").requiredOption("--email <email>", "Privacy contact / DPO email").option("--url <url>", "Also scan a live URL for network trackers").option("--country <country>", 'Jurisdiction (e.g. "EU", "Germany")').option("-o, --output <file>", "Output file", "privacy-policy.md").option("-f, --format <format>", "Output format: md, html, txt", "md").action(async (dir, options) => {
1130
- const spinner = ora("Generating privacy policy...").start();
1423
+ const spinner = ora2("Generating privacy policy...").start();
1131
1424
  try {
1132
1425
  spinner.text = "Running code audit...";
1133
- const audit = await auditProject(dir);
1426
+ const audit = await auditProject2(dir);
1134
1427
  spinner.text = "Analyzing data flows...";
1135
1428
  const { collectFiles } = await import("@etalon/core");
1136
1429
  let dataFlow;
@@ -1161,32 +1454,32 @@ program.command("generate-policy").description("Generate a GDPR privacy policy f
1161
1454
  dataFlow
1162
1455
  });
1163
1456
  const outputFile = options.output ?? "privacy-policy.md";
1164
- writeFileSync2(outputFile, policy.fullText, "utf-8");
1457
+ writeFileSync3(outputFile, policy.fullText, "utf-8");
1165
1458
  spinner.succeed(`Privacy policy generated!`);
1166
1459
  console.log("");
1167
- console.log(chalk4.bold("ETALON Privacy Policy Generator"));
1168
- console.log(chalk4.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1169
- console.log(`Company: ${chalk4.cyan(options.company)}`);
1170
- console.log(`Contact: ${chalk4.cyan(options.email)}`);
1460
+ console.log(chalk6.bold("ETALON Privacy Policy Generator"));
1461
+ console.log(chalk6.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1462
+ console.log(`Company: ${chalk6.cyan(options.company)}`);
1463
+ console.log(`Contact: ${chalk6.cyan(options.email)}`);
1171
1464
  if (options.url) {
1172
- console.log(`Site scanned: ${chalk4.cyan(options.url)}`);
1465
+ console.log(`Site scanned: ${chalk6.cyan(options.url)}`);
1173
1466
  }
1174
- console.log(`Sources: ${chalk4.dim(policy.meta.sources.join(", "))}`);
1467
+ console.log(`Sources: ${chalk6.dim(policy.meta.sources.join(", "))}`);
1175
1468
  console.log("");
1176
- console.log(`\u{1F4CB} Sections: ${chalk4.bold(String(policy.sections.length))}`);
1177
- console.log(`\u{1F50D} Vendors: ${chalk4.bold(String(policy.vendors.length))}`);
1178
- console.log(`\u{1F6E1}\uFE0F PII types: ${chalk4.bold(String(policy.piiTypes.length))}`);
1469
+ console.log(`\u{1F4CB} Sections: ${chalk6.bold(String(policy.sections.length))}`);
1470
+ console.log(`\u{1F50D} Vendors: ${chalk6.bold(String(policy.vendors.length))}`);
1471
+ console.log(`\u{1F6E1}\uFE0F PII types: ${chalk6.bold(String(policy.piiTypes.length))}`);
1179
1472
  console.log("");
1180
1473
  if (policy.vendors.length > 0) {
1181
- console.log(chalk4.dim("Third-party vendors included:"));
1474
+ console.log(chalk6.dim("Third-party vendors included:"));
1182
1475
  for (const v of policy.vendors) {
1183
- const src = v.source === "both" ? chalk4.magenta("code+network") : v.source === "code" ? chalk4.blue("code") : chalk4.green("network");
1184
- console.log(` ${chalk4.bold(v.vendorName)} ${chalk4.dim(`(${v.category})`)} \u2014 ${src}`);
1476
+ const src = v.source === "both" ? chalk6.magenta("code+network") : v.source === "code" ? chalk6.blue("code") : chalk6.green("network");
1477
+ console.log(` ${chalk6.bold(v.vendorName)} ${chalk6.dim(`(${v.category})`)} \u2014 ${src}`);
1185
1478
  }
1186
1479
  console.log("");
1187
1480
  }
1188
- console.log(chalk4.green(`\u2713 Written to ${chalk4.bold(outputFile)}`));
1189
- console.log(chalk4.dim("\u26A0 Review with a legal professional before publishing."));
1481
+ console.log(chalk6.green(`\u2713 Written to ${chalk6.bold(outputFile)}`));
1482
+ console.log(chalk6.dim("\u26A0 Review with a legal professional before publishing."));
1190
1483
  console.log("");
1191
1484
  } catch (error) {
1192
1485
  spinner.fail("Policy generation failed");
@@ -1198,16 +1491,16 @@ Error: ${error.message}`);
1198
1491
  }
1199
1492
  });
1200
1493
  program.command("badge").description("Generate a compliance badge SVG for your README").argument("[dir]", "Directory to audit", "./").option("-o, --output <file>", "Output file", "etalon-badge.svg").action(async (dir, options) => {
1201
- const spinner = ora("Generating badge...").start();
1494
+ const spinner = ora2("Generating badge...").start();
1202
1495
  try {
1203
- const report = await auditProject(dir);
1496
+ const report = await auditProject2(dir);
1204
1497
  const score = report.score ?? calculateScore(report);
1205
1498
  const svg = generateBadgeSvg(score);
1206
- writeFileSync2(options.output, svg, "utf-8");
1207
- spinner.succeed(`Badge written to ${chalk4.cyan(options.output)} \u2014 Grade: ${score.grade} (${score.score}/100)`);
1499
+ writeFileSync3(options.output, svg, "utf-8");
1500
+ spinner.succeed(`Badge written to ${chalk6.cyan(options.output)} \u2014 Grade: ${score.grade} (${score.score}/100)`);
1208
1501
  console.log("");
1209
- console.log(chalk4.bold("Shields.io badge for your README:"));
1210
- console.log(chalk4.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1502
+ console.log(chalk6.bold("Shields.io badge for your README:"));
1503
+ console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1211
1504
  const { badgeMarkdown: toBadgeMd } = await import("@etalon/core");
1212
1505
  console.log(toBadgeMd(score.grade, score.score));
1213
1506
  console.log("");
@@ -1219,14 +1512,14 @@ Error: ${error.message}`);
1219
1512
  }
1220
1513
  });
1221
1514
  program.command("data-flow").description("Map PII data flows through your codebase").argument("[dir]", "Directory to analyze", "./").option("-f, --format <format>", "Output format: text, mermaid, json", "text").action(async (dir, options) => {
1222
- const spinner = ora("Analyzing data flows...").start();
1515
+ const spinner = ora2("Analyzing data flows...").start();
1223
1516
  try {
1224
1517
  const { readdirSync } = await import("fs");
1225
- const { join: join3, relative } = await import("path");
1518
+ const { join: join4, relative } = await import("path");
1226
1519
  const files = [];
1227
1520
  const walk = (d) => {
1228
1521
  for (const entry of readdirSync(d, { withFileTypes: true })) {
1229
- const full = join3(d, entry.name);
1522
+ const full = join4(d, entry.name);
1230
1523
  if (entry.isDirectory()) {
1231
1524
  if (!["node_modules", ".git", "dist", "build", ".next", "__pycache__"].includes(entry.name)) {
1232
1525
  walk(full);
@@ -1266,76 +1559,96 @@ program.command("report-fp").description("Report a false positive finding").requ
1266
1559
  reason: options.reason ?? "Not a tracker",
1267
1560
  suggested_action: "whitelist_domain"
1268
1561
  });
1269
- console.log(chalk4.green(`\u2713 False positive reported: ${chalk4.bold(report.id)}`));
1270
- console.log(chalk4.dim(` Domain: ${report.domain}`));
1271
- console.log(chalk4.dim(` Rule: ${report.rule}`));
1272
- console.log(chalk4.dim(` Reason: ${report.reason}`));
1562
+ console.log(chalk6.green(`\u2713 False positive reported: ${chalk6.bold(report.id)}`));
1563
+ console.log(chalk6.dim(` Domain: ${report.domain}`));
1564
+ console.log(chalk6.dim(` Rule: ${report.rule}`));
1565
+ console.log(chalk6.dim(` Reason: ${report.reason}`));
1273
1566
  console.log("");
1274
- console.log(chalk4.dim("Reports help ETALON learn and reduce false positives over time."));
1567
+ console.log(chalk6.dim("Reports help ETALON learn and reduce false positives over time."));
1275
1568
  });
1276
1569
  program.command("telemetry").description("Manage anonymous usage telemetry").argument("<action>", "enable, disable, or status").action((action) => {
1277
1570
  switch (action) {
1278
1571
  case "enable":
1279
1572
  enableTelemetry();
1280
- console.log(chalk4.green("\u2713 Telemetry enabled."));
1281
- console.log(chalk4.dim(" Anonymous usage data helps improve ETALON for everyone."));
1282
- console.log(chalk4.dim(" No PII is ever collected. Set DO_NOT_TRACK=1 to override."));
1573
+ console.log(chalk6.green("\u2713 Telemetry enabled."));
1574
+ console.log(chalk6.dim(" Anonymous usage data helps improve ETALON for everyone."));
1575
+ console.log(chalk6.dim(" No PII is ever collected. Set DO_NOT_TRACK=1 to override."));
1283
1576
  break;
1284
1577
  case "disable":
1285
1578
  disableTelemetry();
1286
- console.log(chalk4.yellow("\u2713 Telemetry disabled."));
1579
+ console.log(chalk6.yellow("\u2713 Telemetry disabled."));
1287
1580
  break;
1288
1581
  case "status":
1289
- console.log(`Telemetry: ${isTelemetryEnabled() ? chalk4.green("enabled") : chalk4.yellow("disabled")}`);
1582
+ console.log(`Telemetry: ${isTelemetryEnabled() ? chalk6.green("enabled") : chalk6.yellow("disabled")}`);
1290
1583
  break;
1291
1584
  default:
1292
- console.error(chalk4.red(`Unknown action: ${action}. Use enable, disable, or status.`));
1585
+ console.error(chalk6.red(`Unknown action: ${action}. Use enable, disable, or status.`));
1293
1586
  process.exit(1);
1294
1587
  }
1295
1588
  });
1296
1589
  program.command("intelligence").description("Show intelligence engine status and learned patterns").argument("[dir]", "Project directory for context detection", "./").action((dir) => {
1297
- console.log(chalk4.bold("ETALON Intelligence Engine"));
1298
- console.log(chalk4.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1590
+ console.log(chalk6.bold("ETALON Intelligence Engine"));
1591
+ console.log(chalk6.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1299
1592
  console.log("");
1300
1593
  const ctx = detectProjectContext(dir);
1301
- console.log(chalk4.bold("\u{1F3AF} Project Context"));
1302
- console.log(` Industry: ${chalk4.cyan(ctx.industry)}`);
1303
- console.log(` Region: ${chalk4.cyan(ctx.region)}`);
1304
- console.log(` Data Sensitivity: ${chalk4.cyan(ctx.data_sensitivity)}`);
1594
+ console.log(chalk6.bold("\u{1F3AF} Project Context"));
1595
+ console.log(` Industry: ${chalk6.cyan(ctx.industry)}`);
1596
+ console.log(` Region: ${chalk6.cyan(ctx.region)}`);
1597
+ console.log(` Data Sensitivity: ${chalk6.cyan(ctx.data_sensitivity)}`);
1305
1598
  if (ctx.detected_signals.length > 0) {
1306
- console.log(chalk4.dim(" Signals:"));
1599
+ console.log(chalk6.dim(" Signals:"));
1307
1600
  for (const s of ctx.detected_signals) {
1308
- console.log(chalk4.dim(` \u2022 ${s}`));
1601
+ console.log(chalk6.dim(` \u2022 ${s}`));
1309
1602
  }
1310
1603
  }
1311
1604
  console.log("");
1312
1605
  const stats = getLearningStats();
1313
- console.log(chalk4.bold("\u{1F9E0} Learning Engine"));
1314
- console.log(` Patterns learned: ${chalk4.cyan(String(stats.patterns_learned))}`);
1315
- console.log(` Feedback processed: ${chalk4.cyan(String(stats.feedback_processed))}`);
1316
- console.log(` Impact: ${chalk4.cyan(stats.accuracy_improvement)}`);
1606
+ console.log(chalk6.bold("\u{1F9E0} Learning Engine"));
1607
+ console.log(` Patterns learned: ${chalk6.cyan(String(stats.patterns_learned))}`);
1608
+ console.log(` Feedback processed: ${chalk6.cyan(String(stats.feedback_processed))}`);
1609
+ console.log(` Impact: ${chalk6.cyan(stats.accuracy_improvement)}`);
1317
1610
  console.log("");
1318
1611
  const feedback = getFeedbackSummary();
1319
1612
  if (feedback.total_reports > 0) {
1320
- console.log(chalk4.bold("\u{1F4CA} False Positive Reports"));
1613
+ console.log(chalk6.bold("\u{1F4CA} False Positive Reports"));
1321
1614
  console.log(` Total reports: ${feedback.total_reports}`);
1322
1615
  if (feedback.suggested_whitelists.length > 0) {
1323
- console.log(chalk4.dim(" Suggested whitelists (3+ reports):"));
1616
+ console.log(chalk6.dim(" Suggested whitelists (3+ reports):"));
1324
1617
  for (const d of feedback.suggested_whitelists) {
1325
- console.log(chalk4.dim(` \u2022 ${d}`));
1618
+ console.log(chalk6.dim(` \u2022 ${d}`));
1326
1619
  }
1327
1620
  }
1328
1621
  console.log("");
1329
1622
  }
1330
1623
  const learned = analyzePatterns();
1331
1624
  if (learned.length > 0) {
1332
- console.log(chalk4.bold("\u{1F4DD} Learned Patterns"));
1625
+ console.log(chalk6.bold("\u{1F4DD} Learned Patterns"));
1333
1626
  for (const p of learned) {
1334
- console.log(` ${chalk4.cyan(p.domain)} \u2014 ${p.suggested_action} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
1627
+ console.log(` ${chalk6.cyan(p.domain)} \u2014 ${p.suggested_action} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
1335
1628
  }
1336
1629
  console.log("");
1337
1630
  }
1338
- console.log(chalk4.dim("Telemetry: ") + (isTelemetryEnabled() ? chalk4.green("enabled") : chalk4.yellow("disabled")));
1631
+ console.log(chalk6.dim("Telemetry: ") + (isTelemetryEnabled() ? chalk6.green("enabled") : chalk6.yellow("disabled")));
1339
1632
  console.log("");
1340
1633
  });
1634
+ var authCmd = program.command("auth").description("Manage ETALON Cloud authentication");
1635
+ authCmd.command("login").description("Login with API key from dashboard").action(async () => {
1636
+ await runLogin();
1637
+ });
1638
+ authCmd.command("logout").description("Logout and remove stored API key").action(() => {
1639
+ runLogout();
1640
+ });
1641
+ authCmd.command("status").description("Show current authentication status").action(async () => {
1642
+ await runStatus();
1643
+ });
1644
+ program.command("push").description("Scan a website, audit codebase, and upload results to ETALON Cloud").argument("<url>", "URL to scan").argument("[dir]", "Directory to audit", "./").option("--site <id>", "Site ID (from dashboard or `etalon sites`)").option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").option("-d, --deep", "Deep scan mode", false).action(async (url, dir, options) => {
1645
+ await runPush(url, dir, {
1646
+ site: options.site,
1647
+ timeout: options.timeout,
1648
+ deep: options.deep
1649
+ });
1650
+ });
1651
+ program.command("sites").description("List your cloud sites and their IDs").action(async () => {
1652
+ await runListSites();
1653
+ });
1341
1654
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etalon/cli",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "ETALON — Privacy audit tool for websites. Scan any site for trackers and GDPR compliance.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",