@usebetterdev/audit-cli 0.6.1 → 0.8.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.
package/dist/check.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { CliSilentExitError, CheckOutput, CheckResult } from '@usebetterdev/cli-utils';
2
+ export { CheckOutput, CheckResult } from '@usebetterdev/cli-utils';
1
3
  import { ColumnType } from '@usebetterdev/audit-core';
2
- import { D as DatabaseDialect } from './detect-adapter-DNHcPCKz.js';
4
+ import { D as DatabaseDialect } from './detect-adapter-WIB0ARFR.js';
3
5
 
4
6
  /**
5
7
  * `better-audit check` — Verify audit_logs table schema, indexes, and write path.
@@ -20,19 +22,8 @@ interface CheckOptions {
20
22
  databaseUrl?: string | undefined;
21
23
  verbose?: boolean | undefined;
22
24
  }
23
- interface CheckResult {
24
- check: string;
25
- passed: boolean;
26
- message?: string;
27
- remediation?: string;
28
- }
29
- interface CheckOutput {
30
- passed: boolean;
31
- results: CheckResult[];
32
- warnings: CheckResult[];
33
- }
34
25
  /** Thrown when one or more checks fail. Carries the full output for CLI rendering. */
35
- declare class CheckFailedError extends Error {
26
+ declare class CheckFailedError extends CliSilentExitError {
36
27
  readonly output: CheckOutput;
37
28
  constructor(output: CheckOutput);
38
29
  }
@@ -61,4 +52,4 @@ declare function runCheck(databaseUrl: string): Promise<CheckOutput>;
61
52
  /** CLI action handler for `better-audit check`. */
62
53
  declare function check(options?: CheckOptions): Promise<void>;
63
54
 
64
- export { CheckFailedError, type CheckOptions, type CheckOutput, type CheckResult, DIALECT_TYPE_MAP, type IntrospectedColumn as _IntrospectedColumn, checkColumns as _checkColumns, checkIndexes as _checkIndexes, check, runCheck };
55
+ export { CheckFailedError, type CheckOptions, DIALECT_TYPE_MAP, type IntrospectedColumn as _IntrospectedColumn, checkColumns as _checkColumns, checkIndexes as _checkIndexes, check, runCheck };
package/dist/check.js CHANGED
@@ -5,10 +5,10 @@ import {
5
5
  checkColumns,
6
6
  checkIndexes,
7
7
  runCheck
8
- } from "./chunk-55KKYFKR.js";
9
- import "./chunk-O5LHE2AC.js";
8
+ } from "./chunk-PGOZIYS5.js";
9
+ import "./chunk-Q23AEDWF.js";
10
10
  import "./chunk-7GSN73TA.js";
11
- import "./chunk-HDO5P6X7.js";
11
+ import "./chunk-GHOKL227.js";
12
12
  export {
13
13
  CheckFailedError,
14
14
  DIALECT_TYPE_MAP,
@@ -1,17 +1,23 @@
1
1
  import {
2
- generateMigrationSql
3
- } from "./chunk-O5LHE2AC.js";
2
+ generateMigrationSql,
3
+ generatePluginMigrationSql
4
+ } from "./chunk-Q23AEDWF.js";
4
5
  import {
5
- detectAdapter,
6
6
  detectDialect,
7
+ detectOrmAdapter,
7
8
  findMigrationDirectory,
8
9
  formatTimestamp
9
- } from "./chunk-HDO5P6X7.js";
10
+ } from "./chunk-GHOKL227.js";
10
11
 
11
12
  // src/migrate.ts
12
13
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
13
14
  import { dirname, join } from "path";
14
15
  import pc from "picocolors";
16
+ import {
17
+ loadBetterConfig,
18
+ BetterConfigNotFoundError
19
+ } from "@usebetterdev/plugin/config";
20
+ import { mergeSchemas } from "@usebetterdev/plugin";
15
21
  var AUDIT_TABLE_REGEX = /create\s+table\b[^;]*\baudit_logs\b/i;
16
22
  var MAX_SCAN_DEPTH = 3;
17
23
  function migrationAlreadyExists(directory, depth = 0) {
@@ -47,12 +53,15 @@ function migrationAlreadyExists(directory, depth = 0) {
47
53
  async function migrate(options = {}) {
48
54
  const cwd = options.cwd ?? process.cwd();
49
55
  const dialect = options.dialect ?? detectDialect(options.databaseUrl ?? process.env["DATABASE_URL"]);
50
- const sql = generateMigrationSql(dialect);
56
+ let sql = generateMigrationSql(dialect);
57
+ if (options.noPlugins !== true) {
58
+ sql += await loadPluginSql(cwd, dialect);
59
+ }
51
60
  if (options.dryRun === true) {
52
61
  process.stdout.write(sql);
53
62
  return;
54
63
  }
55
- const adapter = options.adapter ?? detectAdapter(cwd);
64
+ const adapter = options.adapter ?? detectOrmAdapter(cwd);
56
65
  if (adapter === void 0) {
57
66
  throw new Error(
58
67
  "Could not detect ORM adapter. Install drizzle-orm or @prisma/client, or pass --adapter drizzle|prisma."
@@ -66,12 +75,12 @@ async function migrate(options = {}) {
66
75
  outputPath = dirname(filePath);
67
76
  } else {
68
77
  outputPath = options.output;
69
- filePath = adapter === "prisma" ? join(outputPath, "migration.sql") : join(outputPath, `${formatTimestamp(/* @__PURE__ */ new Date())}_audit_logs.sql`);
78
+ filePath = adapter === "prisma" ? join(outputPath, "migration.sql") : join(outputPath, `${formatTimestamp(/* @__PURE__ */ new Date(), "")}_audit_logs.sql`);
70
79
  }
71
80
  } else {
72
81
  const migrationDir = findMigrationDirectory(cwd, adapter);
73
82
  outputPath = migrationDir;
74
- filePath = adapter === "prisma" ? join(migrationDir, "migration.sql") : join(migrationDir, `${formatTimestamp(/* @__PURE__ */ new Date())}_audit_logs.sql`);
83
+ filePath = adapter === "prisma" ? join(migrationDir, "migration.sql") : join(migrationDir, `${formatTimestamp(/* @__PURE__ */ new Date(), "")}_audit_logs.sql`);
75
84
  }
76
85
  const scanDir = adapter === "prisma" ? join(cwd, "prisma", "migrations") : outputPath;
77
86
  if (migrationAlreadyExists(scanDir)) {
@@ -94,8 +103,24 @@ async function migrate(options = {}) {
94
103
  console.log(` ${pc.yellow("$")} npx prisma migrate dev`);
95
104
  }
96
105
  }
106
+ async function loadPluginSql(cwd, dialect) {
107
+ try {
108
+ const config = await loadBetterConfig({ cwd });
109
+ const plugins = config.audit?.plugins;
110
+ if (!plugins || plugins.length === 0) {
111
+ return "";
112
+ }
113
+ const schema = mergeSchemas(plugins);
114
+ return generatePluginMigrationSql(schema, dialect);
115
+ } catch (error) {
116
+ if (error instanceof BetterConfigNotFoundError) {
117
+ return "";
118
+ }
119
+ throw error;
120
+ }
121
+ }
97
122
 
98
123
  export {
99
124
  migrate
100
125
  };
101
- //# sourceMappingURL=chunk-WVH5TQ2O.js.map
126
+ //# sourceMappingURL=chunk-AWFOUH4H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["/**\n * `better-audit migrate` — Generate the audit_logs table migration.\n *\n * Generates appropriate DDL for the configured database:\n * - Postgres: JSONB columns, TIMESTAMPTZ, gen_random_uuid()\n * - MySQL: JSON columns, DATETIME(6), UUID() default\n * - SQLite: TEXT columns for JSON, no UUID default\n *\n * Writes the migration file into the appropriate ORM directory,\n * or prints to stdout with --dry-run.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport pc from \"picocolors\";\nimport { generateMigrationSql, generatePluginMigrationSql, type DatabaseDialect } from \"./generate-sql.js\";\nimport { formatTimestamp } from \"@usebetterdev/cli-utils\";\nimport {\n detectAdapter,\n detectDialect,\n findMigrationDirectory,\n type AdapterType,\n} from \"./detect-adapter.js\";\nimport {\n loadBetterConfig,\n BetterConfigNotFoundError,\n} from \"@usebetterdev/plugin/config\";\nimport { mergeSchemas } from \"@usebetterdev/plugin\";\n\nexport interface MigrateOptions {\n /** Output SQL to stdout instead of writing a file */\n dryRun?: boolean | undefined;\n /** Override ORM auto-detection */\n adapter?: AdapterType | undefined;\n /** Override database dialect detection */\n dialect?: DatabaseDialect | undefined;\n /** Override output directory/file */\n output?: string | undefined;\n /** Working directory (defaults to process.cwd()) */\n cwd?: string | undefined;\n /** Database connection URL for dialect detection (defaults to DATABASE_URL env) */\n databaseUrl?: string | undefined;\n /** Skip plugin table generation even if plugins are configured */\n noPlugins?: boolean | undefined;\n}\n\n/** Regex to detect an existing audit_logs CREATE TABLE statement (case-insensitive). */\nconst AUDIT_TABLE_REGEX = /create\\s+table\\b[^;]*\\baudit_logs\\b/i;\n\n/** Maximum directory depth for the idempotency scan. */\nconst MAX_SCAN_DEPTH = 3;\n\n/**\n * Check if a migration for `audit_logs` already exists in the given directory.\n * Scans `.sql` files up to `MAX_SCAN_DEPTH` levels deep, skipping symlinks.\n */\nfunction migrationAlreadyExists(directory: string, depth: number = 0): boolean {\n if (depth > MAX_SCAN_DEPTH) {\n return false;\n }\n if (!existsSync(directory)) {\n return false;\n }\n\n try {\n const entries = readdirSync(directory, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isSymbolicLink()) {\n continue;\n }\n const fullPath = join(directory, entry.name);\n if (entry.isFile() && entry.name.endsWith(\".sql\")) {\n const content = readFileSync(fullPath, \"utf-8\");\n if (AUDIT_TABLE_REGEX.test(content)) {\n return true;\n }\n }\n if (entry.isDirectory()) {\n if (migrationAlreadyExists(fullPath, depth + 1)) {\n return true;\n }\n }\n }\n } catch {\n // Read error — treat as no existing migration\n }\n\n return false;\n}\n\nexport async function migrate(options: MigrateOptions = {}): Promise<void> {\n const cwd = options.cwd ?? process.cwd();\n\n // 1. Resolve dialect\n const dialect: DatabaseDialect =\n options.dialect ?? detectDialect(options.databaseUrl ?? process.env[\"DATABASE_URL\"]);\n\n // 2. Generate SQL\n let sql = generateMigrationSql(dialect);\n\n // 2b. Load plugin schemas from better.config.ts (unless --no-plugins)\n if (options.noPlugins !== true) {\n sql += await loadPluginSql(cwd, dialect);\n }\n\n // 3. Dry-run: print to stdout and return\n if (options.dryRun === true) {\n process.stdout.write(sql);\n return;\n }\n\n // 4. Resolve adapter\n const adapter: AdapterType | undefined =\n options.adapter ?? detectAdapter(cwd);\n\n if (adapter === undefined) {\n throw new Error(\n \"Could not detect ORM adapter. Install drizzle-orm or @prisma/client, \" +\n \"or pass --adapter drizzle|prisma.\",\n );\n }\n\n // 5. Resolve output directory\n let outputPath: string;\n let filePath: string;\n\n if (options.output !== undefined) {\n if (options.output.endsWith(\".sql\")) {\n filePath = options.output;\n outputPath = dirname(filePath);\n } else {\n outputPath = options.output;\n filePath =\n adapter === \"prisma\"\n ? join(outputPath, \"migration.sql\")\n : join(outputPath, `${formatTimestamp(new Date(), \"\")}_audit_logs.sql`);\n }\n } else {\n const migrationDir = findMigrationDirectory(cwd, adapter);\n outputPath = migrationDir;\n filePath =\n adapter === \"prisma\"\n ? join(migrationDir, \"migration.sql\")\n : join(migrationDir, `${formatTimestamp(new Date(), \"\")}_audit_logs.sql`);\n }\n\n // 6. Check idempotency — scan parent migration directory\n const scanDir =\n adapter === \"prisma\"\n ? join(cwd, \"prisma\", \"migrations\")\n : outputPath;\n\n if (migrationAlreadyExists(scanDir)) {\n console.log(\n pc.green(\"✓\") + \" audit_logs migration already exists — up to date.\",\n );\n return;\n }\n\n // 7. Warn if output file already exists (explicit --output path)\n if (options.output !== undefined && existsSync(filePath)) {\n console.warn(pc.yellow(`Warning: overwriting existing file ${filePath}`));\n }\n\n // 8. Write migration file\n mkdirSync(outputPath, { recursive: true });\n writeFileSync(filePath, sql, \"utf-8\");\n\n console.log(`${pc.green(\"✓\")} Wrote migration: ${pc.dim(filePath)}`);\n console.log(\"\");\n console.log(pc.bold(\"Next steps:\"));\n\n if (adapter === \"drizzle\") {\n console.log(` ${pc.yellow(\"$\")} npx drizzle-kit migrate`);\n } else {\n console.log(` ${pc.yellow(\"$\")} npx prisma migrate dev`);\n }\n}\n\n/**\n * Attempt to load better.config.ts and generate plugin migration SQL.\n * Returns empty string if config not found or no audit plugins defined.\n */\nasync function loadPluginSql(\n cwd: string,\n dialect: DatabaseDialect,\n): Promise<string> {\n try {\n const config = await loadBetterConfig({ cwd });\n const plugins = config.audit?.plugins;\n if (!plugins || plugins.length === 0) {\n return \"\";\n }\n const schema = mergeSchemas(plugins);\n return generatePluginMigrationSql(schema, dialect);\n } catch (error) {\n if (error instanceof BetterConfigNotFoundError) {\n return \"\";\n }\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAS,YAAY,WAAW,aAAa,cAAc,qBAAqB;AAChF,SAAS,SAAS,YAAY;AAC9B,OAAO,QAAQ;AASf;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAoB7B,IAAM,oBAAoB;AAG1B,IAAM,iBAAiB;AAMvB,SAAS,uBAAuB,WAAmB,QAAgB,GAAY;AAC7E,MAAI,QAAQ,gBAAgB;AAC1B,WAAO;AAAA,EACT;AACA,MAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,YAAY,WAAW,EAAE,eAAe,KAAK,CAAC;AAC9D,eAAW,SAAS,SAAS;AAC3B,UAAI,MAAM,eAAe,GAAG;AAC1B;AAAA,MACF;AACA,YAAM,WAAW,KAAK,WAAW,MAAM,IAAI;AAC3C,UAAI,MAAM,OAAO,KAAK,MAAM,KAAK,SAAS,MAAM,GAAG;AACjD,cAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,YAAI,kBAAkB,KAAK,OAAO,GAAG;AACnC,iBAAO;AAAA,QACT;AAAA,MACF;AACA,UAAI,MAAM,YAAY,GAAG;AACvB,YAAI,uBAAuB,UAAU,QAAQ,CAAC,GAAG;AAC/C,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAEA,eAAsB,QAAQ,UAA0B,CAAC,GAAkB;AACzE,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AAGvC,QAAM,UACJ,QAAQ,WAAW,cAAc,QAAQ,eAAe,QAAQ,IAAI,cAAc,CAAC;AAGrF,MAAI,MAAM,qBAAqB,OAAO;AAGtC,MAAI,QAAQ,cAAc,MAAM;AAC9B,WAAO,MAAM,cAAc,KAAK,OAAO;AAAA,EACzC;AAGA,MAAI,QAAQ,WAAW,MAAM;AAC3B,YAAQ,OAAO,MAAM,GAAG;AACxB;AAAA,EACF;AAGA,QAAM,UACJ,QAAQ,WAAW,iBAAc,GAAG;AAEtC,MAAI,YAAY,QAAW;AACzB,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAGA,MAAI;AACJ,MAAI;AAEJ,MAAI,QAAQ,WAAW,QAAW;AAChC,QAAI,QAAQ,OAAO,SAAS,MAAM,GAAG;AACnC,iBAAW,QAAQ;AACnB,mBAAa,QAAQ,QAAQ;AAAA,IAC/B,OAAO;AACL,mBAAa,QAAQ;AACrB,iBACE,YAAY,WACR,KAAK,YAAY,eAAe,IAChC,KAAK,YAAY,GAAG,gBAAgB,oBAAI,KAAK,GAAG,EAAE,CAAC,iBAAiB;AAAA,IAC5E;AAAA,EACF,OAAO;AACL,UAAM,eAAe,uBAAuB,KAAK,OAAO;AACxD,iBAAa;AACb,eACE,YAAY,WACR,KAAK,cAAc,eAAe,IAClC,KAAK,cAAc,GAAG,gBAAgB,oBAAI,KAAK,GAAG,EAAE,CAAC,iBAAiB;AAAA,EAC9E;AAGA,QAAM,UACJ,YAAY,WACR,KAAK,KAAK,UAAU,YAAY,IAChC;AAEN,MAAI,uBAAuB,OAAO,GAAG;AACnC,YAAQ;AAAA,MACN,GAAG,MAAM,QAAG,IAAI;AAAA,IAClB;AACA;AAAA,EACF;AAGA,MAAI,QAAQ,WAAW,UAAa,WAAW,QAAQ,GAAG;AACxD,YAAQ,KAAK,GAAG,OAAO,sCAAsC,QAAQ,EAAE,CAAC;AAAA,EAC1E;AAGA,YAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AACzC,gBAAc,UAAU,KAAK,OAAO;AAEpC,UAAQ,IAAI,GAAG,GAAG,MAAM,QAAG,CAAC,qBAAqB,GAAG,IAAI,QAAQ,CAAC,EAAE;AACnE,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,GAAG,KAAK,aAAa,CAAC;AAElC,MAAI,YAAY,WAAW;AACzB,YAAQ,IAAI,KAAK,GAAG,OAAO,GAAG,CAAC,0BAA0B;AAAA,EAC3D,OAAO;AACL,YAAQ,IAAI,KAAK,GAAG,OAAO,GAAG,CAAC,yBAAyB;AAAA,EAC1D;AACF;AAMA,eAAe,cACb,KACA,SACiB;AACjB,MAAI;AACF,UAAM,SAAS,MAAM,iBAAiB,EAAE,IAAI,CAAC;AAC7C,UAAM,UAAU,OAAO,OAAO;AAC9B,QAAI,CAAC,WAAW,QAAQ,WAAW,GAAG;AACpC,aAAO;AAAA,IACT;AACA,UAAM,SAAS,aAAa,OAAO;AACnC,WAAO,2BAA2B,QAAQ,OAAO;AAAA,EACnD,SAAS,OAAO;AACd,QAAI,iBAAiB,2BAA2B;AAC9C,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACF;","names":[]}
@@ -0,0 +1,15 @@
1
+ // src/parse-since.ts
2
+ var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(T[\w:.+-]+)?$/;
3
+ function parseIsoDate(value) {
4
+ const date = new Date(value);
5
+ if (Number.isNaN(date.getTime())) {
6
+ throw new Error(`Invalid date "${value}". Expected ISO-8601 format.`);
7
+ }
8
+ return date;
9
+ }
10
+
11
+ export {
12
+ ISO_DATE_REGEX,
13
+ parseIsoDate
14
+ };
15
+ //# sourceMappingURL=chunk-EEYS3G5Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/parse-since.ts"],"sourcesContent":["/**\n * Shared ISO date parsing primitives for `--since` flag handling.\n *\n * Used by both `export.ts` and `purge.ts`. Each command has its own\n * `parseSinceValue()` with different return types and duration handling,\n * but they share the regex and ISO date validation logic.\n */\n\nexport const ISO_DATE_REGEX = /^\\d{4}-\\d{2}-\\d{2}(T[\\w:.+-]+)?$/;\n\n/**\n * Parse and validate an ISO-8601 date string.\n *\n * Throws if the value is not a valid date. Does **not** handle duration\n * strings — callers are responsible for duration resolution.\n */\nexport function parseIsoDate(value: string): Date {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Invalid date \"${value}\". Expected ISO-8601 format.`);\n }\n return date;\n}\n"],"mappings":";AAQO,IAAM,iBAAiB;AAQvB,SAAS,aAAa,OAAqB;AAChD,QAAM,OAAO,IAAI,KAAK,KAAK;AAC3B,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG;AAChC,UAAM,IAAI,MAAM,iBAAiB,KAAK,8BAA8B;AAAA,EACtE;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,187 @@
1
+ // ../../shared/cli-utils/dist/index.js
2
+ import pc from "picocolors";
3
+ import { existsSync as existsSync2, readFileSync } from "fs";
4
+ import { join as join2 } from "path";
5
+ import { readFileSync as readFileSync2 } from "fs";
6
+ import { dirname as dirname2, join as join3 } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import pc2 from "picocolors";
9
+ import pc3 from "picocolors";
10
+ function formatTimestamp(date, separator = "_") {
11
+ const y = date.getFullYear();
12
+ const m = String(date.getMonth() + 1).padStart(2, "0");
13
+ const d = String(date.getDate()).padStart(2, "0");
14
+ const h = String(date.getHours()).padStart(2, "0");
15
+ const min = String(date.getMinutes()).padStart(2, "0");
16
+ const sec = String(date.getSeconds()).padStart(2, "0");
17
+ return `${y}${m}${d}${separator}${h}${min}${sec}`;
18
+ }
19
+ function detectOrmAdapter(cwd) {
20
+ const pkgPath = join2(cwd, "package.json");
21
+ if (!existsSync2(pkgPath)) {
22
+ return void 0;
23
+ }
24
+ try {
25
+ const content = readFileSync(pkgPath, "utf-8");
26
+ const parsed = JSON.parse(content);
27
+ if (typeof parsed !== "object" || parsed === null) {
28
+ return void 0;
29
+ }
30
+ const allDeps = {};
31
+ if ("dependencies" in parsed && typeof parsed.dependencies === "object" && parsed.dependencies !== null) {
32
+ Object.assign(allDeps, parsed.dependencies);
33
+ }
34
+ if ("devDependencies" in parsed && typeof parsed.devDependencies === "object" && parsed.devDependencies !== null) {
35
+ Object.assign(allDeps, parsed.devDependencies);
36
+ }
37
+ if ("drizzle-orm" in allDeps) {
38
+ return "drizzle";
39
+ }
40
+ if ("@prisma/client" in allDeps) {
41
+ return "prisma";
42
+ }
43
+ } catch {
44
+ }
45
+ return void 0;
46
+ }
47
+ function readCliVersion(metaUrl) {
48
+ const cliDir = dirname2(fileURLToPath(metaUrl));
49
+ try {
50
+ const raw = JSON.parse(
51
+ readFileSync2(join3(cliDir, "..", "package.json"), "utf-8")
52
+ );
53
+ if (typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string") {
54
+ return raw.version;
55
+ }
56
+ } catch {
57
+ }
58
+ return "0.0.0";
59
+ }
60
+ function printCheckResults(output, options) {
61
+ const verbose = options?.verbose === true;
62
+ const failures = [];
63
+ let passedCount = 0;
64
+ for (const r of output.results) {
65
+ if (r.passed) {
66
+ passedCount++;
67
+ } else {
68
+ failures.push(r);
69
+ }
70
+ }
71
+ const failedCount = failures.length;
72
+ if (verbose) {
73
+ console.log("");
74
+ for (const r of output.results) {
75
+ const icon = r.passed ? pc2.green("\u2713") : pc2.red("\u2717");
76
+ const detail = r.message !== void 0 ? pc2.dim(` \u2014 ${r.message}`) : "";
77
+ console.log(` ${icon} ${r.check}${detail}`);
78
+ }
79
+ }
80
+ if (output.warnings.length > 0) {
81
+ console.log("");
82
+ for (const w of output.warnings) {
83
+ console.log(` ${pc2.yellow("\u26A0")} ${w.message ?? w.check}`);
84
+ }
85
+ }
86
+ console.log("");
87
+ if (output.passed) {
88
+ console.log(pc2.green(`\u2713 All ${passedCount} checks passed`));
89
+ } else {
90
+ console.log(
91
+ pc2.red(
92
+ `\u2717 ${failedCount} check${failedCount === 1 ? "" : "s"} failed`
93
+ ) + pc2.dim(`, ${passedCount} passed`)
94
+ );
95
+ console.log("");
96
+ if (verbose) {
97
+ for (const f of failures) {
98
+ if (f.remediation !== void 0) {
99
+ console.log(` ${pc2.cyan("\u2192")} ${f.check}: ${f.remediation}`);
100
+ }
101
+ }
102
+ } else {
103
+ for (const f of failures) {
104
+ console.log(` ${pc2.red("\u2717")} ${f.check}`);
105
+ if (f.message !== void 0) {
106
+ console.log(` ${f.message}`);
107
+ }
108
+ if (f.remediation !== void 0) {
109
+ console.log(` ${pc2.cyan("\u2192")} ${f.remediation}`);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ var CliSilentExitError = class extends Error {
116
+ exitCode;
117
+ constructor(exitCode, message) {
118
+ super(message ?? `CLI exited with code ${String(exitCode)}`);
119
+ this.name = "CliSilentExitError";
120
+ this.exitCode = exitCode;
121
+ }
122
+ };
123
+ function formatErrorMessage(err) {
124
+ if (err instanceof AggregateError && err.errors.length > 0) {
125
+ const nested = err.errors.map((e) => e instanceof Error ? e.message : String(e)).join("; ");
126
+ return err.message !== "" ? `${err.message}: ${nested}` : nested;
127
+ }
128
+ if (err instanceof Error) {
129
+ return err.message;
130
+ }
131
+ return String(err);
132
+ }
133
+ async function runAction(fn) {
134
+ try {
135
+ await fn();
136
+ } catch (err) {
137
+ if (err instanceof CliSilentExitError) {
138
+ process.exit(err.exitCode);
139
+ }
140
+ const message = formatErrorMessage(err);
141
+ console.error(pc3.red(message));
142
+ process.exit(1);
143
+ }
144
+ }
145
+
146
+ // src/detect-adapter.ts
147
+ import { existsSync } from "fs";
148
+ import { join } from "path";
149
+ function detectDialect(databaseUrl) {
150
+ if (databaseUrl === void 0) {
151
+ return "postgres";
152
+ }
153
+ const lower = databaseUrl.toLowerCase();
154
+ if (lower.startsWith("postgres://") || lower.startsWith("postgresql://")) {
155
+ return "postgres";
156
+ }
157
+ if (lower.startsWith("mysql://")) {
158
+ return "mysql";
159
+ }
160
+ if (lower.startsWith("file:") || lower.endsWith(".db") || lower.endsWith(".sqlite") || lower.endsWith(".sqlite3")) {
161
+ return "sqlite";
162
+ }
163
+ return "postgres";
164
+ }
165
+ function findMigrationDirectory(cwd, adapter) {
166
+ if (adapter === "drizzle") {
167
+ const supabasePath = join(cwd, "supabase", "migrations");
168
+ if (existsSync(supabasePath)) {
169
+ return supabasePath;
170
+ }
171
+ return join(cwd, "drizzle");
172
+ }
173
+ const timestamp = formatTimestamp(/* @__PURE__ */ new Date(), "");
174
+ return join(cwd, "prisma", "migrations", `${timestamp}_add_audit_logs`);
175
+ }
176
+
177
+ export {
178
+ formatTimestamp,
179
+ detectOrmAdapter,
180
+ readCliVersion,
181
+ printCheckResults,
182
+ CliSilentExitError,
183
+ runAction,
184
+ detectDialect,
185
+ findMigrationDirectory
186
+ };
187
+ //# sourceMappingURL=chunk-GHOKL227.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../shared/cli-utils/src/format-timestamp.ts","../../../shared/cli-utils/src/write-sql-output.ts","../../../shared/cli-utils/src/detect-orm-adapter.ts","../../../shared/cli-utils/src/read-cli-version.ts","../../../shared/cli-utils/src/print-check-results.ts","../../../shared/cli-utils/src/cli-silent-exit-error.ts","../../../shared/cli-utils/src/format-error-message.ts","../../../shared/cli-utils/src/run-action.ts","../src/detect-adapter.ts"],"sourcesContent":["/**\n * Format a date as `YYYYMMDD<sep>HHMMSS`.\n *\n * @param date - The date to format.\n * @param separator - String inserted between date and time parts. Defaults to `\"_\"`.\n * Pass `\"\"` for a compact `YYYYMMDDHHMMSS` format (e.g. for Prisma migration directories).\n */\nexport function formatTimestamp(date: Date, separator: string = \"_\"): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n const d = String(date.getDate()).padStart(2, \"0\");\n const h = String(date.getHours()).padStart(2, \"0\");\n const min = String(date.getMinutes()).padStart(2, \"0\");\n const sec = String(date.getSeconds()).padStart(2, \"0\");\n return `${y}${m}${d}${separator}${h}${min}${sec}`;\n}\n","import { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport pc from \"picocolors\";\n\n/**\n * Write SQL to a file. If `output` ends in `.sql`, treat it as a file path.\n * Otherwise treat it as a directory and generate a filename inside it.\n *\n * @returns `true` if the file was written, `false` if it already exists and `force` is not set.\n */\nexport function writeSqlOutput(\n sql: string,\n output: string,\n defaultFilename: string,\n force: boolean,\n): boolean {\n let filePath: string;\n if (output.endsWith(\".sql\")) {\n mkdirSync(dirname(output), { recursive: true });\n filePath = output;\n } else {\n mkdirSync(output, { recursive: true });\n filePath = join(output, defaultFilename);\n }\n const existed = existsSync(filePath);\n if (existed && !force) {\n console.error(pc.red(`File already exists: ${filePath}`));\n console.error(pc.dim(\"Use --force to overwrite.\"));\n return false;\n }\n if (existed) {\n console.warn(pc.yellow(`Warning: overwriting existing file ${filePath}`));\n }\n writeFileSync(filePath, sql, \"utf-8\");\n console.log(`${pc.green(\"✓\")} Wrote ${filePath}`);\n return true;\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { OrmAdapter } from \"./types.js\";\n\n/**\n * Detect the ORM adapter from `package.json` dependencies.\n * Prefers drizzle when both are present.\n * Returns `undefined` if neither is found.\n */\nexport function detectOrmAdapter(cwd: string): OrmAdapter | undefined {\n const pkgPath = join(cwd, \"package.json\");\n if (!existsSync(pkgPath)) {\n return undefined;\n }\n\n try {\n const content = readFileSync(pkgPath, \"utf-8\");\n const parsed: unknown = JSON.parse(content);\n if (typeof parsed !== \"object\" || parsed === null) {\n return undefined;\n }\n\n const allDeps: Record<string, unknown> = {};\n if (\"dependencies\" in parsed && typeof parsed.dependencies === \"object\" && parsed.dependencies !== null) {\n Object.assign(allDeps, parsed.dependencies);\n }\n if (\"devDependencies\" in parsed && typeof parsed.devDependencies === \"object\" && parsed.devDependencies !== null) {\n Object.assign(allDeps, parsed.devDependencies);\n }\n\n if (\"drizzle-orm\" in allDeps) {\n return \"drizzle\";\n }\n if (\"@prisma/client\" in allDeps) {\n return \"prisma\";\n }\n } catch {\n // Invalid JSON or read error — skip detection\n }\n\n return undefined;\n}\n","import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Read the `version` field from the `package.json` located one directory above\n * the given `import.meta.url`.\n *\n * Returns `\"0.0.0\"` if the file cannot be read or does not contain a valid version.\n */\nexport function readCliVersion(metaUrl: string): string {\n const cliDir = dirname(fileURLToPath(metaUrl));\n try {\n const raw: unknown = JSON.parse(\n readFileSync(join(cliDir, \"..\", \"package.json\"), \"utf-8\"),\n );\n if (\n typeof raw === \"object\" &&\n raw !== null &&\n \"version\" in raw &&\n typeof raw.version === \"string\"\n ) {\n return raw.version;\n }\n } catch {\n // Read or parse error — fall through to default\n }\n return \"0.0.0\";\n}\n","import pc from \"picocolors\";\nimport type { CheckOutput, PrintCheckOptions } from \"./types.js\";\n\n/**\n * Print structured check results to stdout.\n *\n * When `verbose` is true, every individual check is printed.\n * Failed checks are always shown with details and optional remediation hints.\n * A summary line is always printed at the end.\n */\nexport function printCheckResults(\n output: CheckOutput,\n options?: PrintCheckOptions,\n): void {\n const verbose = options?.verbose === true;\n const failures: typeof output.results = [];\n let passedCount = 0;\n for (const r of output.results) {\n if (r.passed) {\n passedCount++;\n } else {\n failures.push(r);\n }\n }\n const failedCount = failures.length;\n\n // Verbose: show all individual checks\n if (verbose) {\n console.log(\"\");\n for (const r of output.results) {\n const icon = r.passed ? pc.green(\"✓\") : pc.red(\"✗\");\n const detail =\n r.message !== undefined ? pc.dim(` — ${r.message}`) : \"\";\n console.log(` ${icon} ${r.check}${detail}`);\n }\n }\n\n // Warnings\n if (output.warnings.length > 0) {\n console.log(\"\");\n for (const w of output.warnings) {\n console.log(` ${pc.yellow(\"⚠\")} ${w.message ?? w.check}`);\n }\n }\n\n // Summary\n console.log(\"\");\n if (output.passed) {\n console.log(pc.green(`✓ All ${passedCount} checks passed`));\n } else {\n console.log(\n pc.red(\n `✗ ${failedCount} check${failedCount === 1 ? \"\" : \"s\"} failed`,\n ) + pc.dim(`, ${passedCount} passed`),\n );\n\n // Failure details\n console.log(\"\");\n if (verbose) {\n // In verbose mode, individual results were already printed — just show remediation\n for (const f of failures) {\n if (f.remediation !== undefined) {\n console.log(` ${pc.cyan(\"→\")} ${f.check}: ${f.remediation}`);\n }\n }\n } else {\n // Default mode: show full failure details\n for (const f of failures) {\n console.log(` ${pc.red(\"✗\")} ${f.check}`);\n if (f.message !== undefined) {\n console.log(` ${f.message}`);\n }\n if (f.remediation !== undefined) {\n console.log(` ${pc.cyan(\"→\")} ${f.remediation}`);\n }\n }\n }\n }\n}\n","/**\n * Error class for CLI commands that have already rendered their output\n * or represent an intentional exit (e.g. user cancellation).\n *\n * `runAction` catches this and calls `process.exit(exitCode)` without\n * printing an additional error message.\n */\nexport class CliSilentExitError extends Error {\n readonly exitCode: number;\n\n constructor(exitCode: number, message?: string) {\n super(message ?? `CLI exited with code ${String(exitCode)}`);\n this.name = \"CliSilentExitError\";\n this.exitCode = exitCode;\n }\n}\n","/**\n * Extract a human-readable message from an error value.\n *\n * Handles `AggregateError` (common from pg connection failures where\n * `.message` is empty but nested errors contain the real details),\n * regular `Error`, and unknown/non-Error values.\n */\nexport function formatErrorMessage(err: unknown): string {\n if (err instanceof AggregateError && err.errors.length > 0) {\n const nested = err.errors\n .map((e: unknown) => (e instanceof Error ? e.message : String(e)))\n .join(\"; \");\n return err.message !== \"\" ? `${err.message}: ${nested}` : nested;\n }\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n","import pc from \"picocolors\";\nimport { CliSilentExitError } from \"./cli-silent-exit-error.js\";\nimport { formatErrorMessage } from \"./format-error-message.js\";\n\n/**\n * Wrap a CLI subcommand action with consistent error handling.\n *\n * - `CliSilentExitError` → `process.exit(exitCode)` (output already rendered)\n * - Other errors → red error message + `process.exit(1)`\n */\nexport async function runAction(fn: () => Promise<void>): Promise<void> {\n try {\n await fn();\n } catch (err) {\n if (err instanceof CliSilentExitError) {\n process.exit(err.exitCode);\n }\n const message = formatErrorMessage(err);\n console.error(pc.red(message));\n process.exit(1);\n }\n}\n","/**\n * Auto-detection utilities for ORM adapter, database dialect, and migration directories.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { detectOrmAdapter, formatTimestamp } from \"@usebetterdev/cli-utils\";\n\nexport { detectOrmAdapter as detectAdapter };\nexport type { OrmAdapter as AdapterType } from \"@usebetterdev/cli-utils\";\n\nexport type DatabaseDialect = \"postgres\" | \"mysql\" | \"sqlite\";\n\n/**\n * Detect the database dialect from a connection URL.\n * Falls back to `\"postgres\"` if the URL is missing or unrecognizable.\n */\nexport function detectDialect(databaseUrl?: string): DatabaseDialect {\n if (databaseUrl === undefined) {\n return \"postgres\";\n }\n\n const lower = databaseUrl.toLowerCase();\n\n if (lower.startsWith(\"postgres://\") || lower.startsWith(\"postgresql://\")) {\n return \"postgres\";\n }\n if (lower.startsWith(\"mysql://\")) {\n return \"mysql\";\n }\n if (lower.startsWith(\"file:\") || lower.endsWith(\".db\") || lower.endsWith(\".sqlite\") || lower.endsWith(\".sqlite3\")) {\n return \"sqlite\";\n }\n\n return \"postgres\";\n}\n\n/**\n * Determine the migration output directory based on the detected adapter.\n *\n * - **Drizzle**: `./drizzle/` (or `./supabase/migrations/` if that directory exists)\n * - **Prisma**: `./prisma/migrations/<timestamp_add_audit_logs>/migration.sql`\n */\nexport function findMigrationDirectory(\n cwd: string,\n adapter: \"drizzle\" | \"prisma\",\n): string {\n if (adapter === \"drizzle\") {\n const supabasePath = join(cwd, \"supabase\", \"migrations\");\n if (existsSync(supabasePath)) {\n return supabasePath;\n }\n return join(cwd, \"drizzle\");\n }\n\n // prisma\n const timestamp = formatTimestamp(new Date(), \"\");\n return join(cwd, \"prisma\", \"migrations\", `${timestamp}_add_audit_logs`);\n}\n"],"mappings":"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","names":["existsSync","join","readFileSync","dirname","pc","join","existsSync","dirname","readFileSync","pc"]}
@@ -1,10 +1,14 @@
1
+ import {
2
+ ISO_DATE_REGEX,
3
+ parseIsoDate
4
+ } from "./chunk-EEYS3G5Y.js";
1
5
  import {
2
6
  createKyselyInstance,
3
7
  createSqlExecutor
4
8
  } from "./chunk-7GSN73TA.js";
5
9
  import {
6
10
  detectDialect
7
- } from "./chunk-HDO5P6X7.js";
11
+ } from "./chunk-GHOKL227.js";
8
12
 
9
13
  // src/export.ts
10
14
  import { createWriteStream } from "fs";
@@ -17,7 +21,6 @@ var VALID_SEVERITIES = /* @__PURE__ */ new Set([
17
21
  "high",
18
22
  "critical"
19
23
  ]);
20
- var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(T[\w:.+-]+)?$/;
21
24
  function parseFormat(value) {
22
25
  if (value === void 0) {
23
26
  return "csv";
@@ -29,6 +32,9 @@ function parseFormat(value) {
29
32
  }
30
33
  return value;
31
34
  }
35
+ function parseActors(value) {
36
+ return [...new Set(value.split(",").map((v) => v.trim()).filter((v) => v.length > 0))];
37
+ }
32
38
  function parseSeverity(value) {
33
39
  if (!VALID_SEVERITIES.has(value)) {
34
40
  throw new Error(
@@ -39,11 +45,7 @@ function parseSeverity(value) {
39
45
  }
40
46
  function parseSinceValue(value) {
41
47
  if (ISO_DATE_REGEX.test(value)) {
42
- const date = new Date(value);
43
- if (Number.isNaN(date.getTime())) {
44
- throw new Error(`Invalid date "${value}". Expected ISO-8601 format.`);
45
- }
46
- return date;
48
+ return parseIsoDate(value);
47
49
  }
48
50
  parseDuration(value);
49
51
  return value;
@@ -136,7 +138,8 @@ async function exportLogs(options) {
136
138
  builder = builder.compliance(...tags);
137
139
  }
138
140
  if (options.actor !== void 0) {
139
- builder = builder.actor(options.actor);
141
+ const actors = parseActors(options.actor);
142
+ builder = builder.actor(...actors);
140
143
  }
141
144
  if (options.limit !== void 0) {
142
145
  const n = Number(options.limit);
@@ -173,10 +176,11 @@ async function exportLogs(options) {
173
176
 
174
177
  export {
175
178
  parseFormat,
179
+ parseActors,
176
180
  parseSeverity,
177
181
  parseSinceValue,
178
182
  createFileWritableStream,
179
183
  createStdoutWritableStream,
180
184
  exportLogs
181
185
  };
182
- //# sourceMappingURL=chunk-M46VJ3FO.js.map
186
+ //# sourceMappingURL=chunk-O4577NVP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/export.ts"],"sourcesContent":["/**\n * `better-audit export` — Export audit log entries as CSV or JSON.\n *\n * Connects to the database via Kysely (multi-dialect), builds query filters\n * from CLI flags, and streams results to a file or stdout.\n */\n\nimport { createWriteStream } from \"node:fs\";\nimport pc from \"picocolors\";\nimport { runExport, AuditQueryBuilder, parseDuration } from \"@usebetterdev/audit-core\";\nimport type { AuditSeverity, ExportOptions } from \"@usebetterdev/audit-core\";\nimport { createKyselyInstance, createSqlExecutor } from \"./sql-executor.js\";\nimport { detectDialect } from \"./detect-adapter.js\";\nimport { ISO_DATE_REGEX, parseIsoDate } from \"./parse-since.js\";\n\nconst VALID_FORMATS = new Set([\"csv\", \"json\"]);\nconst VALID_SEVERITIES: ReadonlySet<string> = new Set([\n \"low\",\n \"medium\",\n \"high\",\n \"critical\",\n]);\n\nexport interface ExportCommandOptions {\n format?: string;\n output?: string;\n since?: string;\n severity?: string;\n compliance?: string;\n actor?: string;\n limit?: string;\n databaseUrl?: string;\n}\n\n/** Parse `--format` flag, validate and default to \"csv\". */\nexport function parseFormat(value: string | undefined): \"csv\" | \"json\" {\n if (value === undefined) {\n return \"csv\";\n }\n if (!VALID_FORMATS.has(value)) {\n throw new Error(\n `Invalid format \"${value}\". Expected \"csv\" or \"json\".`,\n );\n }\n return value as \"csv\" | \"json\";\n}\n\n/** Parse `--actor` flag, split comma-separated values and deduplicate. */\nexport function parseActors(value: string): string[] {\n return [...new Set(value.split(\",\").map((v) => v.trim()).filter((v) => v.length > 0))];\n}\n\n/** Parse `--severity` flag, validate against known values. */\nexport function parseSeverity(value: string): AuditSeverity {\n if (!VALID_SEVERITIES.has(value)) {\n throw new Error(\n `Invalid severity \"${value}\". Expected one of: low, medium, high, critical.`,\n );\n }\n return value as AuditSeverity;\n}\n\n/**\n * Parse `--since` flag. ISO date strings (e.g. \"2025-01-01\") become Date objects.\n * Duration strings (e.g. \"90d\") are returned as-is for the query builder.\n */\nexport function parseSinceValue(value: string): Date | string {\n if (ISO_DATE_REGEX.test(value)) {\n return parseIsoDate(value);\n }\n // Validate as duration — throws if invalid\n parseDuration(value);\n return value;\n}\n\n/**\n * Bridge a single `nodeStream.write()` call into a Promise that respects\n * backpressure and settles exactly once — whichever of error/drain fires first.\n */\nfunction writeToNodeStream(\n nodeStream: NodeJS.WritableStream,\n chunk: string,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n cleanup();\n fn();\n }\n };\n const onError = (err: Error) => { settle(() => reject(err)); };\n const onDrain = () => { settle(resolve); };\n const cleanup = () => {\n nodeStream.removeListener(\"error\", onError);\n nodeStream.removeListener(\"drain\", onDrain);\n };\n\n nodeStream.once(\"error\", onError);\n const canContinue = nodeStream.write(chunk);\n if (canContinue) {\n settle(resolve);\n } else {\n nodeStream.once(\"drain\", onDrain);\n }\n });\n}\n\n/**\n * Create a WHATWG WritableStream<string> that writes to a Node.js file.\n * Handles backpressure by awaiting the drain event.\n */\nexport function createFileWritableStream(\n path: string,\n): WritableStream<string> {\n const nodeStream = createWriteStream(path, { encoding: \"utf-8\" });\n\n return new WritableStream<string>({\n write(chunk) {\n return writeToNodeStream(nodeStream, chunk);\n },\n close() {\n return new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => { reject(err); };\n nodeStream.once(\"error\", onError);\n nodeStream.end(() => {\n nodeStream.removeListener(\"error\", onError);\n resolve();\n });\n });\n },\n abort() {\n nodeStream.destroy();\n },\n });\n}\n\n/** Create a WHATWG WritableStream<string> that writes to process.stdout. */\nexport function createStdoutWritableStream(): WritableStream<string> {\n return new WritableStream<string>({\n write(chunk) {\n return writeToNodeStream(process.stdout, chunk);\n },\n });\n}\n\nexport async function exportLogs(options: ExportCommandOptions): Promise<void> {\n // 1. Resolve DATABASE_URL\n const databaseUrl = options.databaseUrl ?? process.env[\"DATABASE_URL\"];\n if (databaseUrl === undefined || databaseUrl === \"\") {\n throw new Error(\n \"DATABASE_URL is required. Set the DATABASE_URL environment variable or pass --database-url.\",\n );\n }\n\n // 2. Parse flags\n const format = parseFormat(options.format);\n\n // 3. Detect dialect + create Kysely instance\n const dialect = detectDialect(databaseUrl);\n const db = await createKyselyInstance(databaseUrl, dialect);\n\n try {\n const executor = createSqlExecutor(db, dialect);\n\n // 4. Build query\n let query: AuditQueryBuilder | undefined;\n const hasFilters =\n options.since !== undefined ||\n options.severity !== undefined ||\n options.compliance !== undefined ||\n options.actor !== undefined ||\n options.limit !== undefined;\n\n if (hasFilters) {\n let builder = new AuditQueryBuilder(executor);\n\n if (options.since !== undefined) {\n const sinceValue = parseSinceValue(options.since);\n builder = builder.since(sinceValue);\n }\n\n if (options.severity !== undefined) {\n const severity = parseSeverity(options.severity);\n builder = builder.severity(severity);\n }\n\n if (options.compliance !== undefined) {\n const tags = options.compliance.split(\",\").map((t) => t.trim());\n builder = builder.compliance(...tags);\n }\n\n if (options.actor !== undefined) {\n const actors = parseActors(options.actor);\n builder = builder.actor(...actors);\n }\n\n if (options.limit !== undefined) {\n const n = Number(options.limit);\n if (Number.isNaN(n) || n <= 0 || !Number.isInteger(n)) {\n throw new Error(\n `Invalid limit \"${options.limit}\". Expected a positive integer.`,\n );\n }\n builder = builder.limit(n);\n }\n\n query = builder;\n }\n\n // 5. Create output sink\n const outputPath = options.output;\n const output: WritableStream<string> = outputPath !== undefined\n ? createFileWritableStream(outputPath)\n : createStdoutWritableStream();\n\n // 6. Run export\n const exportOptions: ExportOptions = {\n format,\n output,\n ...(query !== undefined && { query }),\n };\n\n const result = await runExport(executor, exportOptions);\n\n // 7. Summary message\n if (outputPath !== undefined) {\n console.error(\n `${pc.green(\"✓\")} Exported ${result.rowCount} rows to ${pc.dim(outputPath)}`,\n );\n } else {\n console.error(\n `${pc.green(\"✓\")} Exported ${result.rowCount} rows`,\n );\n }\n } finally {\n await db.destroy();\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,22 +1,23 @@
1
1
  import {
2
2
  INDEX_DEFINITIONS
3
- } from "./chunk-O5LHE2AC.js";
3
+ } from "./chunk-Q23AEDWF.js";
4
4
  import {
5
5
  createKyselyInstance
6
6
  } from "./chunk-7GSN73TA.js";
7
7
  import {
8
- detectDialect
9
- } from "./chunk-HDO5P6X7.js";
8
+ CliSilentExitError,
9
+ detectDialect,
10
+ printCheckResults
11
+ } from "./chunk-GHOKL227.js";
10
12
 
11
13
  // src/check.ts
12
14
  import { randomUUID } from "crypto";
13
15
  import { sql } from "kysely";
14
- import pc from "picocolors";
15
16
  import { AUDIT_LOG_SCHEMA } from "@usebetterdev/audit-core";
16
- var CheckFailedError = class extends Error {
17
+ var CheckFailedError = class extends CliSilentExitError {
17
18
  output;
18
19
  constructor(output) {
19
- super("One or more audit checks failed");
20
+ super(1, "One or more audit checks failed");
20
21
  this.name = "CheckFailedError";
21
22
  this.output = output;
22
23
  }
@@ -30,21 +31,24 @@ var DIALECT_TYPE_MAP = {
30
31
  timestamptz: ["timestamptz"],
31
32
  text: ["text"],
32
33
  jsonb: ["jsonb"],
33
- boolean: ["bool"]
34
+ boolean: ["bool"],
35
+ integer: ["integer"]
34
36
  },
35
37
  mysql: {
36
38
  uuid: ["char"],
37
39
  timestamptz: ["datetime"],
38
40
  text: ["text", "mediumtext", "longtext"],
39
41
  jsonb: ["json"],
40
- boolean: ["tinyint"]
42
+ boolean: ["tinyint"],
43
+ integer: ["bigint", "int"]
41
44
  },
42
45
  sqlite: {
43
46
  uuid: ["TEXT"],
44
47
  timestamptz: ["TEXT"],
45
48
  text: ["TEXT"],
46
49
  jsonb: ["TEXT"],
47
- boolean: ["INTEGER"]
50
+ boolean: ["INTEGER"],
51
+ integer: ["INTEGER"]
48
52
  }
49
53
  };
50
54
  function normalizeReportedType(raw, dialect) {
@@ -344,58 +348,11 @@ async function check(options = {}) {
344
348
  }
345
349
  const output = await runCheck(databaseUrl);
346
350
  const verbose = options.verbose === true;
347
- printCheckOutput(output, verbose);
351
+ printCheckResults(output, { verbose });
348
352
  if (!output.passed) {
349
353
  throw new CheckFailedError(output);
350
354
  }
351
355
  }
352
- function printCheckOutput(output, verbose) {
353
- const passedCount = output.results.filter((r) => r.passed).length;
354
- const failedCount = output.results.length - passedCount;
355
- const failures = output.results.filter((r) => !r.passed);
356
- if (verbose) {
357
- console.log("");
358
- for (const r of output.results) {
359
- const icon = r.passed ? pc.green("\u2713") : pc.red("\u2717");
360
- const detail = r.message !== void 0 ? pc.dim(` \u2014 ${r.message}`) : "";
361
- console.log(` ${icon} ${r.check}${detail}`);
362
- }
363
- }
364
- if (output.warnings.length > 0) {
365
- console.log("");
366
- for (const w of output.warnings) {
367
- console.log(` ${pc.yellow("\u26A0")} ${w.message ?? w.check}`);
368
- }
369
- }
370
- console.log("");
371
- if (output.passed) {
372
- console.log(pc.green(`\u2713 All ${passedCount} checks passed`));
373
- } else {
374
- console.log(
375
- pc.red(
376
- `\u2717 ${failedCount} check${failedCount === 1 ? "" : "s"} failed`
377
- ) + pc.dim(`, ${passedCount} passed`)
378
- );
379
- console.log("");
380
- if (verbose) {
381
- for (const f of failures) {
382
- if (f.remediation !== void 0) {
383
- console.log(` ${pc.cyan("\u2192")} ${f.check}: ${f.remediation}`);
384
- }
385
- }
386
- } else {
387
- for (const f of failures) {
388
- console.log(` ${pc.red("\u2717")} ${f.check}`);
389
- if (f.message !== void 0) {
390
- console.log(` ${f.message}`);
391
- }
392
- if (f.remediation !== void 0) {
393
- console.log(` ${pc.cyan("\u2192")} ${f.remediation}`);
394
- }
395
- }
396
- }
397
- }
398
- }
399
356
 
400
357
  export {
401
358
  CheckFailedError,
@@ -405,4 +362,4 @@ export {
405
362
  runCheck,
406
363
  check
407
364
  };
408
- //# sourceMappingURL=chunk-55KKYFKR.js.map
365
+ //# sourceMappingURL=chunk-PGOZIYS5.js.map