pinata-security-cli 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { z } from 'zod';
3
- import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
2
+ import fs, { readFile, mkdir, writeFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
4
3
  import path, { dirname, resolve, join, basename, relative, extname } from 'path';
5
- import { existsSync, writeFileSync, readFileSync, chmodSync, mkdirSync } from 'fs';
4
+ import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
6
5
  import { homedir, tmpdir } from 'os';
6
+ import { z } from 'zod';
7
7
  import { spawn } from 'child_process';
8
8
  import { useState } from 'react';
9
9
  import { render, useApp, useInput, Box, Text } from 'ink';
10
10
  import Spinner from 'ink-spinner';
11
11
  import { jsx, jsxs } from 'react/jsx-runtime';
12
- import { fileURLToPath } from 'url';
13
- import chalk5 from 'chalk';
12
+ import chalk7 from 'chalk';
14
13
  import { Command } from 'commander';
15
- import ora from 'ora';
14
+ import ora3 from 'ora';
16
15
  import YAML from 'yaml';
16
+ import { fileURLToPath } from 'url';
17
17
  import { Query, Parser, Language } from 'web-tree-sitter';
18
18
  import { minimatch } from 'minimatch';
19
19
 
@@ -125,35 +125,6 @@ var init_result = __esm({
125
125
  "src/lib/result.ts"() {
126
126
  }
127
127
  });
128
- var SEVERITY_WEIGHTS, CONFIDENCE_WEIGHTS, PRIORITY_WEIGHTS, DEFAULT_TEST_PATTERNS;
129
- var init_types = __esm({
130
- "src/core/scanner/types.ts"() {
131
- SEVERITY_WEIGHTS = {
132
- critical: 4,
133
- high: 3,
134
- medium: 2,
135
- low: 1
136
- };
137
- CONFIDENCE_WEIGHTS = {
138
- high: 1,
139
- medium: 0.7,
140
- low: 0.4
141
- };
142
- PRIORITY_WEIGHTS = {
143
- P0: 3,
144
- P1: 2,
145
- P2: 1
146
- };
147
- DEFAULT_TEST_PATTERNS = {
148
- python: ["test_*.py", "*_test.py", "tests/**/*.py", "test/**/*.py"],
149
- typescript: ["*.test.ts", "*.spec.ts", "__tests__/**/*.ts", "tests/**/*.ts"],
150
- javascript: ["*.test.js", "*.spec.js", "__tests__/**/*.js", "tests/**/*.js"],
151
- go: ["*_test.go"],
152
- java: ["*Test.java", "*Tests.java", "src/test/**/*.java"],
153
- rust: ["tests/**/*.rs"]
154
- };
155
- }
156
- });
157
128
  function getCachePath(projectRoot) {
158
129
  return resolve(projectRoot, CACHE_DIR, CACHE_FILE);
159
130
  }
@@ -850,7 +821,6 @@ var init_config = __esm({
850
821
  var SKIP_PATTERNS, BATCH_PROMPT, SINGLE_ITEM_TEMPLATE, AIVerifier;
851
822
  var init_ai_verifier = __esm({
852
823
  "src/core/verifier/ai-verifier.ts"() {
853
- init_types();
854
824
  SKIP_PATTERNS = {
855
825
  paths: [
856
826
  /\.test\.(ts|js|tsx|jsx)$/,
@@ -1255,7 +1225,7 @@ function isTestable(categoryId) {
1255
1225
  return TESTABLE_VULNERABILITIES.includes(categoryId);
1256
1226
  }
1257
1227
  var DEFAULT_SANDBOX_CONFIG, TESTABLE_VULNERABILITIES;
1258
- var init_types2 = __esm({
1228
+ var init_types = __esm({
1259
1229
  "src/execution/types.ts"() {
1260
1230
  DEFAULT_SANDBOX_CONFIG = {
1261
1231
  image: "pinata-sandbox:latest",
@@ -1285,7 +1255,7 @@ function createSandbox(config2) {
1285
1255
  var Sandbox;
1286
1256
  var init_sandbox = __esm({
1287
1257
  "src/execution/sandbox.ts"() {
1288
- init_types2();
1258
+ init_types();
1289
1259
  Sandbox = class {
1290
1260
  config;
1291
1261
  tempDir = null;
@@ -1488,7 +1458,7 @@ export default defineConfig({
1488
1458
  * Execute a command and capture output
1489
1459
  */
1490
1460
  exec(command, args, options = {}) {
1491
- return new Promise((resolve7) => {
1461
+ return new Promise((resolve9) => {
1492
1462
  let stdout = "";
1493
1463
  let stderr = "";
1494
1464
  let timedOut = false;
@@ -1508,7 +1478,7 @@ export default defineConfig({
1508
1478
  }, timeout);
1509
1479
  proc.on("close", (code) => {
1510
1480
  clearTimeout(timer);
1511
- resolve7({
1481
+ resolve9({
1512
1482
  stdout,
1513
1483
  stderr,
1514
1484
  exitCode: code ?? 1,
@@ -1517,7 +1487,7 @@ export default defineConfig({
1517
1487
  });
1518
1488
  proc.on("error", (err3) => {
1519
1489
  clearTimeout(timer);
1520
- resolve7({
1490
+ resolve9({
1521
1491
  stdout,
1522
1492
  stderr: stderr + "\n" + err3.message,
1523
1493
  exitCode: 1,
@@ -2776,7 +2746,7 @@ var init_runner = __esm({
2776
2746
  init_sandbox();
2777
2747
  init_results();
2778
2748
  init_generator();
2779
- init_types2();
2749
+ init_types();
2780
2750
  ExecutionRunner = class {
2781
2751
  sandbox;
2782
2752
  dryRun;
@@ -2983,53 +2953,35 @@ ${code}
2983
2953
  Generate 3-5 targeted payloads that would exploit THIS SPECIFIC code.
2984
2954
  Consider the exact variable names, function calls, and data flow shown above.`;
2985
2955
  }
2956
+ function matchFirst(code, patterns) {
2957
+ for (const [keywords, name] of patterns) {
2958
+ if (keywords.some((k) => code.includes(k))) {
2959
+ return name;
2960
+ }
2961
+ }
2962
+ return void 0;
2963
+ }
2986
2964
  function extractTechStack(code, filePath) {
2965
+ const ext = "." + (filePath.split(".").pop() ?? "");
2987
2966
  const hints = {
2988
- language: detectLanguage2(filePath)
2967
+ language: LANGUAGE_EXTENSIONS[ext] ?? "unknown",
2968
+ hasEscaping: ESCAPING_KEYWORDS.some((k) => code.includes(k)),
2969
+ hasWaf: WAF_KEYWORDS.some((k) => code.includes(k))
2989
2970
  };
2990
- if (code.includes("express") || code.includes("app.get") || code.includes("app.post")) {
2991
- hints.framework = "express";
2992
- } else if (code.includes("fastify")) {
2993
- hints.framework = "fastify";
2994
- } else if (code.includes("django") || code.includes("from django")) {
2995
- hints.framework = "django";
2996
- } else if (code.includes("flask") || code.includes("from flask")) {
2997
- hints.framework = "flask";
2998
- } else if (code.includes("gin.") || code.includes("fiber.")) {
2999
- hints.framework = code.includes("gin.") ? "gin" : "fiber";
3000
- }
3001
- if (code.includes("postgres") || code.includes("pg.") || code.includes("$1")) {
3002
- hints.database = "postgres";
3003
- } else if (code.includes("mysql") || code.includes("?") && code.includes("query")) {
3004
- hints.database = "mysql";
3005
- } else if (code.includes("mongodb") || code.includes("mongoose") || code.includes("$where")) {
3006
- hints.database = "mongodb";
3007
- } else if (code.includes("sqlite") || code.includes("sqlite3")) {
3008
- hints.database = "sqlite";
3009
- }
3010
- if (code.includes("prisma")) {
3011
- hints.orm = "prisma";
3012
- } else if (code.includes("sequelize")) {
3013
- hints.orm = "sequelize";
3014
- } else if (code.includes("typeorm") || code.includes("TypeORM")) {
3015
- hints.orm = "typeorm";
3016
- } else if (code.includes("sqlalchemy") || code.includes("SQLAlchemy")) {
3017
- hints.orm = "sqlalchemy";
3018
- }
3019
- hints.hasEscaping = code.includes("escape") || code.includes("sanitize") || code.includes("DOMPurify") || code.includes("htmlspecialchars");
3020
- hints.hasWaf = code.includes("waf") || code.includes("WAF") || code.includes("cloudflare") || code.includes("akamai");
2971
+ const framework = matchFirst(code, FRAMEWORK_PATTERNS);
2972
+ if (framework) {
2973
+ hints.framework = framework;
2974
+ }
2975
+ const database = matchFirst(code, DATABASE_PATTERNS);
2976
+ if (database) {
2977
+ hints.database = database;
2978
+ }
2979
+ const orm = matchFirst(code, ORM_PATTERNS);
2980
+ if (orm) {
2981
+ hints.orm = orm;
2982
+ }
3021
2983
  return hints;
3022
2984
  }
3023
- function detectLanguage2(filePath) {
3024
- if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
3025
- if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
3026
- if (filePath.endsWith(".py")) return "python";
3027
- if (filePath.endsWith(".go")) return "go";
3028
- if (filePath.endsWith(".java")) return "java";
3029
- if (filePath.endsWith(".rb")) return "ruby";
3030
- if (filePath.endsWith(".php")) return "php";
3031
- return "unknown";
3032
- }
3033
2985
  function parseAiPayloadResponse(response) {
3034
2986
  try {
3035
2987
  const jsonMatch = response.match(/\{[\s\S]*"payloads"[\s\S]*\}/);
@@ -3080,7 +3032,7 @@ function getFallbackPayloads(context, maxPayloads = 10) {
3080
3032
  }
3081
3033
  return [...new Set(payloads)].slice(0, maxPayloads);
3082
3034
  }
3083
- var AI_PAYLOAD_SYSTEM_PROMPT;
3035
+ var AI_PAYLOAD_SYSTEM_PROMPT, FRAMEWORK_PATTERNS, DATABASE_PATTERNS, ORM_PATTERNS, ESCAPING_KEYWORDS, WAF_KEYWORDS, LANGUAGE_EXTENSIONS;
3084
3036
  var init_ai_payloads = __esm({
3085
3037
  "src/execution/ai-payloads.ts"() {
3086
3038
  init_payloads();
@@ -3119,6 +3071,39 @@ Return payloads in JSON format:
3119
3071
  }
3120
3072
  ]
3121
3073
  }`;
3074
+ FRAMEWORK_PATTERNS = [
3075
+ [["express", "app.get", "app.post"], "express"],
3076
+ [["fastify"], "fastify"],
3077
+ [["django", "from django"], "django"],
3078
+ [["flask", "from flask"], "flask"],
3079
+ [["gin."], "gin"],
3080
+ [["fiber."], "fiber"]
3081
+ ];
3082
+ DATABASE_PATTERNS = [
3083
+ [["postgres", "pg."], "postgres"],
3084
+ [["mysql"], "mysql"],
3085
+ [["mongodb", "mongoose", "$where"], "mongodb"],
3086
+ [["sqlite", "sqlite3"], "sqlite"]
3087
+ ];
3088
+ ORM_PATTERNS = [
3089
+ [["prisma"], "prisma"],
3090
+ [["sequelize"], "sequelize"],
3091
+ [["typeorm", "TypeORM"], "typeorm"],
3092
+ [["sqlalchemy", "SQLAlchemy"], "sqlalchemy"]
3093
+ ];
3094
+ ESCAPING_KEYWORDS = ["escape", "sanitize", "DOMPurify", "htmlspecialchars"];
3095
+ WAF_KEYWORDS = ["waf", "WAF", "cloudflare", "akamai"];
3096
+ LANGUAGE_EXTENSIONS = {
3097
+ ".ts": "typescript",
3098
+ ".tsx": "typescript",
3099
+ ".js": "javascript",
3100
+ ".jsx": "javascript",
3101
+ ".py": "python",
3102
+ ".go": "go",
3103
+ ".java": "java",
3104
+ ".rb": "ruby",
3105
+ ".php": "php"
3106
+ };
3122
3107
  }
3123
3108
  });
3124
3109
 
@@ -3441,7 +3426,7 @@ __export(execution_exports, {
3441
3426
  });
3442
3427
  var init_execution = __esm({
3443
3428
  "src/execution/index.ts"() {
3444
- init_types2();
3429
+ init_types();
3445
3430
  init_sandbox();
3446
3431
  init_runner();
3447
3432
  init_results();
@@ -3792,7 +3777,7 @@ function suggestConfidence(precision) {
3792
3777
  return "low";
3793
3778
  }
3794
3779
  var EMPTY_FEEDBACK_STATE, CONFIDENCE_THRESHOLDS;
3795
- var init_types3 = __esm({
3780
+ var init_types2 = __esm({
3796
3781
  "src/feedback/types.ts"() {
3797
3782
  EMPTY_FEEDBACK_STATE = {
3798
3783
  version: 1,
@@ -3926,7 +3911,7 @@ function generateReport(state) {
3926
3911
  var FEEDBACK_DIR, FEEDBACK_FILE;
3927
3912
  var init_store = __esm({
3928
3913
  "src/feedback/store.ts"() {
3929
- init_types3();
3914
+ init_types2();
3930
3915
  FEEDBACK_DIR = join(homedir(), ".pinata");
3931
3916
  FEEDBACK_FILE = join(FEEDBACK_DIR, "feedback.json");
3932
3917
  }
@@ -3948,7 +3933,7 @@ __export(feedback_exports, {
3948
3933
  });
3949
3934
  var init_feedback = __esm({
3950
3935
  "src/feedback/index.ts"() {
3951
- init_types3();
3936
+ init_types2();
3952
3937
  init_store();
3953
3938
  }
3954
3939
  });
@@ -4598,7 +4583,7 @@ var Logger = class _Logger {
4598
4583
  */
4599
4584
  debug(message, ...args) {
4600
4585
  if (this.shouldLog("debug")) {
4601
- console.debug(chalk5.gray(this.format(message)), ...args);
4586
+ console.debug(chalk7.gray(this.format(message)), ...args);
4602
4587
  }
4603
4588
  }
4604
4589
  /**
@@ -4614,7 +4599,7 @@ var Logger = class _Logger {
4614
4599
  */
4615
4600
  warn(message, ...args) {
4616
4601
  if (this.shouldLog("warn")) {
4617
- console.warn(chalk5.yellow(this.format(message)), ...args);
4602
+ console.warn(chalk7.yellow(this.format(message)), ...args);
4618
4603
  }
4619
4604
  }
4620
4605
  /**
@@ -4622,7 +4607,7 @@ var Logger = class _Logger {
4622
4607
  */
4623
4608
  error(message, ...args) {
4624
4609
  if (this.shouldLog("error")) {
4625
- console.error(chalk5.red(this.format(message)), ...args);
4610
+ console.error(chalk7.red(this.format(message)), ...args);
4626
4611
  }
4627
4612
  }
4628
4613
  /**
@@ -4630,7 +4615,7 @@ var Logger = class _Logger {
4630
4615
  */
4631
4616
  success(message, ...args) {
4632
4617
  if (this.shouldLog("info")) {
4633
- console.info(chalk5.green(this.format(message)), ...args);
4618
+ console.info(chalk7.green(this.format(message)), ...args);
4634
4619
  }
4635
4620
  }
4636
4621
  /**
@@ -5705,7 +5690,32 @@ function getProjectTypeDescription(type) {
5705
5690
  }
5706
5691
  init_errors();
5707
5692
  init_result();
5708
- init_types();
5693
+ var SEVERITY_WEIGHTS = {
5694
+ critical: 4,
5695
+ high: 3,
5696
+ medium: 2,
5697
+ low: 1
5698
+ };
5699
+ var CONFIDENCE_WEIGHTS = {
5700
+ high: 1,
5701
+ medium: 0.7,
5702
+ low: 0.4
5703
+ };
5704
+ var PRIORITY_WEIGHTS = {
5705
+ P0: 3,
5706
+ P1: 2,
5707
+ P2: 1
5708
+ };
5709
+ var DEFAULT_TEST_PATTERNS = {
5710
+ python: ["test_*.py", "*_test.py", "tests/**/*.py", "test/**/*.py"],
5711
+ typescript: ["*.test.ts", "*.spec.ts", "__tests__/**/*.ts", "tests/**/*.ts"],
5712
+ javascript: ["*.test.js", "*.spec.js", "__tests__/**/*.js", "tests/**/*.js"],
5713
+ go: ["*_test.go"],
5714
+ java: ["*Test.java", "*Tests.java", "src/test/**/*.java"],
5715
+ rust: ["tests/**/*.rs"]
5716
+ };
5717
+
5718
+ // src/core/scanner/scanner.ts
5709
5719
  var DEFAULT_OPTIONS = {
5710
5720
  excludeDirs: [
5711
5721
  // Package managers
@@ -6335,9 +6345,6 @@ function createScanner(categoryStore) {
6335
6345
  return new Scanner(categoryStore);
6336
6346
  }
6337
6347
 
6338
- // src/core/scanner/index.ts
6339
- init_types();
6340
-
6341
6348
  // src/core/index.ts
6342
6349
  var VERSION = "0.4.0";
6343
6350
 
@@ -7071,34 +7078,34 @@ function createRenderer(options) {
7071
7078
  return new TemplateRenderer(options);
7072
7079
  }
7073
7080
  var SEVERITY_COLORS = {
7074
- critical: chalk5.red.bold,
7075
- high: chalk5.red,
7076
- medium: chalk5.yellow,
7077
- low: chalk5.gray
7081
+ critical: chalk7.red.bold,
7082
+ high: chalk7.red,
7083
+ medium: chalk7.yellow,
7084
+ low: chalk7.gray
7078
7085
  };
7079
7086
  var PRIORITY_COLORS = {
7080
- P0: chalk5.red.bold,
7081
- P1: chalk5.yellow,
7082
- P2: chalk5.gray
7087
+ P0: chalk7.red.bold,
7088
+ P1: chalk7.yellow,
7089
+ P2: chalk7.gray
7083
7090
  };
7084
7091
  var DOMAIN_COLORS = {
7085
- security: chalk5.red,
7086
- data: chalk5.blue,
7087
- concurrency: chalk5.magenta,
7088
- input: chalk5.cyan,
7089
- resource: chalk5.yellow,
7090
- reliability: chalk5.green,
7091
- performance: chalk5.yellowBright,
7092
- platform: chalk5.gray,
7093
- business: chalk5.white,
7094
- compliance: chalk5.blueBright
7092
+ security: chalk7.red,
7093
+ data: chalk7.blue,
7094
+ concurrency: chalk7.magenta,
7095
+ input: chalk7.cyan,
7096
+ resource: chalk7.yellow,
7097
+ reliability: chalk7.green,
7098
+ performance: chalk7.yellowBright,
7099
+ platform: chalk7.gray,
7100
+ business: chalk7.white,
7101
+ compliance: chalk7.blueBright
7095
7102
  };
7096
7103
  function formatTerminal(categories) {
7097
7104
  if (categories.length === 0) {
7098
- return chalk5.yellow("No categories found matching the filters.");
7105
+ return chalk7.yellow("No categories found matching the filters.");
7099
7106
  }
7100
7107
  const lines = [];
7101
- lines.push(chalk5.bold.underline(`Found ${categories.length} categories:
7108
+ lines.push(chalk7.bold.underline(`Found ${categories.length} categories:
7102
7109
  `));
7103
7110
  const byDomain = /* @__PURE__ */ new Map();
7104
7111
  for (const cat of categories) {
@@ -7109,26 +7116,26 @@ function formatTerminal(categories) {
7109
7116
  byDomain.get(domain).push(cat);
7110
7117
  }
7111
7118
  for (const [domain, domainCategories] of byDomain) {
7112
- const domainColor = DOMAIN_COLORS[domain] ?? chalk5.white;
7119
+ const domainColor = DOMAIN_COLORS[domain] ?? chalk7.white;
7113
7120
  lines.push(domainColor.bold(`
7114
7121
  ${domain.toUpperCase()} (${domainCategories.length})`));
7115
- lines.push(chalk5.gray("\u2500".repeat(40)));
7122
+ lines.push(chalk7.gray("\u2500".repeat(40)));
7116
7123
  for (const cat of domainCategories) {
7117
7124
  const priorityColor = PRIORITY_COLORS[cat.priority];
7118
7125
  const severityColor = SEVERITY_COLORS[cat.severity];
7119
7126
  const priority = priorityColor(`[${cat.priority}]`);
7120
7127
  const severity = severityColor(`${cat.severity}`);
7121
- const level = chalk5.cyan(`${cat.level}`);
7122
- const name = chalk5.white.bold(cat.name);
7123
- const id = chalk5.gray(`(${cat.id})`);
7128
+ const level = chalk7.cyan(`${cat.level}`);
7129
+ const name = chalk7.white.bold(cat.name);
7130
+ const id = chalk7.gray(`(${cat.id})`);
7124
7131
  lines.push(` ${priority} ${name} ${id}`);
7125
7132
  lines.push(` ${severity} | ${level}`);
7126
7133
  const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
7127
- lines.push(chalk5.gray(` ${desc}`));
7134
+ lines.push(chalk7.gray(` ${desc}`));
7128
7135
  lines.push("");
7129
7136
  }
7130
7137
  }
7131
- lines.push(chalk5.gray("\u2500".repeat(40)));
7138
+ lines.push(chalk7.gray("\u2500".repeat(40)));
7132
7139
  lines.push(formatStats(categories));
7133
7140
  return lines.join("\n");
7134
7141
  }
@@ -7150,7 +7157,7 @@ function formatStats(categories) {
7150
7157
  if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
7151
7158
  if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
7152
7159
  if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
7153
- parts.push(chalk5.gray("|"));
7160
+ parts.push(chalk7.gray("|"));
7154
7161
  if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
7155
7162
  if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
7156
7163
  if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
@@ -7207,13 +7214,9 @@ function isValidOutputFormat(format) {
7207
7214
  return ["terminal", "json", "markdown"].includes(format);
7208
7215
  }
7209
7216
  function formatError(error) {
7210
- return chalk5.red(`Error: ${error.message}`);
7217
+ return chalk7.red(`Error: ${error.message}`);
7211
7218
  }
7212
7219
 
7213
- // src/cli/generate-formatters.ts
7214
- init_errors();
7215
- init_result();
7216
-
7217
7220
  // src/ai/service.ts
7218
7221
  var DEFAULT_CONFIG = {
7219
7222
  provider: "anthropic",
@@ -7260,11 +7263,11 @@ var AIService = class {
7260
7263
  */
7261
7264
  getApiKeyFromConfig(provider) {
7262
7265
  try {
7263
- const { existsSync: existsSync5, readFileSync: readFileSync3 } = __require("fs");
7266
+ const { existsSync: existsSync6, readFileSync: readFileSync3 } = __require("fs");
7264
7267
  const { homedir: homedir3 } = __require("os");
7265
7268
  const { join: join5 } = __require("path");
7266
7269
  const configPath = join5(homedir3(), ".pinata", "config.json");
7267
- if (!existsSync5(configPath)) {
7270
+ if (!existsSync6(configPath)) {
7268
7271
  return "";
7269
7272
  }
7270
7273
  const content = readFileSync3(configPath, "utf-8");
@@ -7515,8 +7518,119 @@ function createAIService(config2) {
7515
7518
  return new AIService(config2);
7516
7519
  }
7517
7520
 
7521
+ // src/ai/explainer.ts
7522
+ var SYSTEM_PROMPT = `You are a security expert explaining code vulnerabilities to developers.
7523
+ Your explanations should be:
7524
+ - Clear and actionable
7525
+ - Focused on the specific code pattern
7526
+ - Include concrete remediation steps
7527
+ - Reference relevant security standards (OWASP, CWE) when applicable
7528
+
7529
+ Always respond with valid JSON matching this structure:
7530
+ {
7531
+ "summary": "1-2 sentence summary",
7532
+ "explanation": "Detailed explanation of the vulnerability",
7533
+ "risk": "What an attacker could do if this is exploited",
7534
+ "remediation": "Step-by-step instructions to fix",
7535
+ "safeExample": "Code example showing the safe pattern",
7536
+ "references": ["optional array of CVE/CWE/OWASP references"]
7537
+ }`;
7538
+ async function explainGap(gap, category, config2) {
7539
+ const ai = createAIService(config2);
7540
+ if (!ai.isConfigured()) {
7541
+ return {
7542
+ success: false,
7543
+ error: "AI service not configured",
7544
+ durationMs: 0
7545
+ };
7546
+ }
7547
+ const prompt = buildExplainPrompt(gap);
7548
+ const response = await ai.completeJSON({
7549
+ systemPrompt: SYSTEM_PROMPT,
7550
+ messages: [{ role: "user", content: prompt }],
7551
+ maxTokens: 1024,
7552
+ temperature: 0.3
7553
+ });
7554
+ return response;
7555
+ }
7556
+ async function explainGaps(gaps, categories, config2) {
7557
+ const results = /* @__PURE__ */ new Map();
7558
+ const BATCH_SIZE = 5;
7559
+ for (let i = 0; i < gaps.length; i += BATCH_SIZE) {
7560
+ const batch = gaps.slice(i, i + BATCH_SIZE);
7561
+ const promises = batch.map(async (gap) => {
7562
+ const category = categories?.get(gap.categoryId);
7563
+ const result = await explainGap(gap, category, config2);
7564
+ return { key: `${gap.filePath}:${gap.lineStart}:${gap.categoryId}`, result };
7565
+ });
7566
+ const batchResults = await Promise.all(promises);
7567
+ for (const { key, result } of batchResults) {
7568
+ results.set(key, result);
7569
+ }
7570
+ }
7571
+ return results;
7572
+ }
7573
+ function buildExplainPrompt(gap, category) {
7574
+ const parts = [];
7575
+ parts.push(`Explain this security finding:
7576
+ `);
7577
+ parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
7578
+ parts.push(`**Severity:** ${gap.severity}`);
7579
+ parts.push(`**Confidence:** ${gap.confidence}`);
7580
+ parts.push(`**File:** ${gap.filePath}`);
7581
+ parts.push(`**Line:** ${gap.lineStart}`);
7582
+ if (gap.codeSnippet) {
7583
+ parts.push(`
7584
+ **Code:**
7585
+ \`\`\`
7586
+ ${gap.codeSnippet}
7587
+ \`\`\``);
7588
+ }
7589
+ parts.push(`
7590
+ **Pattern:** ${gap.patternId}`);
7591
+ parts.push(`**Detection Type:** ${gap.patternType}`);
7592
+ parts.push(`
7593
+ Provide a clear, actionable explanation for a developer.`);
7594
+ return parts.join("\n");
7595
+ }
7596
+ function generateFallbackExplanation(gap) {
7597
+ const summaries = {
7598
+ "sql-injection": "SQL query constructed with user input may allow injection attacks.",
7599
+ "xss": "User input rendered without escaping may allow script injection.",
7600
+ "command-injection": "Shell command constructed with user input may allow command execution.",
7601
+ "path-traversal": "File path constructed with user input may allow directory traversal.",
7602
+ "hardcoded-secrets": "Sensitive credentials found in source code.",
7603
+ "deserialization": "Untrusted data deserialization may allow code execution.",
7604
+ "ssrf": "Server-side request with user-controlled URL may allow internal access.",
7605
+ "xxe": "XML parser may be vulnerable to external entity injection.",
7606
+ "csrf": "State-changing request lacks CSRF protection.",
7607
+ "ldap-injection": "LDAP query constructed with user input may allow injection."
7608
+ };
7609
+ const remediations = {
7610
+ "sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
7611
+ "xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
7612
+ "command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
7613
+ "path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
7614
+ "hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
7615
+ "deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
7616
+ "ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
7617
+ "xxe": "Disable external entity processing in XML parser configuration.",
7618
+ "csrf": "Implement CSRF tokens for all state-changing requests.",
7619
+ "ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
7620
+ };
7621
+ const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
7622
+ const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
7623
+ return {
7624
+ summary,
7625
+ explanation: `The pattern "${gap.patternId}" detected a potential ${gap.categoryName} vulnerability at line ${gap.lineStart}. This type of issue has ${gap.severity} severity and was detected with ${gap.confidence} confidence.`,
7626
+ risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
7627
+ remediation,
7628
+ references: []
7629
+ };
7630
+ }
7631
+
7518
7632
  // src/ai/template-filler.ts
7519
- var SYSTEM_PROMPT = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
7633
+ var SYSTEM_PROMPT2 = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
7520
7634
  Given a code snippet and a list of template variables, suggest appropriate values for each variable.
7521
7635
 
7522
7636
  For each variable, analyze:
@@ -7551,7 +7665,7 @@ async function suggestVariables(request, config2) {
7551
7665
  const prompt = buildVariablePrompt(request);
7552
7666
  const startTime = Date.now();
7553
7667
  const response = await ai.completeJSON({
7554
- systemPrompt: SYSTEM_PROMPT,
7668
+ systemPrompt: SYSTEM_PROMPT2,
7555
7669
  messages: [{ role: "user", content: prompt }],
7556
7670
  maxTokens: 1024,
7557
7671
  temperature: 0.2
@@ -7742,438 +7856,72 @@ function toPascalCase(str) {
7742
7856
  return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
7743
7857
  }
7744
7858
 
7745
- // src/cli/generate-formatters.ts
7746
- function formatGeneratedTerminal(tests, basePath) {
7747
- const lines = [];
7748
- if (tests.length === 0) {
7749
- lines.push(chalk5.yellow("No tests generated."));
7750
- return lines.join("\n");
7751
- }
7752
- lines.push(chalk5.bold.cyan(`
7753
- Generated ${tests.length} test(s):
7754
- `));
7755
- lines.push(chalk5.gray("\u2500".repeat(60)));
7756
- for (const test of tests) {
7757
- const relGapPath = relative(basePath, test.gap.filePath);
7758
- lines.push("");
7759
- lines.push(chalk5.bold.white(`Test for: ${test.gap.categoryName}`));
7760
- lines.push(chalk5.gray(` Gap location: ${relGapPath}:${test.gap.lineStart}`));
7761
- lines.push(chalk5.gray(` Template: ${test.template.id}`));
7762
- lines.push(chalk5.gray(` Output: ${test.suggestedPath}`));
7763
- lines.push("");
7764
- lines.push(chalk5.cyan(`// --- ${test.suggestedPath} ---`));
7765
- lines.push("");
7766
- if (test.result.imports.length > 0) {
7767
- for (const imp of test.result.imports) {
7768
- lines.push(chalk5.gray(imp));
7769
- }
7770
- lines.push("");
7859
+ // src/ai/pattern-suggester.ts
7860
+ var SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
7861
+ Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
7862
+
7863
+ Your patterns should:
7864
+ 1. Be specific enough to avoid false positives
7865
+ 2. Be general enough to catch variations
7866
+ 3. Use standard regex syntax (no lookbehind for compatibility)
7867
+ 4. Include examples of what matches and what doesn't
7868
+
7869
+ Always respond with valid JSON matching this structure:
7870
+ {
7871
+ "suggestions": [
7872
+ {
7873
+ "id": "pattern-id-kebab-case",
7874
+ "pattern": "regex pattern here",
7875
+ "description": "What this pattern detects",
7876
+ "confidence": "high|medium|low",
7877
+ "matchExample": "code that should match",
7878
+ "safeExample": "similar code that should NOT match",
7879
+ "reasoning": "Why this pattern works"
7771
7880
  }
7772
- lines.push(test.result.content);
7773
- lines.push("");
7774
- lines.push(chalk5.gray("\u2500".repeat(60)));
7881
+ ]
7882
+ }
7883
+
7884
+ Important:
7885
+ - Escape backslashes properly for JSON (use \\\\s not \\s)
7886
+ - Test your patterns mentally against the examples
7887
+ - Prefer simpler patterns that are less likely to cause ReDoS`;
7888
+ async function suggestPatterns(request, config2) {
7889
+ const ai = createAIService(config2);
7890
+ if (!ai.isConfigured()) {
7891
+ return {
7892
+ success: false,
7893
+ error: "AI service not configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.",
7894
+ durationMs: 0
7895
+ };
7775
7896
  }
7776
- lines.push("");
7777
- lines.push(chalk5.bold("Summary:"));
7778
- lines.push(` Tests generated: ${chalk5.green(tests.length.toString())}`);
7779
- const categories = new Set(tests.map((t) => t.category.id));
7780
- lines.push(` Categories covered: ${chalk5.cyan(categories.size.toString())}`);
7781
- if (tests.some((t) => t.result.unresolved.length > 0)) {
7782
- const unresolvedCount = tests.reduce((acc, t) => acc + t.result.unresolved.length, 0);
7783
- lines.push(chalk5.yellow(` Unresolved placeholders: ${unresolvedCount}`));
7784
- lines.push(chalk5.gray(" (Some placeholders need manual completion)"));
7897
+ const prompt = buildPatternPrompt(request);
7898
+ const response = await ai.completeJSON({
7899
+ systemPrompt: SYSTEM_PROMPT3,
7900
+ messages: [{ role: "user", content: prompt }],
7901
+ maxTokens: 2048,
7902
+ temperature: 0.3
7903
+ });
7904
+ if (!response.success || !response.data) {
7905
+ return {
7906
+ success: false,
7907
+ error: response.error ?? "Failed to generate patterns",
7908
+ durationMs: response.durationMs
7909
+ };
7785
7910
  }
7786
- lines.push("");
7787
- lines.push(chalk5.gray("This is a dry run. Use --write to save files."));
7788
- return lines.join("\n");
7789
- }
7790
- function formatGeneratedJson(tests) {
7791
- const output = tests.map((test) => ({
7792
- gap: {
7793
- categoryId: test.gap.categoryId,
7794
- categoryName: test.gap.categoryName,
7795
- filePath: test.gap.filePath,
7796
- lineStart: test.gap.lineStart,
7797
- severity: test.gap.severity,
7798
- confidence: test.gap.confidence
7799
- },
7800
- template: {
7801
- id: test.template.id,
7802
- framework: test.template.framework,
7803
- language: test.template.language
7804
- },
7805
- suggestedPath: test.suggestedPath,
7806
- content: test.result.content,
7807
- imports: test.result.imports,
7808
- fixtures: test.result.fixtures,
7809
- substituted: test.result.substituted,
7810
- unresolved: test.result.unresolved
7811
- }));
7812
- return JSON.stringify(output, null, 2);
7813
- }
7814
- function suggestTestPath(sourceFile, template, basePath) {
7815
- const dir = dirname(sourceFile);
7816
- const name = basename(sourceFile);
7817
- const nameWithoutExt = name.replace(/\.[^.]+$/, "");
7818
- const extMap = {
7819
- python: ".py",
7820
- typescript: ".ts",
7821
- javascript: ".js",
7822
- go: "_test.go",
7823
- java: "Test.java",
7824
- rust: ".rs"
7825
- };
7826
- const ext = extMap[template.language] ?? ".test.ts";
7827
- let testFileName;
7828
- switch (template.language) {
7829
- case "python":
7830
- testFileName = `test_${nameWithoutExt}${ext}`;
7831
- break;
7832
- case "go":
7833
- testFileName = `${nameWithoutExt}${ext}`;
7834
- break;
7835
- case "java":
7836
- testFileName = `${nameWithoutExt}${ext}`;
7837
- break;
7838
- default:
7839
- testFileName = `${nameWithoutExt}.test${ext}`;
7840
- }
7841
- const relativeSrc = relative(basePath, dir);
7842
- const testDir = relativeSrc.replace(/^src/, "tests");
7843
- return `${testDir}/${testFileName}`;
7844
- }
7845
- function extractVariablesFromGap(gap) {
7846
- const fileName = basename(gap.filePath);
7847
- const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
7848
- const funcMatch = gap.codeSnippet.match(/(?:def|function|async function|const|let|var)\s+(\w+)/);
7849
- const classMatch = gap.codeSnippet.match(/(?:class)\s+(\w+)/);
7850
- return {
7851
- // File info
7852
- filePath: gap.filePath,
7853
- fileName,
7854
- fileNameWithoutExt,
7855
- lineNumber: gap.lineStart,
7856
- // Category info
7857
- categoryId: gap.categoryId,
7858
- categoryName: gap.categoryName,
7859
- domain: gap.domain,
7860
- level: gap.level,
7861
- severity: gap.severity,
7862
- confidence: gap.confidence,
7863
- // Code context
7864
- codeSnippet: gap.codeSnippet,
7865
- functionName: funcMatch?.[1] ?? "targetFunction",
7866
- className: classMatch?.[1] ?? "TargetClass",
7867
- // Common template variables
7868
- testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
7869
- testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
7870
- // Pattern info
7871
- patternId: gap.patternId,
7872
- patternType: gap.patternType
7873
- };
7874
- }
7875
- async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
7876
- const baseVars = extractVariablesFromGap(gap);
7877
- const result = await suggestVariables(
7878
- {
7879
- codeSnippet: gap.codeSnippet,
7880
- filePath: gap.filePath,
7881
- variables: templateVariables,
7882
- gap,
7883
- existingValues: baseVars
7884
- },
7885
- aiConfig
7886
- );
7887
- if (result.success && result.data) {
7888
- return result.data.values;
7889
- }
7890
- return baseVars;
7891
- }
7892
- async function writeGeneratedTests(tests, basePath, outputDir) {
7893
- const summary = {
7894
- created: [],
7895
- updated: [],
7896
- failed: [],
7897
- totalTests: 0
7898
- };
7899
- const testsByFile = /* @__PURE__ */ new Map();
7900
- for (const test of tests) {
7901
- let outputPath;
7902
- if (outputDir) {
7903
- outputPath = resolve(basePath, outputDir, test.suggestedPath);
7904
- } else {
7905
- outputPath = resolve(basePath, test.suggestedPath);
7906
- }
7907
- const existing = testsByFile.get(outputPath) ?? [];
7908
- existing.push(test);
7909
- testsByFile.set(outputPath, existing);
7910
- }
7911
- for (const [outputPath, fileTests] of testsByFile) {
7912
- try {
7913
- const dir = dirname(outputPath);
7914
- await mkdir(dir, { recursive: true });
7915
- let existingContent = "";
7916
- let fileExists = false;
7917
- try {
7918
- existingContent = await readFile(outputPath, "utf-8");
7919
- fileExists = true;
7920
- } catch {
7921
- }
7922
- const contentParts = [];
7923
- const allImports = /* @__PURE__ */ new Set();
7924
- for (const test of fileTests) {
7925
- for (const imp of test.result.imports) {
7926
- allImports.add(imp);
7927
- }
7928
- }
7929
- if (!fileExists && allImports.size > 0) {
7930
- contentParts.push(Array.from(allImports).join("\n"));
7931
- contentParts.push("");
7932
- }
7933
- for (const test of fileTests) {
7934
- contentParts.push(`// Test for ${test.gap.categoryName}`);
7935
- contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
7936
- contentParts.push(`// Generated by Pinata`);
7937
- contentParts.push("");
7938
- contentParts.push(test.result.content);
7939
- contentParts.push("");
7940
- }
7941
- const newContent = contentParts.join("\n");
7942
- let finalContent;
7943
- let appended = false;
7944
- if (fileExists) {
7945
- finalContent = existingContent.trimEnd() + "\n\n" + newContent;
7946
- appended = true;
7947
- } else {
7948
- finalContent = newContent;
7949
- }
7950
- await writeFile(outputPath, finalContent, "utf-8");
7951
- for (const test of fileTests) {
7952
- const result = {
7953
- path: outputPath,
7954
- created: !fileExists,
7955
- appended,
7956
- categoryId: test.gap.categoryId,
7957
- gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
7958
- };
7959
- if (fileExists) {
7960
- summary.updated.push(result);
7961
- } else {
7962
- summary.created.push(result);
7963
- }
7964
- summary.totalTests++;
7965
- }
7966
- } catch (error) {
7967
- summary.failed.push({
7968
- path: outputPath,
7969
- error: error instanceof Error ? error.message : String(error)
7970
- });
7971
- }
7972
- }
7973
- return ok(summary);
7974
- }
7975
- function formatWriteSummary(summary, basePath) {
7976
- const lines = [];
7977
- lines.push("");
7978
- lines.push(chalk5.bold.cyan("Write Summary:"));
7979
- lines.push(chalk5.gray("\u2500".repeat(60)));
7980
- if (summary.created.length > 0) {
7981
- const uniquePaths = new Set(summary.created.map((r) => r.path));
7982
- lines.push("");
7983
- lines.push(chalk5.green.bold(`Created ${uniquePaths.size} file(s):`));
7984
- for (const path2 of uniquePaths) {
7985
- const relPath = relative(basePath, path2);
7986
- const testsInFile = summary.created.filter((r) => r.path === path2).length;
7987
- lines.push(chalk5.green(` + ${relPath} (${testsInFile} test(s))`));
7988
- }
7989
- }
7990
- if (summary.updated.length > 0) {
7991
- const uniquePaths = new Set(summary.updated.map((r) => r.path));
7992
- lines.push("");
7993
- lines.push(chalk5.yellow.bold(`Updated ${uniquePaths.size} file(s):`));
7994
- for (const path2 of uniquePaths) {
7995
- const relPath = relative(basePath, path2);
7996
- const testsInFile = summary.updated.filter((r) => r.path === path2).length;
7997
- lines.push(chalk5.yellow(` ~ ${relPath} (${testsInFile} test(s) appended)`));
7998
- }
7999
- }
8000
- if (summary.failed.length > 0) {
8001
- lines.push("");
8002
- lines.push(chalk5.red.bold(`Failed to write ${summary.failed.length} file(s):`));
8003
- for (const fail of summary.failed) {
8004
- const relPath = relative(basePath, fail.path);
8005
- lines.push(chalk5.red(` \u2717 ${relPath}: ${fail.error}`));
8006
- }
8007
- }
8008
- lines.push("");
8009
- lines.push(chalk5.gray("\u2500".repeat(60)));
8010
- lines.push(chalk5.bold(`Total: ${summary.totalTests} test(s) written to ${(/* @__PURE__ */ new Set([...summary.created.map((r) => r.path), ...summary.updated.map((r) => r.path)])).size} file(s)`));
8011
- if (summary.failed.length > 0) {
8012
- lines.push(chalk5.red(`Failures: ${summary.failed.length}`));
8013
- }
8014
- return lines.join("\n");
8015
- }
8016
-
8017
- // src/ai/explainer.ts
8018
- var SYSTEM_PROMPT2 = `You are a security expert explaining code vulnerabilities to developers.
8019
- Your explanations should be:
8020
- - Clear and actionable
8021
- - Focused on the specific code pattern
8022
- - Include concrete remediation steps
8023
- - Reference relevant security standards (OWASP, CWE) when applicable
8024
-
8025
- Always respond with valid JSON matching this structure:
8026
- {
8027
- "summary": "1-2 sentence summary",
8028
- "explanation": "Detailed explanation of the vulnerability",
8029
- "risk": "What an attacker could do if this is exploited",
8030
- "remediation": "Step-by-step instructions to fix",
8031
- "safeExample": "Code example showing the safe pattern",
8032
- "references": ["optional array of CVE/CWE/OWASP references"]
8033
- }`;
8034
- async function explainGap(gap, category, config2) {
8035
- const ai = createAIService(config2);
8036
- if (!ai.isConfigured()) {
8037
- return {
8038
- success: false,
8039
- error: "AI service not configured",
8040
- durationMs: 0
8041
- };
8042
- }
8043
- const prompt = buildExplainPrompt(gap);
8044
- const response = await ai.completeJSON({
8045
- systemPrompt: SYSTEM_PROMPT2,
8046
- messages: [{ role: "user", content: prompt }],
8047
- maxTokens: 1024,
8048
- temperature: 0.3
8049
- });
8050
- return response;
8051
- }
8052
- function buildExplainPrompt(gap, category) {
8053
- const parts = [];
8054
- parts.push(`Explain this security finding:
8055
- `);
8056
- parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
8057
- parts.push(`**Severity:** ${gap.severity}`);
8058
- parts.push(`**Confidence:** ${gap.confidence}`);
8059
- parts.push(`**File:** ${gap.filePath}`);
8060
- parts.push(`**Line:** ${gap.lineStart}`);
8061
- if (gap.codeSnippet) {
8062
- parts.push(`
8063
- **Code:**
8064
- \`\`\`
8065
- ${gap.codeSnippet}
8066
- \`\`\``);
8067
- }
8068
- parts.push(`
8069
- **Pattern:** ${gap.patternId}`);
8070
- parts.push(`**Detection Type:** ${gap.patternType}`);
8071
- parts.push(`
8072
- Provide a clear, actionable explanation for a developer.`);
8073
- return parts.join("\n");
8074
- }
8075
- function generateFallbackExplanation(gap) {
8076
- const summaries = {
8077
- "sql-injection": "SQL query constructed with user input may allow injection attacks.",
8078
- "xss": "User input rendered without escaping may allow script injection.",
8079
- "command-injection": "Shell command constructed with user input may allow command execution.",
8080
- "path-traversal": "File path constructed with user input may allow directory traversal.",
8081
- "hardcoded-secrets": "Sensitive credentials found in source code.",
8082
- "deserialization": "Untrusted data deserialization may allow code execution.",
8083
- "ssrf": "Server-side request with user-controlled URL may allow internal access.",
8084
- "xxe": "XML parser may be vulnerable to external entity injection.",
8085
- "csrf": "State-changing request lacks CSRF protection.",
8086
- "ldap-injection": "LDAP query constructed with user input may allow injection."
8087
- };
8088
- const remediations = {
8089
- "sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
8090
- "xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
8091
- "command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
8092
- "path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
8093
- "hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
8094
- "deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
8095
- "ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
8096
- "xxe": "Disable external entity processing in XML parser configuration.",
8097
- "csrf": "Implement CSRF tokens for all state-changing requests.",
8098
- "ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
8099
- };
8100
- const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
8101
- const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
8102
- return {
8103
- summary,
8104
- explanation: `The pattern "${gap.patternId}" detected a potential ${gap.categoryName} vulnerability at line ${gap.lineStart}. This type of issue has ${gap.severity} severity and was detected with ${gap.confidence} confidence.`,
8105
- risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
8106
- remediation,
8107
- references: []
8108
- };
8109
- }
8110
-
8111
- // src/ai/pattern-suggester.ts
8112
- var SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
8113
- Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
8114
-
8115
- Your patterns should:
8116
- 1. Be specific enough to avoid false positives
8117
- 2. Be general enough to catch variations
8118
- 3. Use standard regex syntax (no lookbehind for compatibility)
8119
- 4. Include examples of what matches and what doesn't
8120
-
8121
- Always respond with valid JSON matching this structure:
8122
- {
8123
- "suggestions": [
8124
- {
8125
- "id": "pattern-id-kebab-case",
8126
- "pattern": "regex pattern here",
8127
- "description": "What this pattern detects",
8128
- "confidence": "high|medium|low",
8129
- "matchExample": "code that should match",
8130
- "safeExample": "similar code that should NOT match",
8131
- "reasoning": "Why this pattern works"
8132
- }
8133
- ]
8134
- }
8135
-
8136
- Important:
8137
- - Escape backslashes properly for JSON (use \\\\s not \\s)
8138
- - Test your patterns mentally against the examples
8139
- - Prefer simpler patterns that are less likely to cause ReDoS`;
8140
- async function suggestPatterns(request, config2) {
8141
- const ai = createAIService(config2);
8142
- if (!ai.isConfigured()) {
8143
- return {
8144
- success: false,
8145
- error: "AI service not configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.",
8146
- durationMs: 0
8147
- };
8148
- }
8149
- const prompt = buildPatternPrompt(request);
8150
- const response = await ai.completeJSON({
8151
- systemPrompt: SYSTEM_PROMPT3,
8152
- messages: [{ role: "user", content: prompt }],
8153
- maxTokens: 2048,
8154
- temperature: 0.3
8155
- });
8156
- if (!response.success || !response.data) {
8157
- return {
8158
- success: false,
8159
- error: response.error ?? "Failed to generate patterns",
8160
- durationMs: response.durationMs
8161
- };
8162
- }
8163
- const validated = validatePatterns(
8164
- response.data.suggestions ?? [],
8165
- request.vulnerableCode,
8166
- request.safeCode ?? []
8167
- );
8168
- const result = {
8169
- success: true,
8170
- data: validated,
8171
- durationMs: response.durationMs
8172
- };
8173
- if (response.usage) {
8174
- result.usage = response.usage;
8175
- }
8176
- return result;
7911
+ const validated = validatePatterns(
7912
+ response.data.suggestions ?? [],
7913
+ request.vulnerableCode,
7914
+ request.safeCode ?? []
7915
+ );
7916
+ const result = {
7917
+ success: true,
7918
+ data: validated,
7919
+ durationMs: response.durationMs
7920
+ };
7921
+ if (response.usage) {
7922
+ result.usage = response.usage;
7923
+ }
7924
+ return result;
8177
7925
  }
8178
7926
  function buildPatternPrompt(request) {
8179
7927
  const parts = [];
@@ -8277,73 +8025,89 @@ function hasRedosPotential(pattern) {
8277
8025
 
8278
8026
  // src/cli/index.ts
8279
8027
  init_results_cache();
8028
+ var __filename2 = fileURLToPath(import.meta.url);
8029
+ var __dirname2 = dirname(__filename2);
8030
+ function getDefinitionsPath() {
8031
+ const candidates = [
8032
+ resolve(__dirname2, "../../src/categories/definitions"),
8033
+ resolve(process.cwd(), "src/categories/definitions"),
8034
+ resolve(__dirname2, "../categories/definitions")
8035
+ ];
8036
+ for (const candidate of candidates) {
8037
+ if (existsSync(candidate)) {
8038
+ return candidate;
8039
+ }
8040
+ }
8041
+ return candidates[0];
8042
+ }
8043
+ init_results_cache();
8280
8044
  var SEVERITY_COLORS2 = {
8281
- critical: chalk5.red.bold,
8282
- high: chalk5.red,
8283
- medium: chalk5.yellow,
8284
- low: chalk5.gray
8045
+ critical: chalk7.red.bold,
8046
+ high: chalk7.red,
8047
+ medium: chalk7.yellow,
8048
+ low: chalk7.gray
8285
8049
  };
8286
8050
  var DOMAIN_COLORS2 = {
8287
- security: chalk5.red,
8288
- data: chalk5.blue,
8289
- concurrency: chalk5.magenta,
8290
- input: chalk5.cyan,
8291
- resource: chalk5.yellow,
8292
- reliability: chalk5.green,
8293
- performance: chalk5.yellowBright,
8294
- platform: chalk5.gray,
8295
- business: chalk5.white,
8296
- compliance: chalk5.blueBright
8051
+ security: chalk7.red,
8052
+ data: chalk7.blue,
8053
+ concurrency: chalk7.magenta,
8054
+ input: chalk7.cyan,
8055
+ resource: chalk7.yellow,
8056
+ reliability: chalk7.green,
8057
+ performance: chalk7.yellowBright,
8058
+ platform: chalk7.gray,
8059
+ business: chalk7.white,
8060
+ compliance: chalk7.blueBright
8297
8061
  };
8298
8062
  var GRADE_COLORS = {
8299
- A: chalk5.green.bold,
8300
- B: chalk5.green,
8301
- C: chalk5.yellow,
8302
- D: chalk5.red,
8303
- F: chalk5.red.bold
8063
+ A: chalk7.green.bold,
8064
+ B: chalk7.green,
8065
+ C: chalk7.yellow,
8066
+ D: chalk7.red,
8067
+ F: chalk7.red.bold
8304
8068
  };
8305
8069
  var BANNER = `
8306
- ${chalk5.cyan(" ____ _ _ ")}
8307
- ${chalk5.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
8308
- ${chalk5.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
8309
- ${chalk5.cyan("| __/| | | | | (_| | || (_| |")}
8310
- ${chalk5.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
8070
+ ${chalk7.cyan(" ____ _ _ ")}
8071
+ ${chalk7.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
8072
+ ${chalk7.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
8073
+ ${chalk7.cyan("| __/| | | | | (_| | || (_| |")}
8074
+ ${chalk7.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
8311
8075
  `;
8312
8076
  function formatScanTerminal(result, basePath) {
8313
8077
  const lines = [];
8314
8078
  lines.push(BANNER);
8315
- lines.push(chalk5.gray(`Analyzing: ${result.targetDirectory}`));
8079
+ lines.push(chalk7.gray(`Analyzing: ${result.targetDirectory}`));
8316
8080
  const projectTypeLabel = getProjectTypeDescription(result.projectType.type);
8317
- lines.push(chalk5.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
8318
- lines.push(chalk5.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
8081
+ lines.push(chalk7.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
8082
+ lines.push(chalk7.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
8319
8083
  lines.push("");
8320
8084
  lines.push(formatScoreBox(result.score));
8321
8085
  lines.push("");
8322
- lines.push(chalk5.bold("Domain Coverage:"));
8086
+ lines.push(chalk7.bold("Domain Coverage:"));
8323
8087
  lines.push(formatDomainCoverage(result.coverage));
8324
8088
  lines.push("");
8325
8089
  if (result.gaps.length > 0) {
8326
8090
  lines.push(formatGapsSummary(result.gaps, basePath));
8327
8091
  lines.push("");
8328
8092
  } else {
8329
- lines.push(chalk5.green.bold("No gaps detected! Your codebase has good test coverage."));
8093
+ lines.push(chalk7.green.bold("No gaps detected! Your codebase has good test coverage."));
8330
8094
  lines.push("");
8331
8095
  }
8332
8096
  if (result.gaps.length > 0) {
8333
- lines.push(chalk5.gray("Run `pinata generate --gaps` to create tests for these gaps."));
8097
+ lines.push(chalk7.gray("Run `pinata generate --gaps` to create tests for these gaps."));
8334
8098
  }
8335
- lines.push(chalk5.gray(`
8099
+ lines.push(chalk7.gray(`
8336
8100
  Scan completed in ${result.durationMs}ms`));
8337
8101
  return lines.join("\n");
8338
8102
  }
8339
8103
  function formatScoreBox(score) {
8340
- const gradeColor = GRADE_COLORS[score.grade] ?? chalk5.white;
8104
+ const gradeColor = GRADE_COLORS[score.grade] ?? chalk7.white;
8341
8105
  const scoreStr = `Pinata Score: ${score.overall}/100 ${gradeColor(`(${score.grade})`)}`;
8342
8106
  const boxWidth = 60;
8343
8107
  const padding = Math.floor((boxWidth - scoreStr.length) / 2);
8344
- const top = chalk5.cyan("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
8345
- const middle = chalk5.cyan("\u2551") + " ".repeat(padding) + scoreStr + " ".repeat(boxWidth - padding - scoreStr.length) + chalk5.cyan("\u2551");
8346
- const bottom = chalk5.cyan("\u255A" + "\u2550".repeat(boxWidth) + "\u255D");
8108
+ const top = chalk7.cyan("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
8109
+ const middle = chalk7.cyan("\u2551") + " ".repeat(padding) + scoreStr + " ".repeat(boxWidth - padding - scoreStr.length) + chalk7.cyan("\u2551");
8110
+ const bottom = chalk7.cyan("\u255A" + "\u2550".repeat(boxWidth) + "\u255D");
8347
8111
  return `${top}
8348
8112
  ${middle}
8349
8113
  ${bottom}`;
@@ -8358,14 +8122,14 @@ function formatDomainCoverage(coverage) {
8358
8122
  }
8359
8123
  const percent = domainCoverage.coveragePercent;
8360
8124
  const filledWidth = Math.round(percent / 100 * barWidth);
8361
- const bar = chalk5.green("\u2588".repeat(filledWidth)) + chalk5.gray("\u2591".repeat(barWidth - filledWidth));
8362
- const domainColor = DOMAIN_COLORS2[domain] ?? chalk5.white;
8125
+ const bar = chalk7.green("\u2588".repeat(filledWidth)) + chalk7.gray("\u2591".repeat(barWidth - filledWidth));
8126
+ const domainColor = DOMAIN_COLORS2[domain] ?? chalk7.white;
8363
8127
  const domainName = domain.padEnd(15);
8364
8128
  const stats = `${domainCoverage.categoriesCovered}/${domainCoverage.categoriesScanned} categories`;
8365
8129
  lines.push(` ${domainColor(domainName)} ${bar} ${percent.toString().padStart(3)}% (${stats})`);
8366
8130
  }
8367
8131
  if (lines.length === 0) {
8368
- lines.push(chalk5.gray(" No domain coverage data available."));
8132
+ lines.push(chalk7.gray(" No domain coverage data available."));
8369
8133
  }
8370
8134
  return lines.join("\n");
8371
8135
  }
@@ -8376,48 +8140,48 @@ function formatGapsSummary(gaps, basePath) {
8376
8140
  const medium = gaps.filter((g) => g.severity === "medium");
8377
8141
  const low = gaps.filter((g) => g.severity === "low");
8378
8142
  if (critical.length > 0) {
8379
- lines.push(chalk5.red.bold(`
8143
+ lines.push(chalk7.red.bold(`
8380
8144
  Critical Gaps (${critical.length}):`));
8381
8145
  for (const gap of critical.slice(0, 5)) {
8382
8146
  lines.push(formatGapLine(gap, basePath, "critical"));
8383
8147
  }
8384
8148
  if (critical.length > 5) {
8385
- lines.push(chalk5.gray(` ... and ${critical.length - 5} more critical gaps`));
8149
+ lines.push(chalk7.gray(` ... and ${critical.length - 5} more critical gaps`));
8386
8150
  }
8387
8151
  }
8388
8152
  if (high.length > 0) {
8389
- lines.push(chalk5.red(`
8153
+ lines.push(chalk7.red(`
8390
8154
  High Severity Gaps (${high.length}):`));
8391
8155
  for (const gap of high.slice(0, 5)) {
8392
8156
  lines.push(formatGapLine(gap, basePath, "high"));
8393
8157
  }
8394
8158
  if (high.length > 5) {
8395
- lines.push(chalk5.gray(` ... and ${high.length - 5} more high severity gaps`));
8159
+ lines.push(chalk7.gray(` ... and ${high.length - 5} more high severity gaps`));
8396
8160
  }
8397
8161
  }
8398
8162
  if (medium.length > 0) {
8399
- lines.push(chalk5.yellow(`
8163
+ lines.push(chalk7.yellow(`
8400
8164
  Medium Severity Gaps (${medium.length}):`));
8401
8165
  for (const gap of medium.slice(0, 3)) {
8402
8166
  lines.push(formatGapLine(gap, basePath, "medium"));
8403
8167
  }
8404
8168
  if (medium.length > 3) {
8405
- lines.push(chalk5.gray(` ... and ${medium.length - 3} more medium severity gaps`));
8169
+ lines.push(chalk7.gray(` ... and ${medium.length - 3} more medium severity gaps`));
8406
8170
  }
8407
8171
  }
8408
8172
  if (low.length > 0) {
8409
- lines.push(chalk5.gray(`
8173
+ lines.push(chalk7.gray(`
8410
8174
  Low Severity: ${low.length} gaps`));
8411
8175
  }
8412
8176
  return lines.join("\n");
8413
8177
  }
8414
8178
  function formatGapLine(gap, basePath, severity) {
8415
- const severityColor = SEVERITY_COLORS2[severity] ?? chalk5.white;
8179
+ const severityColor = SEVERITY_COLORS2[severity] ?? chalk7.white;
8416
8180
  const icon = severity === "critical" ? "\u26D4" : severity === "high" ? "\u{1F534}" : severity === "medium" ? "\u{1F7E1}" : "\u26AA";
8417
8181
  const relPath = relative(basePath, gap.filePath);
8418
8182
  const location = `${relPath}:${gap.lineStart}`;
8419
8183
  const confidence = gap.confidence.toUpperCase();
8420
- return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk5.cyan(location.padEnd(30))} ${chalk5.gray(confidence)} confidence`;
8184
+ return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk7.cyan(location.padEnd(30))} ${chalk7.gray(confidence)} confidence`;
8421
8185
  }
8422
8186
  function formatLanguages(result) {
8423
8187
  const languages = [];
@@ -8640,637 +8404,785 @@ function isValidScanOutputFormat(format) {
8640
8404
  return ["terminal", "json", "markdown", "sarif", "html", "junit-xml"].includes(format);
8641
8405
  }
8642
8406
 
8643
- // src/cli/index.ts
8644
- var __filename2 = fileURLToPath(import.meta.url);
8645
- var __dirname2 = dirname(__filename2);
8646
- function getDefinitionsPath() {
8647
- const candidates = [
8648
- // When running from dist/cli/index.js
8649
- resolve(__dirname2, "../../src/categories/definitions"),
8650
- // When running from project root via npx/npm
8651
- resolve(process.cwd(), "src/categories/definitions"),
8652
- // When bundled in dist (future)
8653
- resolve(__dirname2, "../categories/definitions")
8654
- ];
8655
- for (const candidate of candidates) {
8656
- if (existsSync(candidate)) {
8657
- return candidate;
8658
- }
8659
- }
8660
- return candidates[0];
8661
- }
8662
- var program = new Command();
8663
- program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
8664
- program.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("--output-file <path>", "Write output to file (useful for SARIF upload)").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("--execute", "Run dynamic tests in Docker sandbox to confirm vulnerabilities").option("--dry-run", "Preview generated tests without executing (use with --execute)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
8665
- const isQuiet = Boolean(options["quiet"]);
8666
- const isVerbose = Boolean(options["verbose"]);
8667
- if (isQuiet) {
8668
- logger.configure({ level: "error" });
8669
- } else if (isVerbose) {
8670
- logger.configure({ level: "debug" });
8671
- }
8672
- const targetDirectory = resolve(targetPath ?? process.cwd());
8673
- if (!existsSync(targetDirectory)) {
8674
- console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
8675
- process.exit(1);
8676
- }
8677
- const outputFormat = String(options["output"] ?? "terminal");
8678
- if (!isValidScanOutputFormat(outputFormat)) {
8679
- console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
8680
- process.exit(1);
8681
- }
8682
- const validSeverities = ["critical", "high", "medium", "low"];
8683
- const minSeverity = String(options["severity"] ?? "low");
8684
- if (!validSeverities.includes(minSeverity)) {
8685
- console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
8686
- process.exit(1);
8687
- }
8688
- const validConfidences = ["high", "medium", "low"];
8689
- const minConfidence = String(options["confidence"] ?? "high");
8690
- if (!validConfidences.includes(minConfidence)) {
8691
- console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
8692
- process.exit(1);
8693
- }
8694
- const domainsStr = options["domains"];
8695
- let domains = [];
8696
- if (domainsStr) {
8697
- const domainList = domainsStr.split(",").map((d) => d.trim());
8698
- for (const domain of domainList) {
8699
- if (!RISK_DOMAINS.includes(domain)) {
8700
- console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
8701
- process.exit(1);
8702
- }
8407
+ // src/cli/commands/analyze.ts
8408
+ function registerAnalyzeCommand(program2) {
8409
+ program2.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("--output-file <path>", "Write output to file (useful for SARIF upload)").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("--execute", "Run dynamic tests in Docker sandbox to confirm vulnerabilities").option("--dry-run", "Preview generated tests without executing (use with --execute)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
8410
+ const isQuiet = Boolean(options["quiet"]);
8411
+ const isVerbose = Boolean(options["verbose"]);
8412
+ if (isQuiet) {
8413
+ logger.configure({ level: "error" });
8414
+ } else if (isVerbose) {
8415
+ logger.configure({ level: "debug" });
8703
8416
  }
8704
- domains = domainList;
8705
- }
8706
- const excludeStr = options["exclude"];
8707
- const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
8708
- const failOn = options["failOn"];
8709
- if (failOn && !["critical", "high", "medium"].includes(failOn)) {
8710
- console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
8711
- process.exit(1);
8712
- }
8713
- const showSpinner = outputFormat === "terminal" && !isQuiet;
8714
- const spinner = showSpinner ? ora("Loading categories...").start() : null;
8715
- try {
8716
- const store = createCategoryStore();
8717
- const definitionsPath = getDefinitionsPath();
8718
- logger.debug(`Loading categories from: ${definitionsPath}`);
8719
- const loadResult = await store.loadFromDirectory(definitionsPath);
8720
- if (!loadResult.success) {
8721
- spinner?.fail("Failed to load categories");
8722
- console.error(formatError(loadResult.error));
8417
+ const targetDirectory = resolve(targetPath ?? process.cwd());
8418
+ if (!existsSync(targetDirectory)) {
8419
+ console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
8723
8420
  process.exit(1);
8724
8421
  }
8725
- if (spinner) {
8726
- spinner.text = `Loaded ${loadResult.data} categories. Scanning...`;
8727
- }
8728
- logger.debug(`Loaded ${loadResult.data} categories`);
8729
- const scanner = createScanner(store);
8730
- const scanOptions = {
8731
- minSeverity,
8732
- minConfidence,
8733
- detectTestFiles: true
8734
- };
8735
- if (domains.length > 0) {
8736
- scanOptions.domains = domains;
8422
+ const outputFormat = String(options["output"] ?? "terminal");
8423
+ if (!isValidScanOutputFormat(outputFormat)) {
8424
+ console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
8425
+ process.exit(1);
8737
8426
  }
8738
- if (excludeDirs) {
8739
- scanOptions.excludeDirs = excludeDirs;
8427
+ const validSeverities = ["critical", "high", "medium", "low"];
8428
+ const minSeverity = String(options["severity"] ?? "low");
8429
+ if (!validSeverities.includes(minSeverity)) {
8430
+ console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
8431
+ process.exit(1);
8740
8432
  }
8741
- const scanResult = await scanner.scanDirectory(targetDirectory, scanOptions);
8742
- if (!scanResult.success) {
8743
- spinner?.fail("Scan failed");
8744
- console.error(formatError(scanResult.error));
8433
+ const validConfidences = ["high", "medium", "low"];
8434
+ const minConfidence = String(options["confidence"] ?? "high");
8435
+ if (!validConfidences.includes(minConfidence)) {
8436
+ console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
8745
8437
  process.exit(1);
8746
8438
  }
8747
- spinner?.stop();
8748
- const shouldVerify = Boolean(options["verify"]);
8749
- if (shouldVerify && scanResult.data.gaps.length > 0) {
8750
- const { hasApiKey: hasApiKey2, setConfigValue: setConfigValue2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8751
- const { createInterface } = await import('readline');
8752
- let provider = "anthropic";
8753
- if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
8754
- spinner?.stop();
8755
- console.log(chalk5.yellow("\nAI verification requires an API key."));
8756
- console.log(chalk5.gray("Get one at: https://console.anthropic.com/settings/keys"));
8757
- console.log(chalk5.gray("Or: https://platform.openai.com/api-keys\n"));
8758
- const rl = createInterface({ input: process.stdin, output: process.stdout });
8759
- const askQuestion = (question) => {
8760
- return new Promise((resolve7) => {
8761
- rl.question(question, (answer) => resolve7(answer.trim()));
8762
- });
8763
- };
8764
- const apiKey = await askQuestion(chalk5.cyan("Enter your Anthropic or OpenAI API key: "));
8765
- rl.close();
8766
- if (!apiKey) {
8767
- console.log(chalk5.red("No API key provided. Skipping AI verification."));
8768
- } else {
8769
- if (apiKey.startsWith("sk-ant-")) {
8770
- setConfigValue2("anthropicApiKey", apiKey);
8771
- provider = "anthropic";
8772
- console.log(chalk5.green("Anthropic API key saved to ~/.pinata/config.json\n"));
8439
+ const domainsStr = options["domains"];
8440
+ let domains = [];
8441
+ if (domainsStr) {
8442
+ const domainList = domainsStr.split(",").map((d) => d.trim());
8443
+ for (const domain of domainList) {
8444
+ if (!RISK_DOMAINS.includes(domain)) {
8445
+ console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
8446
+ process.exit(1);
8447
+ }
8448
+ }
8449
+ domains = domainList;
8450
+ }
8451
+ const excludeStr = options["exclude"];
8452
+ const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
8453
+ const failOn = options["failOn"];
8454
+ if (failOn && !["critical", "high", "medium"].includes(failOn)) {
8455
+ console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
8456
+ process.exit(1);
8457
+ }
8458
+ const showSpinner = outputFormat === "terminal" && !isQuiet;
8459
+ const spinner = showSpinner ? ora3("Loading categories...").start() : null;
8460
+ try {
8461
+ const store = createCategoryStore();
8462
+ const definitionsPath = getDefinitionsPath();
8463
+ logger.debug(`Loading categories from: ${definitionsPath}`);
8464
+ const loadResult = await store.loadFromDirectory(definitionsPath);
8465
+ if (!loadResult.success) {
8466
+ spinner?.fail("Failed to load categories");
8467
+ console.error(formatError(loadResult.error));
8468
+ process.exit(1);
8469
+ }
8470
+ if (spinner) {
8471
+ spinner.text = `Loaded ${loadResult.data} categories. Scanning...`;
8472
+ }
8473
+ const scanner = createScanner(store);
8474
+ const scanOptions = {
8475
+ minSeverity,
8476
+ minConfidence,
8477
+ detectTestFiles: true
8478
+ };
8479
+ if (domains.length > 0) {
8480
+ scanOptions.domains = domains;
8481
+ }
8482
+ if (excludeDirs) {
8483
+ scanOptions.excludeDirs = excludeDirs;
8484
+ }
8485
+ const scanResult = await scanner.scanDirectory(targetDirectory, scanOptions);
8486
+ if (!scanResult.success) {
8487
+ spinner?.fail("Scan failed");
8488
+ console.error(formatError(scanResult.error));
8489
+ process.exit(1);
8490
+ }
8491
+ spinner?.stop();
8492
+ const shouldVerify = Boolean(options["verify"]);
8493
+ if (shouldVerify && scanResult.data.gaps.length > 0) {
8494
+ const { hasApiKey: hasApiKey2, setConfigValue: setConfigValue2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8495
+ const { createInterface } = await import('readline');
8496
+ let provider = "anthropic";
8497
+ if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
8498
+ spinner?.stop();
8499
+ console.log(chalk7.yellow("\nAI verification requires an API key."));
8500
+ console.log(chalk7.gray("Get one at: https://console.anthropic.com/settings/keys"));
8501
+ console.log(chalk7.gray("Or: https://platform.openai.com/api-keys\n"));
8502
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8503
+ const askQuestion = (question) => new Promise((resolve9) => rl.question(question, (answer) => resolve9(answer.trim())));
8504
+ const apiKey = await askQuestion(chalk7.cyan("Enter your Anthropic or OpenAI API key: "));
8505
+ rl.close();
8506
+ if (!apiKey) {
8507
+ console.log(chalk7.red("No API key provided. Skipping AI verification."));
8773
8508
  } else {
8774
- setConfigValue2("openaiApiKey", apiKey);
8775
- provider = "openai";
8776
- console.log(chalk5.green("OpenAI API key saved to ~/.pinata/config.json\n"));
8509
+ if (apiKey.startsWith("sk-ant-")) {
8510
+ setConfigValue2("anthropicApiKey", apiKey);
8511
+ provider = "anthropic";
8512
+ console.log(chalk7.green("Anthropic API key saved to ~/.pinata/config.json\n"));
8513
+ } else {
8514
+ setConfigValue2("openaiApiKey", apiKey);
8515
+ provider = "openai";
8516
+ console.log(chalk7.green("OpenAI API key saved to ~/.pinata/config.json\n"));
8517
+ }
8777
8518
  }
8519
+ } else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
8520
+ provider = "openai";
8778
8521
  }
8779
- } else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
8780
- provider = "openai";
8781
- }
8782
- if (!hasApiKey2(provider)) {
8783
- } else {
8784
- const verifySpinner = showSpinner ? ora("Verifying gaps with AI...").start() : null;
8785
- try {
8786
- const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
8787
- const { readFile: readFile7 } = await import('fs/promises');
8788
- const apiKey = getApiKey2(provider);
8789
- const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
8790
- const { verified, dismissed, stats } = await verifier.verifyAll(
8791
- scanResult.data.gaps,
8792
- async (path2) => readFile7(path2, "utf-8")
8793
- );
8794
- scanResult.data.gaps = verified;
8795
- const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
8796
- let deduction = 0;
8797
- for (const gap of verified) {
8798
- deduction += severityWeights[gap.severity] ?? 1;
8799
- }
8800
- const newOverall = Math.max(0, 100 - deduction);
8801
- const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
8802
- scanResult.data.score.overall = newOverall;
8803
- scanResult.data.score.grade = newGrade;
8804
- verifySpinner?.succeed(
8805
- `AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
8806
- );
8807
- if (isVerbose && dismissed.length > 0) {
8808
- console.log(chalk5.gray("\nDismissed as false positives:"));
8809
- for (const { gap, reason } of dismissed.slice(0, 5)) {
8810
- console.log(chalk5.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
8811
- console.log(chalk5.gray(` Reason: ${reason.slice(0, 100)}...`));
8522
+ if (hasApiKey2(provider)) {
8523
+ const verifySpinner = showSpinner ? ora3("Verifying gaps with AI...").start() : null;
8524
+ try {
8525
+ const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
8526
+ const { readFile: readFile7 } = await import('fs/promises');
8527
+ const apiKey = getApiKey2(provider);
8528
+ const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
8529
+ const { verified, dismissed, stats } = await verifier.verifyAll(
8530
+ scanResult.data.gaps,
8531
+ async (path2) => readFile7(path2, "utf-8")
8532
+ );
8533
+ scanResult.data.gaps = verified;
8534
+ const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
8535
+ let deduction = 0;
8536
+ for (const gap of verified) {
8537
+ deduction += severityWeights[gap.severity] ?? 1;
8538
+ }
8539
+ const newOverall = Math.max(0, 100 - deduction);
8540
+ const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
8541
+ scanResult.data.score.overall = newOverall;
8542
+ scanResult.data.score.grade = newGrade;
8543
+ verifySpinner?.succeed(
8544
+ `AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
8545
+ );
8546
+ if (isVerbose && dismissed.length > 0) {
8547
+ console.log(chalk7.gray("\nDismissed as false positives:"));
8548
+ for (const { gap, reason } of dismissed.slice(0, 5)) {
8549
+ console.log(chalk7.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
8550
+ console.log(chalk7.gray(` Reason: ${reason.slice(0, 100)}...`));
8551
+ }
8552
+ if (dismissed.length > 5) {
8553
+ console.log(chalk7.gray(` ... and ${dismissed.length - 5} more`));
8554
+ }
8812
8555
  }
8813
- if (dismissed.length > 5) {
8814
- console.log(chalk5.gray(` ... and ${dismissed.length - 5} more`));
8556
+ } catch (error) {
8557
+ verifySpinner?.fail("AI verification failed (results unverified)");
8558
+ if (isVerbose) {
8559
+ console.error(chalk7.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
8815
8560
  }
8816
8561
  }
8817
- } catch (error) {
8818
- verifySpinner?.fail("AI verification failed (results unverified)");
8819
- if (isVerbose) {
8820
- console.error(chalk5.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
8821
- }
8822
8562
  }
8823
8563
  }
8824
- }
8825
- const shouldExecute = Boolean(options["execute"]);
8826
- const isDryRun = Boolean(options["dryRun"]);
8827
- if (shouldExecute && scanResult.data.gaps.length > 0) {
8828
- const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
8829
- const { readFile: readFile7 } = await import('fs/promises');
8830
- const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
8831
- if (testableGaps.length === 0) {
8832
- console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
8833
- console.log(chalk5.gray("Testable types: sql-injection, xss, command-injection, path-traversal"));
8834
- } else {
8835
- const runner = createRunner2(void 0, isDryRun);
8836
- const initResult = await runner.initialize();
8837
- if (!initResult.ready) {
8838
- console.log(chalk5.red(`
8839
- Dynamic execution unavailable: ${initResult.error}`));
8564
+ const shouldExecute = Boolean(options["execute"]);
8565
+ const isDryRun = Boolean(options["dryRun"]);
8566
+ if (shouldExecute && scanResult.data.gaps.length > 0) {
8567
+ const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
8568
+ const { readFile: readFile7 } = await import('fs/promises');
8569
+ const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
8570
+ if (testableGaps.length === 0) {
8571
+ console.log(chalk7.yellow("\nNo dynamically testable gaps found."));
8840
8572
  } else {
8841
- const fileContents = /* @__PURE__ */ new Map();
8842
- for (const gap of testableGaps) {
8843
- if (!fileContents.has(gap.filePath)) {
8844
- try {
8845
- fileContents.set(gap.filePath, await readFile7(gap.filePath, "utf-8"));
8846
- } catch {
8573
+ const runner = createRunner2(void 0, isDryRun);
8574
+ const initResult = await runner.initialize();
8575
+ if (!initResult.ready) {
8576
+ console.log(chalk7.red(`
8577
+ Dynamic execution unavailable: ${initResult.error}`));
8578
+ } else {
8579
+ const fileContents = /* @__PURE__ */ new Map();
8580
+ for (const gap of testableGaps) {
8581
+ if (!fileContents.has(gap.filePath)) {
8582
+ try {
8583
+ fileContents.set(gap.filePath, await readFile7(gap.filePath, "utf-8"));
8584
+ } catch {
8585
+ }
8847
8586
  }
8848
8587
  }
8849
- }
8850
- const executionSummary = await runner.executeAll(testableGaps, fileContents);
8851
- for (const result of executionSummary.results) {
8852
- const gap = scanResult.data.gaps.find(
8853
- (g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
8854
- );
8855
- if (gap && result.status === "confirmed") {
8856
- gap.confirmed = true;
8857
- gap.evidence = result.evidence;
8588
+ const executionSummary = await runner.executeAll(testableGaps, fileContents);
8589
+ for (const result of executionSummary.results) {
8590
+ const gap = scanResult.data.gaps.find(
8591
+ (g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
8592
+ );
8593
+ if (gap && result.status === "confirmed") {
8594
+ gap.confirmed = true;
8595
+ gap.evidence = result.evidence;
8596
+ }
8858
8597
  }
8859
- }
8860
- if (executionSummary.confirmed > 0) {
8861
- console.log(chalk5.red.bold(`
8598
+ if (executionSummary.confirmed > 0) {
8599
+ console.log(chalk7.red.bold(`
8862
8600
  \u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
8601
+ }
8863
8602
  }
8864
8603
  }
8865
8604
  }
8605
+ const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
8606
+ if (!cacheResult.success) {
8607
+ logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
8608
+ }
8609
+ const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
8610
+ const outputFile = options["outputFile"];
8611
+ if (outputFile) {
8612
+ writeFileSync(resolve(outputFile), output, "utf-8");
8613
+ logger.info(`Results written to: ${resolve(outputFile)}`);
8614
+ } else {
8615
+ console.log(output);
8616
+ }
8617
+ if (isVerbose && scanResult.data.warnings.length > 0) {
8618
+ console.error("\nWarnings:");
8619
+ for (const warning of scanResult.data.warnings) {
8620
+ console.error(` - ${warning}`);
8621
+ }
8622
+ }
8623
+ if (failOn) {
8624
+ const severityOrder = { critical: 3, high: 2, medium: 1 };
8625
+ const failLevel = severityOrder[failOn] ?? 0;
8626
+ const hasFailingGaps = scanResult.data.gaps.some((gap) => (severityOrder[gap.severity] ?? 0) >= failLevel);
8627
+ if (hasFailingGaps) {
8628
+ process.exit(1);
8629
+ }
8630
+ }
8631
+ process.exit(0);
8632
+ } catch (error) {
8633
+ spinner?.fail("Analysis failed");
8634
+ console.error(formatError(error instanceof Error ? error : new Error(String(error))));
8635
+ process.exit(1);
8866
8636
  }
8867
- const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
8868
- if (!cacheResult.success) {
8869
- logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
8637
+ });
8638
+ }
8639
+
8640
+ // src/cli/generate-formatters.ts
8641
+ init_errors();
8642
+ init_result();
8643
+ function formatGeneratedTerminal(tests, basePath) {
8644
+ const lines = [];
8645
+ if (tests.length === 0) {
8646
+ lines.push(chalk7.yellow("No tests generated."));
8647
+ return lines.join("\n");
8648
+ }
8649
+ lines.push(chalk7.bold.cyan(`
8650
+ Generated ${tests.length} test(s):
8651
+ `));
8652
+ lines.push(chalk7.gray("\u2500".repeat(60)));
8653
+ for (const test of tests) {
8654
+ const relGapPath = relative(basePath, test.gap.filePath);
8655
+ lines.push("");
8656
+ lines.push(chalk7.bold.white(`Test for: ${test.gap.categoryName}`));
8657
+ lines.push(chalk7.gray(` Gap location: ${relGapPath}:${test.gap.lineStart}`));
8658
+ lines.push(chalk7.gray(` Template: ${test.template.id}`));
8659
+ lines.push(chalk7.gray(` Output: ${test.suggestedPath}`));
8660
+ lines.push("");
8661
+ lines.push(chalk7.cyan(`// --- ${test.suggestedPath} ---`));
8662
+ lines.push("");
8663
+ if (test.result.imports.length > 0) {
8664
+ for (const imp of test.result.imports) {
8665
+ lines.push(chalk7.gray(imp));
8666
+ }
8667
+ lines.push("");
8870
8668
  }
8871
- const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
8872
- const outputFile = options["outputFile"];
8873
- if (outputFile) {
8874
- const outputPath = resolve(outputFile);
8875
- writeFileSync(outputPath, output, "utf-8");
8876
- logger.info(`Results written to: ${outputPath}`);
8669
+ lines.push(test.result.content);
8670
+ lines.push("");
8671
+ lines.push(chalk7.gray("\u2500".repeat(60)));
8672
+ }
8673
+ lines.push("");
8674
+ lines.push(chalk7.bold("Summary:"));
8675
+ lines.push(` Tests generated: ${chalk7.green(tests.length.toString())}`);
8676
+ const categories = new Set(tests.map((t) => t.category.id));
8677
+ lines.push(` Categories covered: ${chalk7.cyan(categories.size.toString())}`);
8678
+ if (tests.some((t) => t.result.unresolved.length > 0)) {
8679
+ const unresolvedCount = tests.reduce((acc, t) => acc + t.result.unresolved.length, 0);
8680
+ lines.push(chalk7.yellow(` Unresolved placeholders: ${unresolvedCount}`));
8681
+ lines.push(chalk7.gray(" (Some placeholders need manual completion)"));
8682
+ }
8683
+ lines.push("");
8684
+ lines.push(chalk7.gray("This is a dry run. Use --write to save files."));
8685
+ return lines.join("\n");
8686
+ }
8687
+ function formatGeneratedJson(tests) {
8688
+ const output = tests.map((test) => ({
8689
+ gap: {
8690
+ categoryId: test.gap.categoryId,
8691
+ categoryName: test.gap.categoryName,
8692
+ filePath: test.gap.filePath,
8693
+ lineStart: test.gap.lineStart,
8694
+ severity: test.gap.severity,
8695
+ confidence: test.gap.confidence
8696
+ },
8697
+ template: {
8698
+ id: test.template.id,
8699
+ framework: test.template.framework,
8700
+ language: test.template.language
8701
+ },
8702
+ suggestedPath: test.suggestedPath,
8703
+ content: test.result.content,
8704
+ imports: test.result.imports,
8705
+ fixtures: test.result.fixtures,
8706
+ substituted: test.result.substituted,
8707
+ unresolved: test.result.unresolved
8708
+ }));
8709
+ return JSON.stringify(output, null, 2);
8710
+ }
8711
+ function suggestTestPath(sourceFile, template, basePath) {
8712
+ const dir = dirname(sourceFile);
8713
+ const name = basename(sourceFile);
8714
+ const nameWithoutExt = name.replace(/\.[^.]+$/, "");
8715
+ const extMap = {
8716
+ python: ".py",
8717
+ typescript: ".ts",
8718
+ javascript: ".js",
8719
+ go: "_test.go",
8720
+ java: "Test.java",
8721
+ rust: ".rs"
8722
+ };
8723
+ const ext = extMap[template.language] ?? ".test.ts";
8724
+ let testFileName;
8725
+ switch (template.language) {
8726
+ case "python":
8727
+ testFileName = `test_${nameWithoutExt}${ext}`;
8728
+ break;
8729
+ case "go":
8730
+ testFileName = `${nameWithoutExt}${ext}`;
8731
+ break;
8732
+ case "java":
8733
+ testFileName = `${nameWithoutExt}${ext}`;
8734
+ break;
8735
+ default:
8736
+ testFileName = `${nameWithoutExt}.test${ext}`;
8737
+ }
8738
+ const relativeSrc = relative(basePath, dir);
8739
+ const testDir = relativeSrc.replace(/^src/, "tests");
8740
+ return `${testDir}/${testFileName}`;
8741
+ }
8742
+ function extractVariablesFromGap(gap) {
8743
+ const fileName = basename(gap.filePath);
8744
+ const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
8745
+ const funcMatch = gap.codeSnippet.match(/(?:def|function|async function|const|let|var)\s+(\w+)/);
8746
+ const classMatch = gap.codeSnippet.match(/(?:class)\s+(\w+)/);
8747
+ return {
8748
+ // File info
8749
+ filePath: gap.filePath,
8750
+ fileName,
8751
+ fileNameWithoutExt,
8752
+ lineNumber: gap.lineStart,
8753
+ // Category info
8754
+ categoryId: gap.categoryId,
8755
+ categoryName: gap.categoryName,
8756
+ domain: gap.domain,
8757
+ level: gap.level,
8758
+ severity: gap.severity,
8759
+ confidence: gap.confidence,
8760
+ // Code context
8761
+ codeSnippet: gap.codeSnippet,
8762
+ functionName: funcMatch?.[1] ?? "targetFunction",
8763
+ className: classMatch?.[1] ?? "TargetClass",
8764
+ // Common template variables
8765
+ testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
8766
+ testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
8767
+ // Pattern info
8768
+ patternId: gap.patternId,
8769
+ patternType: gap.patternType
8770
+ };
8771
+ }
8772
+ async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
8773
+ const baseVars = extractVariablesFromGap(gap);
8774
+ const result = await suggestVariables(
8775
+ {
8776
+ codeSnippet: gap.codeSnippet,
8777
+ filePath: gap.filePath,
8778
+ variables: templateVariables,
8779
+ gap,
8780
+ existingValues: baseVars
8781
+ },
8782
+ aiConfig
8783
+ );
8784
+ if (result.success && result.data) {
8785
+ return result.data.values;
8786
+ }
8787
+ return baseVars;
8788
+ }
8789
+ async function writeGeneratedTests(tests, basePath, outputDir) {
8790
+ const summary = {
8791
+ created: [],
8792
+ updated: [],
8793
+ failed: [],
8794
+ totalTests: 0
8795
+ };
8796
+ const testsByFile = /* @__PURE__ */ new Map();
8797
+ for (const test of tests) {
8798
+ let outputPath;
8799
+ if (outputDir) {
8800
+ outputPath = resolve(basePath, outputDir, test.suggestedPath);
8877
8801
  } else {
8878
- console.log(output);
8802
+ outputPath = resolve(basePath, test.suggestedPath);
8879
8803
  }
8880
- if (isVerbose && scanResult.data.warnings.length > 0) {
8881
- console.error("\nWarnings:");
8882
- for (const warning of scanResult.data.warnings) {
8883
- console.error(` - ${warning}`);
8804
+ const existing = testsByFile.get(outputPath) ?? [];
8805
+ existing.push(test);
8806
+ testsByFile.set(outputPath, existing);
8807
+ }
8808
+ for (const [outputPath, fileTests] of testsByFile) {
8809
+ try {
8810
+ const dir = dirname(outputPath);
8811
+ await mkdir(dir, { recursive: true });
8812
+ let existingContent = "";
8813
+ let fileExists = false;
8814
+ try {
8815
+ existingContent = await readFile(outputPath, "utf-8");
8816
+ fileExists = true;
8817
+ } catch {
8884
8818
  }
8885
- }
8886
- if (failOn) {
8887
- const severityOrder = {
8888
- critical: 3,
8889
- high: 2,
8890
- medium: 1
8891
- };
8892
- const failLevel = severityOrder[failOn] ?? 0;
8893
- const hasFailingGaps = scanResult.data.gaps.some((gap) => {
8894
- const gapLevel = severityOrder[gap.severity] ?? 0;
8895
- return gapLevel >= failLevel;
8896
- });
8897
- if (hasFailingGaps) {
8898
- const count = scanResult.data.gaps.filter((gap) => {
8899
- const gapLevel = severityOrder[gap.severity] ?? 0;
8900
- return gapLevel >= failLevel;
8901
- }).length;
8902
- logger.debug(`Exiting with code 1 due to ${count} gaps at ${failOn} level or above`);
8903
- process.exit(1);
8819
+ const contentParts = [];
8820
+ const allImports = /* @__PURE__ */ new Set();
8821
+ for (const test of fileTests) {
8822
+ for (const imp of test.result.imports) {
8823
+ allImports.add(imp);
8824
+ }
8825
+ }
8826
+ if (!fileExists && allImports.size > 0) {
8827
+ contentParts.push(Array.from(allImports).join("\n"));
8828
+ contentParts.push("");
8829
+ }
8830
+ for (const test of fileTests) {
8831
+ contentParts.push(`// Test for ${test.gap.categoryName}`);
8832
+ contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
8833
+ contentParts.push(`// Generated by Pinata`);
8834
+ contentParts.push("");
8835
+ contentParts.push(test.result.content);
8836
+ contentParts.push("");
8837
+ }
8838
+ const newContent = contentParts.join("\n");
8839
+ let finalContent;
8840
+ let appended = false;
8841
+ if (fileExists) {
8842
+ finalContent = existingContent.trimEnd() + "\n\n" + newContent;
8843
+ appended = true;
8844
+ } else {
8845
+ finalContent = newContent;
8846
+ }
8847
+ await writeFile(outputPath, finalContent, "utf-8");
8848
+ for (const test of fileTests) {
8849
+ const result = {
8850
+ path: outputPath,
8851
+ created: !fileExists,
8852
+ appended,
8853
+ categoryId: test.gap.categoryId,
8854
+ gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
8855
+ };
8856
+ if (fileExists) {
8857
+ summary.updated.push(result);
8858
+ } else {
8859
+ summary.created.push(result);
8860
+ }
8861
+ summary.totalTests++;
8904
8862
  }
8863
+ } catch (error) {
8864
+ summary.failed.push({
8865
+ path: outputPath,
8866
+ error: error instanceof Error ? error.message : String(error)
8867
+ });
8905
8868
  }
8906
- process.exit(0);
8907
- } catch (error) {
8908
- spinner?.fail("Analysis failed");
8909
- console.error(formatError(error instanceof Error ? error : new Error(String(error))));
8910
- process.exit(1);
8911
- }
8912
- });
8913
- program.command("generate").description("Generate tests for identified gaps").option("--gaps", "Generate tests for all identified gaps").option("-c, --category <id>", "Generate tests for specific category").option("-d, --domain <domain>", "Generate tests for all categories in domain").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "medium").option("--output-dir <dir>", "Directory for generated test files").option("--write", "Write files to disk (default is dry-run)").option("--ai", "Use AI for smarter template variable filling").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json", "terminal").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
8914
- const isQuiet = Boolean(options["quiet"]);
8915
- const isVerbose = Boolean(options["verbose"]);
8916
- const dryRun = !options["write"];
8917
- const useAI = Boolean(options["ai"]);
8918
- const aiProvider = String(options["aiProvider"] ?? "anthropic");
8919
- const outputFormat = String(options["output"] ?? "terminal");
8920
- if (isQuiet) {
8921
- logger.configure({ level: "error" });
8922
- } else if (isVerbose) {
8923
- logger.configure({ level: "debug" });
8924
8869
  }
8925
- if (!["terminal", "json"].includes(outputFormat)) {
8926
- console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
8927
- process.exit(1);
8870
+ return ok(summary);
8871
+ }
8872
+ function formatWriteSummary(summary, basePath) {
8873
+ const lines = [];
8874
+ lines.push("");
8875
+ lines.push(chalk7.bold.cyan("Write Summary:"));
8876
+ lines.push(chalk7.gray("\u2500".repeat(60)));
8877
+ if (summary.created.length > 0) {
8878
+ const uniquePaths = new Set(summary.created.map((r) => r.path));
8879
+ lines.push("");
8880
+ lines.push(chalk7.green.bold(`Created ${uniquePaths.size} file(s):`));
8881
+ for (const path2 of uniquePaths) {
8882
+ const relPath = relative(basePath, path2);
8883
+ const testsInFile = summary.created.filter((r) => r.path === path2).length;
8884
+ lines.push(chalk7.green(` + ${relPath} (${testsInFile} test(s))`));
8885
+ }
8928
8886
  }
8929
- const hasGaps = Boolean(options["gaps"]);
8930
- const categoryId = options["category"];
8931
- const domainFilter = options["domain"];
8932
- if (!hasGaps && !categoryId && !domainFilter) {
8933
- console.error(formatError(new Error(
8934
- "Specify what to generate: --gaps (all gaps), --category <id>, or --domain <domain>"
8935
- )));
8936
- process.exit(1);
8887
+ if (summary.updated.length > 0) {
8888
+ const uniquePaths = new Set(summary.updated.map((r) => r.path));
8889
+ lines.push("");
8890
+ lines.push(chalk7.yellow.bold(`Updated ${uniquePaths.size} file(s):`));
8891
+ for (const path2 of uniquePaths) {
8892
+ const relPath = relative(basePath, path2);
8893
+ const testsInFile = summary.updated.filter((r) => r.path === path2).length;
8894
+ lines.push(chalk7.yellow(` ~ ${relPath} (${testsInFile} test(s) appended)`));
8895
+ }
8937
8896
  }
8938
- if (domainFilter && !RISK_DOMAINS.includes(domainFilter)) {
8939
- console.error(formatError(new Error(
8940
- `Invalid domain: ${domainFilter}. Valid domains: ${RISK_DOMAINS.join(", ")}`
8941
- )));
8942
- process.exit(1);
8897
+ if (summary.failed.length > 0) {
8898
+ lines.push("");
8899
+ lines.push(chalk7.red.bold(`Failed to write ${summary.failed.length} file(s):`));
8900
+ for (const fail of summary.failed) {
8901
+ const relPath = relative(basePath, fail.path);
8902
+ lines.push(chalk7.red(` \u2717 ${relPath}: ${fail.error}`));
8903
+ }
8943
8904
  }
8944
- const validSeverities = ["critical", "high", "medium", "low"];
8945
- const minSeverity = String(options["severity"] ?? "medium");
8946
- if (!validSeverities.includes(minSeverity)) {
8947
- console.error(formatError(new Error(
8948
- `Invalid severity: ${minSeverity}. Use: critical, high, medium, low`
8949
- )));
8950
- process.exit(1);
8905
+ lines.push("");
8906
+ lines.push(chalk7.gray("\u2500".repeat(60)));
8907
+ lines.push(chalk7.bold(`Total: ${summary.totalTests} test(s) written to ${(/* @__PURE__ */ new Set([...summary.created.map((r) => r.path), ...summary.updated.map((r) => r.path)])).size} file(s)`));
8908
+ if (summary.failed.length > 0) {
8909
+ lines.push(chalk7.red(`Failures: ${summary.failed.length}`));
8951
8910
  }
8952
- const severityOrder = {
8953
- critical: 4,
8954
- high: 3,
8955
- medium: 2,
8956
- low: 1
8957
- };
8958
- const showSpinner = outputFormat === "terminal" && !isQuiet;
8959
- const spinner = showSpinner ? ora("Loading cached scan results...").start() : null;
8960
- try {
8961
- const projectRoot = process.cwd();
8962
- const cacheResult = await loadScanResults(projectRoot);
8963
- if (!cacheResult.success) {
8964
- spinner?.fail("No cached results");
8965
- console.error(formatError(cacheResult.error));
8966
- console.error(chalk5.yellow("\nRun `pinata analyze` first to scan for gaps."));
8967
- process.exit(1);
8968
- }
8969
- const cached = cacheResult.data;
8970
- let gaps = cached.gaps;
8971
- if (spinner) {
8972
- spinner.text = `Loaded ${gaps.length} gaps from cache. Filtering...`;
8973
- }
8974
- if (categoryId) {
8975
- gaps = gaps.filter((g) => g.categoryId === categoryId);
8976
- }
8977
- if (domainFilter) {
8978
- gaps = gaps.filter((g) => g.domain === domainFilter);
8979
- }
8980
- gaps = gaps.filter((g) => {
8981
- const gapLevel = severityOrder[g.severity] ?? 0;
8982
- const minLevel = severityOrder[minSeverity] ?? 0;
8983
- return gapLevel >= minLevel;
8984
- });
8985
- if (gaps.length === 0) {
8986
- spinner?.succeed("No gaps match the filters");
8987
- console.log(chalk5.yellow("\nNo gaps found matching the specified filters."));
8988
- process.exit(0);
8911
+ return lines.join("\n");
8912
+ }
8913
+
8914
+ // src/cli/commands/generate.ts
8915
+ init_results_cache();
8916
+ function registerGenerateCommand(program2) {
8917
+ program2.command("generate").description("Generate tests for identified gaps").option("--gaps", "Generate tests for all identified gaps").option("-c, --category <id>", "Generate tests for specific category").option("-d, --domain <domain>", "Generate tests for all categories in domain").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "medium").option("--output-dir <dir>", "Directory for generated test files").option("--write", "Write files to disk (default is dry-run)").option("--ai", "Use AI for smarter template variable filling").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json", "terminal").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
8918
+ const isQuiet = Boolean(options["quiet"]);
8919
+ const isVerbose = Boolean(options["verbose"]);
8920
+ const dryRun = !options["write"];
8921
+ const useAI = Boolean(options["ai"]);
8922
+ const aiProvider = String(options["aiProvider"] ?? "anthropic");
8923
+ const outputFormat = String(options["output"] ?? "terminal");
8924
+ if (isQuiet) {
8925
+ logger.configure({ level: "error" });
8926
+ } else if (isVerbose) {
8927
+ logger.configure({ level: "debug" });
8989
8928
  }
8990
- if (spinner) {
8991
- spinner.text = `Found ${gaps.length} gaps. Loading categories...`;
8929
+ if (!["terminal", "json"].includes(outputFormat)) {
8930
+ console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
8931
+ process.exit(1);
8992
8932
  }
8993
- const store = createCategoryStore();
8994
- const definitionsPath = getDefinitionsPath();
8995
- const loadResult = await store.loadFromDirectory(definitionsPath);
8996
- if (!loadResult.success) {
8997
- spinner?.fail("Failed to load categories");
8998
- console.error(formatError(loadResult.error));
8933
+ const hasGaps = Boolean(options["gaps"]);
8934
+ const categoryId = options["category"];
8935
+ const domainFilter = options["domain"];
8936
+ if (!hasGaps && !categoryId && !domainFilter) {
8937
+ console.error(formatError(new Error("Specify what to generate: --gaps (all gaps), --category <id>, or --domain <domain>")));
8999
8938
  process.exit(1);
9000
8939
  }
9001
- if (spinner) {
9002
- spinner.text = `Generating tests for ${gaps.length} gaps...`;
8940
+ if (domainFilter && !RISK_DOMAINS.includes(domainFilter)) {
8941
+ console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
8942
+ process.exit(1);
9003
8943
  }
9004
- const renderer = createRenderer({ strict: false, allowUnresolved: true });
9005
- const generatedTests = [];
9006
- const errors = [];
9007
- const gapsByCategory = /* @__PURE__ */ new Map();
9008
- for (const gap of gaps) {
9009
- const existing = gapsByCategory.get(gap.categoryId) ?? [];
9010
- existing.push(gap);
9011
- gapsByCategory.set(gap.categoryId, existing);
8944
+ const validSeverities = ["critical", "high", "medium", "low"];
8945
+ const minSeverity = String(options["severity"] ?? "medium");
8946
+ if (!validSeverities.includes(minSeverity)) {
8947
+ console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
8948
+ process.exit(1);
9012
8949
  }
9013
- for (const [catId, categoryGaps] of gapsByCategory) {
9014
- const categoryResult = store.get(catId);
9015
- if (!categoryResult.success) {
9016
- errors.push(`Category not found: ${catId}`);
9017
- continue;
8950
+ const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
8951
+ const showSpinner = outputFormat === "terminal" && !isQuiet;
8952
+ const spinner = showSpinner ? ora3("Loading cached scan results...").start() : null;
8953
+ try {
8954
+ const projectRoot = process.cwd();
8955
+ const cacheResult = await loadScanResults(projectRoot);
8956
+ if (!cacheResult.success) {
8957
+ spinner?.fail("No cached results");
8958
+ console.error(formatError(cacheResult.error));
8959
+ console.error(chalk7.yellow("\nRun `pinata analyze` first to scan for gaps."));
8960
+ process.exit(1);
9018
8961
  }
9019
- const category = categoryResult.data;
9020
- for (const gap of categoryGaps) {
9021
- const gapExt = gap.filePath.split(".").pop() ?? "";
9022
- const langMap = {
9023
- py: "python",
9024
- ts: "typescript",
9025
- tsx: "typescript",
9026
- js: "javascript",
9027
- jsx: "javascript",
9028
- go: "go",
9029
- java: "java",
9030
- rs: "rust"
9031
- };
9032
- const gapLang = langMap[gapExt];
9033
- let template = category.testTemplates.find((t) => t.language === gapLang);
9034
- if (!template) {
9035
- template = category.testTemplates[0];
9036
- }
9037
- if (!template) {
9038
- errors.push(`No templates available for ${catId}`);
8962
+ const cached = cacheResult.data;
8963
+ let gaps = cached.gaps;
8964
+ if (spinner) {
8965
+ spinner.text = `Loaded ${gaps.length} gaps from cache. Filtering...`;
8966
+ }
8967
+ if (categoryId) {
8968
+ gaps = gaps.filter((g) => g.categoryId === categoryId);
8969
+ }
8970
+ if (domainFilter) {
8971
+ gaps = gaps.filter((g) => g.domain === domainFilter);
8972
+ }
8973
+ gaps = gaps.filter((g) => (severityOrder[g.severity] ?? 0) >= (severityOrder[minSeverity] ?? 0));
8974
+ if (gaps.length === 0) {
8975
+ spinner?.succeed("No gaps match the filters");
8976
+ console.log(chalk7.yellow("\nNo gaps found matching the specified filters."));
8977
+ process.exit(0);
8978
+ }
8979
+ if (spinner) {
8980
+ spinner.text = `Found ${gaps.length} gaps. Loading categories...`;
8981
+ }
8982
+ const store = createCategoryStore();
8983
+ const definitionsPath = getDefinitionsPath();
8984
+ const loadResult = await store.loadFromDirectory(definitionsPath);
8985
+ if (!loadResult.success) {
8986
+ spinner?.fail("Failed to load categories");
8987
+ console.error(formatError(loadResult.error));
8988
+ process.exit(1);
8989
+ }
8990
+ if (spinner) {
8991
+ spinner.text = `Generating tests for ${gaps.length} gaps...`;
8992
+ }
8993
+ const renderer = createRenderer({ strict: false, allowUnresolved: true });
8994
+ const generatedTests = [];
8995
+ const errors = [];
8996
+ const gapsByCategory = /* @__PURE__ */ new Map();
8997
+ for (const gap of gaps) {
8998
+ const existing = gapsByCategory.get(gap.categoryId) ?? [];
8999
+ existing.push(gap);
9000
+ gapsByCategory.set(gap.categoryId, existing);
9001
+ }
9002
+ for (const [catId, categoryGaps] of gapsByCategory) {
9003
+ const categoryResult = store.get(catId);
9004
+ if (!categoryResult.success) {
9005
+ errors.push(`Category not found: ${catId}`);
9039
9006
  continue;
9040
9007
  }
9041
- let variables;
9042
- if (useAI) {
9043
- variables = await extractVariablesWithAI(gap, template.variables, {
9044
- provider: aiProvider
9045
- });
9046
- } else {
9047
- variables = extractVariablesFromGap(gap);
9048
- }
9049
- const renderResult = renderer.renderTemplate(template, variables);
9050
- if (!renderResult.success) {
9051
- errors.push(`Failed to render ${catId}: ${renderResult.error.message}`);
9052
- continue;
9008
+ const category = categoryResult.data;
9009
+ for (const gap of categoryGaps) {
9010
+ const gapExt = gap.filePath.split(".").pop() ?? "";
9011
+ const langMap = { py: "python", ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", go: "go", java: "java", rs: "rust" };
9012
+ const gapLang = langMap[gapExt];
9013
+ let template = category.testTemplates.find((t) => t.language === gapLang);
9014
+ if (!template) {
9015
+ template = category.testTemplates[0];
9016
+ }
9017
+ if (!template) {
9018
+ errors.push(`No templates available for ${catId}`);
9019
+ continue;
9020
+ }
9021
+ let variables;
9022
+ if (useAI) {
9023
+ variables = await extractVariablesWithAI(gap, template.variables, { provider: aiProvider });
9024
+ } else {
9025
+ variables = extractVariablesFromGap(gap);
9026
+ }
9027
+ const renderResult = renderer.renderTemplate(template, variables);
9028
+ if (!renderResult.success) {
9029
+ errors.push(`Failed to render ${catId}: ${renderResult.error.message}`);
9030
+ continue;
9031
+ }
9032
+ const suggestedPath = suggestTestPath(gap.filePath, template, cached.targetDirectory);
9033
+ generatedTests.push({ gap, category, template, result: renderResult.data, suggestedPath });
9053
9034
  }
9054
- const suggestedPath = suggestTestPath(gap.filePath, template, cached.targetDirectory);
9055
- generatedTests.push({
9056
- gap,
9057
- category,
9058
- template,
9059
- result: renderResult.data,
9060
- suggestedPath
9061
- });
9062
9035
  }
9063
- }
9064
- spinner?.stop();
9065
- if (outputFormat === "json") {
9066
- console.log(formatGeneratedJson(generatedTests));
9067
- } else {
9068
- console.log(formatGeneratedTerminal(generatedTests, cached.targetDirectory));
9069
- }
9070
- if (isVerbose && errors.length > 0) {
9071
- console.error(chalk5.yellow("\nWarnings:"));
9072
- for (const error of errors) {
9073
- console.error(chalk5.gray(` - ${error}`));
9036
+ spinner?.stop();
9037
+ if (outputFormat === "json") {
9038
+ console.log(formatGeneratedJson(generatedTests));
9039
+ } else {
9040
+ console.log(formatGeneratedTerminal(generatedTests, cached.targetDirectory));
9074
9041
  }
9075
- }
9076
- if (!dryRun) {
9077
- const outputDirOption = options["outputDir"];
9078
- const writeResult = await writeGeneratedTests(
9079
- generatedTests,
9080
- cached.targetDirectory,
9081
- outputDirOption
9082
- );
9083
- if (!writeResult.success) {
9084
- console.error(formatError(writeResult.error));
9085
- process.exit(1);
9042
+ if (isVerbose && errors.length > 0) {
9043
+ console.error(chalk7.yellow("\nWarnings:"));
9044
+ for (const error of errors) {
9045
+ console.error(chalk7.gray(` - ${error}`));
9046
+ }
9086
9047
  }
9087
- console.log(formatWriteSummary(writeResult.data, cached.targetDirectory));
9088
- if (writeResult.data.failed.length > 0) {
9089
- process.exit(1);
9048
+ if (!dryRun) {
9049
+ const outputDirOption = options["outputDir"];
9050
+ const writeResult = await writeGeneratedTests(generatedTests, cached.targetDirectory, outputDirOption);
9051
+ if (!writeResult.success) {
9052
+ console.error(formatError(writeResult.error));
9053
+ process.exit(1);
9054
+ }
9055
+ console.log(formatWriteSummary(writeResult.data, cached.targetDirectory));
9056
+ if (writeResult.data.failed.length > 0) {
9057
+ process.exit(1);
9058
+ }
9090
9059
  }
9060
+ process.exit(0);
9061
+ } catch (error) {
9062
+ spinner?.fail("Generation failed");
9063
+ console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9064
+ process.exit(1);
9091
9065
  }
9092
- process.exit(0);
9093
- } catch (error) {
9094
- spinner?.fail("Generation failed");
9095
- console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9096
- process.exit(1);
9097
- }
9098
- });
9066
+ });
9067
+ }
9068
+
9069
+ // src/cli/index.ts
9070
+ var program = new Command();
9071
+ program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
9072
+ registerAnalyzeCommand(program);
9073
+ registerGenerateCommand(program);
9099
9074
  program.command("explain").description("Get natural language explanations for detected gaps").option("-n, --top <count>", "Explain top N gaps by priority", "5").option("-c, --category <id>", "Explain gaps for specific category").option("-d, --domain <domain>", "Explain gaps for specific domain").option("--ai", "Use AI for detailed explanations (requires API key)").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json, markdown", "terminal").option("-v, --verbose", "Show more details").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
9100
9075
  const isQuiet = Boolean(options["quiet"]);
9101
9076
  const isVerbose = Boolean(options["verbose"]);
9077
+ const topN = parseInt(String(options["top"] ?? "5"), 10);
9078
+ const categoryFilter = options["category"];
9079
+ const domainFilter = options["domain"];
9102
9080
  const useAI = Boolean(options["ai"]);
9103
9081
  const aiProvider = String(options["aiProvider"] ?? "anthropic");
9104
9082
  const outputFormat = String(options["output"] ?? "terminal");
9105
- const topN = parseInt(String(options["top"] ?? "5"), 10);
9106
9083
  if (isQuiet) {
9107
9084
  logger.configure({ level: "error" });
9108
9085
  } else if (isVerbose) {
9109
9086
  logger.configure({ level: "debug" });
9110
9087
  }
9111
9088
  if (!["terminal", "json", "markdown"].includes(outputFormat)) {
9112
- console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown`)));
9089
+ console.error(formatError(new Error(`Invalid format: ${outputFormat}. Use: terminal, json, markdown`)));
9113
9090
  process.exit(1);
9114
9091
  }
9115
- const showSpinner = outputFormat === "terminal" && !isQuiet;
9116
- const spinner = showSpinner ? ora("Loading cached scan results...").start() : null;
9117
- try {
9118
- const projectRoot = process.cwd();
9119
- const cacheResult = await loadScanResults(projectRoot);
9120
- if (!cacheResult.success) {
9121
- spinner?.fail("No cached results");
9122
- console.error(formatError(cacheResult.error));
9123
- console.error(chalk5.yellow("\nRun `pinata analyze` first to scan for gaps."));
9092
+ const projectRoot = process.cwd();
9093
+ const cacheResult = await loadScanResults(projectRoot);
9094
+ if (!cacheResult.success) {
9095
+ console.error(formatError(cacheResult.error));
9096
+ console.error(chalk7.yellow("\nRun `pinata analyze` first to scan for gaps."));
9097
+ process.exit(1);
9098
+ }
9099
+ const cached = cacheResult.data;
9100
+ let gaps = cached.gaps;
9101
+ if (categoryFilter) {
9102
+ gaps = gaps.filter((g) => g.categoryId === categoryFilter);
9103
+ }
9104
+ if (domainFilter) {
9105
+ if (!RISK_DOMAINS.includes(domainFilter)) {
9106
+ console.error(formatError(new Error(`Invalid domain: ${domainFilter}`)));
9124
9107
  process.exit(1);
9125
9108
  }
9126
- const cached = cacheResult.data;
9127
- let gaps = cached.gaps;
9128
- const categoryFilter = options["category"];
9129
- const domainFilter = options["domain"];
9130
- if (categoryFilter) {
9131
- gaps = gaps.filter((g) => g.categoryId === categoryFilter);
9132
- }
9133
- if (domainFilter) {
9134
- if (!RISK_DOMAINS.includes(domainFilter)) {
9135
- spinner?.fail("Invalid domain");
9136
- console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid: ${RISK_DOMAINS.join(", ")}`)));
9137
- process.exit(1);
9109
+ gaps = gaps.filter((g) => g.domain === domainFilter);
9110
+ }
9111
+ gaps = gaps.slice(0, topN);
9112
+ if (gaps.length === 0) {
9113
+ console.log(chalk7.yellow("No gaps to explain."));
9114
+ process.exit(0);
9115
+ }
9116
+ let explanations;
9117
+ if (useAI) {
9118
+ const spinner = ora3("Generating AI explanations...").start();
9119
+ try {
9120
+ const aiConfig = { provider: aiProvider };
9121
+ const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9122
+ if (hasApiKey2(aiProvider)) {
9123
+ const key = getApiKey2(aiProvider);
9124
+ if (key) {
9125
+ aiConfig.apiKey = key;
9126
+ }
9138
9127
  }
9139
- gaps = gaps.filter((g) => g.domain === domainFilter);
9140
- }
9141
- if (gaps.length === 0) {
9142
- spinner?.succeed("No gaps to explain");
9143
- console.log(chalk5.yellow("\nNo gaps found matching the filters."));
9144
- process.exit(0);
9145
- }
9146
- gaps = gaps.sort((a, b) => b.priorityScore - a.priorityScore).slice(0, topN);
9147
- if (spinner) {
9148
- spinner.text = `Explaining ${gaps.length} gap(s)...`;
9149
- }
9150
- const explanations = [];
9151
- if (useAI) {
9152
- const ai = createAIService({ provider: aiProvider });
9153
- if (!ai.isConfigured()) {
9154
- spinner?.warn("AI not configured, using fallback explanations");
9155
- console.error(chalk5.yellow(`
9128
+ const resultMap = await explainGaps(gaps, void 0, aiConfig);
9129
+ explanations = gaps.map((g) => {
9130
+ const key = `${g.categoryId}:${g.filePath}:${g.lineStart}`;
9131
+ const result = resultMap.get(key);
9132
+ if (result?.success && result.data) {
9133
+ return result.data;
9134
+ }
9135
+ return generateFallbackExplanation(g);
9136
+ });
9137
+ spinner.succeed(`Generated ${explanations.length} explanations`);
9138
+ } catch (error) {
9139
+ spinner.fail("AI explanation failed");
9140
+ console.error(chalk7.yellow(`
9156
9141
  Set ${aiProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} for AI explanations.
9157
9142
  `));
9158
- for (const gap of gaps) {
9159
- explanations.push({
9160
- gap,
9161
- explanation: generateFallbackExplanation(gap)
9162
- });
9163
- }
9164
- } else {
9165
- for (const gap of gaps) {
9166
- const result = await explainGap(gap, void 0, { provider: aiProvider });
9167
- if (result.success && result.data) {
9168
- explanations.push({ gap, explanation: result.data });
9169
- } else {
9170
- explanations.push({
9171
- gap,
9172
- explanation: generateFallbackExplanation(gap)
9173
- });
9174
- }
9175
- }
9176
- }
9177
- } else {
9178
- for (const gap of gaps) {
9179
- explanations.push({
9180
- gap,
9181
- explanation: generateFallbackExplanation(gap)
9182
- });
9183
- }
9143
+ explanations = gaps.map((g) => generateFallbackExplanation(g));
9184
9144
  }
9185
- spinner?.stop();
9186
- if (outputFormat === "json") {
9187
- console.log(JSON.stringify(explanations.map((e) => ({
9188
- gap: {
9189
- categoryId: e.gap.categoryId,
9190
- categoryName: e.gap.categoryName,
9191
- filePath: e.gap.filePath,
9192
- lineStart: e.gap.lineStart,
9193
- severity: e.gap.severity,
9194
- confidence: e.gap.confidence,
9195
- codeSnippet: e.gap.codeSnippet
9196
- },
9197
- explanation: e.explanation
9198
- })), null, 2));
9199
- } else if (outputFormat === "markdown") {
9200
- console.log(`# Gap Explanations
9201
- `);
9202
- console.log(`Generated ${explanations.length} explanation(s).
9203
- `);
9204
- for (const { gap, explanation } of explanations) {
9205
- console.log(`## ${gap.categoryName}
9206
- `);
9207
- console.log(`**File:** \`${gap.filePath}:${gap.lineStart}\`
9208
- `);
9209
- console.log(`**Severity:** ${gap.severity} | **Confidence:** ${gap.confidence}
9210
- `);
9211
- console.log(`### Summary
9212
- ${explanation.summary}
9213
- `);
9214
- console.log(`### Explanation
9215
- ${explanation.explanation}
9216
- `);
9217
- console.log(`### Risk
9218
- ${explanation.risk}
9145
+ } else {
9146
+ explanations = gaps.map((g) => generateFallbackExplanation(g));
9147
+ }
9148
+ if (outputFormat === "json") {
9149
+ const output = gaps.map((g, i) => ({ gap: { categoryId: g.categoryId, severity: g.severity, filePath: g.filePath, lineStart: g.lineStart }, ...explanations[i] }));
9150
+ console.log(JSON.stringify(output, null, 2));
9151
+ } else if (outputFormat === "markdown") {
9152
+ console.log("# Gap Explanations\n");
9153
+ for (let i = 0; i < explanations.length; i++) {
9154
+ const exp = explanations[i];
9155
+ const gap = gaps[i];
9156
+ console.log(`## ${exp.summary}
9219
9157
  `);
9220
- console.log(`### How to Fix
9221
- ${explanation.remediation}
9158
+ console.log(`**Severity**: ${gap.severity} | **Category**: ${gap.categoryId}
9222
9159
  `);
9223
- if (explanation.safeExample) {
9224
- console.log(`### Safe Example
9225
- \`\`\`
9226
- ${explanation.safeExample}
9227
- \`\`\`
9160
+ console.log(exp.explanation);
9161
+ if (exp.remediation) {
9162
+ console.log(`
9163
+ **Remediation**: ${exp.remediation}
9228
9164
  `);
9229
- }
9230
- console.log("---\n");
9231
9165
  }
9232
- } else {
9166
+ console.log("---\n");
9167
+ }
9168
+ } else {
9169
+ console.log();
9170
+ for (let i = 0; i < explanations.length; i++) {
9171
+ const exp = explanations[i];
9172
+ const gap = gaps[i];
9173
+ const severityColor = gap.severity === "critical" ? chalk7.red : gap.severity === "high" ? chalk7.yellow : chalk7.blue;
9174
+ console.log(`${severityColor.bold(`[${gap.severity.toUpperCase()}]`)} ${chalk7.bold(exp.summary)}`);
9175
+ console.log(chalk7.gray(` Category: ${gap.categoryId} | ${gap.filePath}:${gap.lineStart}`));
9233
9176
  console.log();
9234
- console.log(chalk5.bold.cyan("Gap Explanations"));
9235
- console.log(chalk5.gray("\u2500".repeat(60)));
9236
- for (const { gap, explanation } of explanations) {
9237
- console.log();
9238
- console.log(chalk5.bold.white(gap.categoryName));
9239
- console.log(chalk5.gray(` ${gap.filePath}:${gap.lineStart}`));
9240
- const severityColor = gap.severity === "critical" ? chalk5.red : gap.severity === "high" ? chalk5.yellow : chalk5.blue;
9241
- console.log(` ${severityColor(gap.severity)} | ${gap.confidence} confidence`);
9242
- console.log();
9243
- console.log(chalk5.cyan(" Summary:"));
9244
- console.log(` ${explanation.summary}`);
9245
- if (isVerbose) {
9246
- console.log();
9247
- console.log(chalk5.cyan(" Explanation:"));
9248
- for (const line of explanation.explanation.split("\n")) {
9249
- console.log(` ${line}`);
9250
- }
9251
- }
9252
- console.log();
9253
- console.log(chalk5.red(" Risk:"));
9254
- console.log(` ${explanation.risk}`);
9255
- console.log();
9256
- console.log(chalk5.green(" How to Fix:"));
9257
- for (const line of explanation.remediation.split("\n")) {
9258
- console.log(` ${line}`);
9259
- }
9260
- if (explanation.safeExample) {
9261
- console.log();
9262
- console.log(chalk5.cyan(" Safe Example:"));
9263
- console.log(chalk5.gray(` ${explanation.safeExample}`));
9264
- }
9177
+ for (const line of exp.explanation.split("\n")) {
9178
+ console.log(` ${line}`);
9179
+ }
9180
+ if (exp.remediation) {
9265
9181
  console.log();
9266
- console.log(chalk5.gray("\u2500".repeat(60)));
9182
+ console.log(chalk7.green(` Fix: ${exp.remediation}`));
9267
9183
  }
9184
+ console.log();
9268
9185
  }
9269
- process.exit(0);
9270
- } catch (error) {
9271
- spinner?.fail("Explanation failed");
9272
- console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9273
- process.exit(1);
9274
9186
  }
9275
9187
  });
9276
9188
  program.command("suggest-patterns").description("Use AI to suggest new detection patterns based on code samples").requiredOption("-c, --category <id>", "Category to suggest patterns for").requiredOption("-l, --language <lang>", "Language of the code samples").option("-f, --file <path>", "File containing vulnerable code samples (one per line)").option("--code <snippet>", "Vulnerable code snippet (can be specified multiple times)", (v, a) => [...a, v], []).option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, yaml, json", "terminal").action(async (options) => {
@@ -9278,96 +9190,73 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
9278
9190
  const language = String(options["language"]);
9279
9191
  const aiProvider = String(options["aiProvider"] ?? "anthropic");
9280
9192
  const outputFormat = String(options["output"] ?? "terminal");
9281
- const codeSnippets = options["code"];
9282
9193
  const filePath = options["file"];
9283
- let vulnerableCode = [...codeSnippets];
9194
+ const codeSnippets = options["code"] ?? [];
9195
+ const samples = [...codeSnippets];
9284
9196
  if (filePath) {
9197
+ const { readFile: readFile7 } = await import('fs/promises');
9285
9198
  try {
9286
- const { readFile: readFile7 } = await import('fs/promises');
9287
- const content = await readFile7(filePath, "utf-8");
9288
- vulnerableCode = [...vulnerableCode, ...content.split("\n---\n").filter(Boolean)];
9199
+ const content = await readFile7(resolve(filePath), "utf-8");
9200
+ samples.push(...content.split("\n---\n").filter((s) => s.trim()));
9289
9201
  } catch (error) {
9290
9202
  console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
9291
9203
  process.exit(1);
9292
9204
  }
9293
9205
  }
9294
- if (vulnerableCode.length === 0) {
9206
+ if (samples.length === 0) {
9295
9207
  console.error(formatError(new Error("Provide code samples via --code or --file")));
9296
9208
  process.exit(1);
9297
9209
  }
9298
- const spinner = ora("Generating pattern suggestions...").start();
9210
+ const spinner = ora3("Generating pattern suggestions with AI...").start();
9299
9211
  try {
9212
+ const aiConfig = { provider: aiProvider };
9213
+ const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9214
+ if (hasApiKey2(aiProvider)) {
9215
+ const key = getApiKey2(aiProvider);
9216
+ if (key) {
9217
+ aiConfig.apiKey = key;
9218
+ }
9219
+ }
9300
9220
  const result = await suggestPatterns(
9301
- {
9302
- category: categoryId,
9303
- language,
9304
- vulnerableCode,
9305
- maxSuggestions: 5
9306
- },
9307
- { provider: aiProvider }
9221
+ { category: categoryId, vulnerableCode: samples, language },
9222
+ aiConfig
9308
9223
  );
9309
- spinner.stop();
9310
- if (!result.success) {
9311
- console.error(formatError(new Error(result.error ?? "Failed to generate patterns")));
9224
+ if (!result.success || !result.data) {
9225
+ spinner.fail("Pattern suggestion failed");
9226
+ console.error(chalk7.red(result.error ?? "Unknown error"));
9312
9227
  process.exit(1);
9313
9228
  }
9314
- const { suggestions, rejected } = result.data ?? { suggestions: [], rejected: [] };
9229
+ const suggestions = result.data.suggestions;
9230
+ spinner.succeed(`Generated ${suggestions.length} pattern suggestions`);
9315
9231
  if (outputFormat === "json") {
9316
- console.log(JSON.stringify({ suggestions, rejected }, null, 2));
9232
+ console.log(JSON.stringify(suggestions, null, 2));
9317
9233
  } else if (outputFormat === "yaml") {
9318
- console.log(`# Suggested patterns for ${categoryId}
9319
- `);
9320
- console.log(`detectionPatterns:`);
9321
- for (const suggestion of suggestions) {
9322
- const escapedPattern = suggestion.pattern.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
9323
- console.log(` - id: ${suggestion.id}`);
9324
- console.log(` type: regex`);
9325
- console.log(` language: ${language}`);
9326
- console.log(` pattern: "${escapedPattern}"`);
9327
- console.log(` confidence: ${suggestion.confidence}`);
9328
- console.log(` description: ${suggestion.description}`);
9234
+ for (const s of suggestions) {
9235
+ console.log("---");
9236
+ console.log(`id: ${s.id}`);
9237
+ console.log(`pattern: "${s.pattern}"`);
9238
+ console.log(`confidence: ${s.confidence}`);
9239
+ console.log(`description: "${s.description}"`);
9240
+ console.log(`matchExample: "${s.matchExample}"`);
9241
+ console.log(`safeExample: "${s.safeExample}"`);
9329
9242
  console.log();
9330
9243
  }
9331
9244
  } else {
9332
9245
  console.log();
9333
- console.log(chalk5.bold.cyan("Pattern Suggestions"));
9334
- console.log(chalk5.gray("\u2500".repeat(60)));
9335
- if (suggestions.length === 0) {
9336
- console.log(chalk5.yellow("\nNo valid patterns could be generated."));
9337
- } else {
9338
- for (const suggestion of suggestions) {
9339
- console.log();
9340
- console.log(chalk5.bold.white(suggestion.id));
9341
- console.log(chalk5.gray(` ${suggestion.description}`));
9342
- console.log();
9343
- console.log(chalk5.cyan(" Pattern:"));
9344
- console.log(` ${suggestion.pattern}`);
9345
- console.log();
9346
- console.log(chalk5.cyan(" Confidence:") + ` ${suggestion.confidence}`);
9347
- console.log();
9348
- console.log(chalk5.green(" Would match:"));
9349
- console.log(chalk5.gray(` ${suggestion.matchExample}`));
9350
- console.log();
9351
- console.log(chalk5.red(" Should NOT match:"));
9352
- console.log(chalk5.gray(` ${suggestion.safeExample}`));
9353
- console.log();
9354
- console.log(chalk5.cyan(" Reasoning:"));
9355
- console.log(` ${suggestion.reasoning}`);
9356
- console.log();
9357
- console.log(chalk5.gray("\u2500".repeat(60)));
9358
- }
9359
- }
9360
- if (rejected.length > 0) {
9246
+ console.log(chalk7.bold(`Suggested Patterns for ${categoryId}`));
9247
+ console.log();
9248
+ for (const s of suggestions) {
9249
+ const confColor = s.confidence === "high" ? chalk7.green : s.confidence === "medium" ? chalk7.yellow : chalk7.red;
9250
+ console.log(` ${chalk7.cyan(s.id)} [${confColor(s.confidence)}]`);
9251
+ console.log(` Pattern: ${chalk7.gray(s.pattern)}`);
9252
+ console.log(` ${s.description}`);
9253
+ console.log(` Match: ${chalk7.gray(s.matchExample.slice(0, 80))}`);
9254
+ console.log(` Safe: ${chalk7.gray(s.safeExample.slice(0, 80))}`);
9361
9255
  console.log();
9362
- console.log(chalk5.yellow.bold(`Rejected ${rejected.length} pattern(s):`));
9363
- for (const r of rejected) {
9364
- console.log(chalk5.gray(` - ${r.pattern.slice(0, 40)}... : ${r.reason}`));
9365
- }
9366
9256
  }
9367
9257
  }
9368
- process.exit(0);
9369
9258
  } catch (error) {
9370
- spinner.fail("Pattern generation failed");
9259
+ spinner.fail("Pattern suggestion failed");
9371
9260
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9372
9261
  process.exit(1);
9373
9262
  }
@@ -9422,9 +9311,7 @@ program.command("search <query>").description("Search category taxonomy by name,
9422
9311
  }
9423
9312
  const langFilter = options["language"];
9424
9313
  if (langFilter) {
9425
- results = results.filter(
9426
- (cat) => cat.applicableLanguages.includes(langFilter)
9427
- );
9314
+ results = results.filter((cat) => cat.applicableLanguages.includes(langFilter));
9428
9315
  }
9429
9316
  if (outputFormat === "json") {
9430
9317
  console.log(JSON.stringify(results, null, 2));
@@ -9446,19 +9333,18 @@ ${cat.description}
9446
9333
  }
9447
9334
  } else {
9448
9335
  console.log();
9449
- console.log(chalk5.bold(`Search Results for "${query}"`));
9450
- console.log(chalk5.gray(`Found ${results.length} matching categories.`));
9336
+ console.log(chalk7.bold(`Search Results for "${query}"`));
9337
+ console.log(chalk7.gray(`Found ${results.length} matching categories.`));
9451
9338
  console.log();
9452
9339
  if (results.length === 0) {
9453
- console.log(chalk5.yellow("No categories match your search."));
9454
- console.log(chalk5.gray("Try a different query or broaden your filters."));
9340
+ console.log(chalk7.yellow("No categories match your search."));
9455
9341
  } else {
9456
9342
  for (const cat of results) {
9457
- const domainColor = cat.domain === "security" ? chalk5.red : chalk5.blue;
9458
- console.log(` ${chalk5.cyan(cat.id)} - ${chalk5.bold(cat.name)}`);
9343
+ const domainColor = cat.domain === "security" ? chalk7.red : chalk7.blue;
9344
+ console.log(` ${chalk7.cyan(cat.id)} - ${chalk7.bold(cat.name)}`);
9459
9345
  console.log(` ${domainColor(cat.domain)} | ${cat.level} | ${cat.priority}`);
9460
9346
  if (options["verbose"]) {
9461
- console.log(` ${chalk5.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
9347
+ console.log(` ${chalk7.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
9462
9348
  }
9463
9349
  console.log();
9464
9350
  }
@@ -9492,21 +9378,17 @@ program.command("list").description("List all categories").option("-d, --domain
9492
9378
  process.exit(1);
9493
9379
  }
9494
9380
  const priorityFilter = options["priority"];
9495
- const validPriorities = ["P0", "P1", "P2"];
9496
- if (priorityFilter !== void 0 && !validPriorities.includes(priorityFilter)) {
9381
+ if (priorityFilter !== void 0 && !["P0", "P1", "P2"].includes(priorityFilter)) {
9497
9382
  console.error(formatError(new Error(`Invalid priority: ${priorityFilter}. Use: P0, P1, P2`)));
9498
9383
  process.exit(1);
9499
9384
  }
9500
- logger.debug("Loading categories...");
9501
9385
  const store = createCategoryStore();
9502
9386
  const definitionsPath = getDefinitionsPath();
9503
- logger.debug(`Loading from: ${definitionsPath}`);
9504
9387
  const loadResult = await store.loadFromDirectory(definitionsPath);
9505
9388
  if (!loadResult.success) {
9506
9389
  console.error(formatError(loadResult.error));
9507
9390
  process.exit(1);
9508
9391
  }
9509
- logger.debug(`Loaded ${loadResult.data} categories`);
9510
9392
  const filter = {};
9511
9393
  if (domainFilter) {
9512
9394
  filter.domain = domainFilter;
@@ -9530,54 +9412,42 @@ program.command("init").description("Initialize Pinata configuration in project"
9530
9412
  const configPath = resolve(process.cwd(), ".pinata.yml");
9531
9413
  const cacheDir = resolve(process.cwd(), ".pinata");
9532
9414
  if (existsSync(configPath) && !options["force"]) {
9533
- console.log(chalk5.yellow("Configuration file already exists at .pinata.yml"));
9534
- console.log(chalk5.gray("Use --force to overwrite."));
9415
+ console.log(chalk7.yellow("Configuration file already exists at .pinata.yml"));
9416
+ console.log(chalk7.gray("Use --force to overwrite."));
9535
9417
  process.exit(0);
9536
9418
  }
9537
9419
  const defaultConfig = `# Pinata Configuration
9538
9420
  # https://github.com/pinata/pinata
9539
9421
 
9540
- # Paths to analyze
9541
9422
  include:
9542
9423
  - "src/**/*.ts"
9543
9424
  - "src/**/*.tsx"
9544
9425
  - "src/**/*.py"
9545
9426
  - "src/**/*.js"
9546
9427
 
9547
- # Paths to exclude from analysis
9548
9428
  exclude:
9549
9429
  - "node_modules/**"
9550
9430
  - "dist/**"
9551
9431
  - "build/**"
9552
9432
  - "**/*.test.ts"
9553
9433
  - "**/*.spec.ts"
9554
- - "**/test/**"
9555
- - "**/tests/**"
9556
- - "**/__tests__/**"
9557
9434
 
9558
- # Risk domains to analyze
9559
- # Options: security, data, concurrency, input, resource, reliability, performance, platform, business, compliance
9560
9435
  domains:
9561
9436
  - security
9562
9437
  - data
9563
9438
  - concurrency
9564
9439
  - input
9565
9440
 
9566
- # Minimum severity to report
9567
- # Options: critical, high, medium, low
9568
9441
  minSeverity: medium
9569
9442
 
9570
- # Output configuration
9571
9443
  output:
9572
- format: terminal # terminal, json, markdown, sarif, html
9444
+ format: terminal
9573
9445
  color: true
9574
9446
 
9575
- # Test generation settings
9576
9447
  generate:
9577
9448
  outputDir: tests/generated
9578
- framework: auto # auto, pytest, jest, vitest, mocha
9449
+ framework: auto
9579
9450
 
9580
- # Fail CI if gaps exceed thresholds
9581
9451
  thresholds:
9582
9452
  critical: 0
9583
9453
  high: 5
@@ -9586,25 +9456,23 @@ thresholds:
9586
9456
  const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
9587
9457
  try {
9588
9458
  await writeFileAsync(configPath, defaultConfig, "utf8");
9589
- console.log(chalk5.green("Created .pinata.yml"));
9459
+ console.log(chalk7.green("Created .pinata.yml"));
9590
9460
  await mkdir4(cacheDir, { recursive: true });
9591
- console.log(chalk5.green("Created .pinata/ directory"));
9461
+ console.log(chalk7.green("Created .pinata/ directory"));
9592
9462
  const gitignorePath = resolve(process.cwd(), ".gitignore");
9593
9463
  if (existsSync(gitignorePath)) {
9594
9464
  const { readFile: readFile7, appendFile } = await import('fs/promises');
9595
9465
  const gitignore = await readFile7(gitignorePath, "utf8");
9596
9466
  if (!gitignore.includes(".pinata/")) {
9597
9467
  await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
9598
- console.log(chalk5.green("Added .pinata/ to .gitignore"));
9468
+ console.log(chalk7.green("Added .pinata/ to .gitignore"));
9599
9469
  }
9600
9470
  }
9601
9471
  console.log();
9602
- console.log(chalk5.bold("Pinata initialized successfully!"));
9603
- console.log();
9604
- console.log("Next steps:");
9605
- console.log(chalk5.gray(" 1. Review and customize .pinata.yml"));
9606
- console.log(chalk5.gray(" 2. Run: pinata analyze"));
9607
- console.log(chalk5.gray(" 3. Generate tests: pinata generate"));
9472
+ console.log(chalk7.bold("Pinata initialized successfully!"));
9473
+ console.log(chalk7.gray(" 1. Review and customize .pinata.yml"));
9474
+ console.log(chalk7.gray(" 2. Run: pinata analyze"));
9475
+ console.log(chalk7.gray(" 3. Generate tests: pinata generate"));
9608
9476
  } catch (error) {
9609
9477
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9610
9478
  process.exit(1);
@@ -9617,18 +9485,15 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9617
9485
  const checkAge = Boolean(options["checkAge"]);
9618
9486
  const strictMode = Boolean(options["strict"]);
9619
9487
  const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
9620
- console.log(chalk5.bold("\nPinata Dependency Audit\n"));
9488
+ console.log(chalk7.bold("\nPinata Dependency Audit\n"));
9621
9489
  if (!existsSync(packagePath)) {
9622
- console.error(chalk5.red(`Error: ${packagePath} not found`));
9490
+ console.error(chalk7.red(`Error: ${packagePath} not found`));
9623
9491
  process.exit(1);
9624
9492
  }
9625
9493
  const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
9626
- const allDeps = {
9627
- ...packageJson.dependencies,
9628
- ...packageJson.devDependencies
9629
- };
9494
+ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
9630
9495
  const packages = Object.keys(allDeps);
9631
- console.log(chalk5.gray(`Found ${packages.length} dependencies
9496
+ console.log(chalk7.gray(`Found ${packages.length} dependencies
9632
9497
  `));
9633
9498
  const issues = [];
9634
9499
  const KNOWN_MALWARE = /* @__PURE__ */ new Set([
@@ -9651,56 +9516,31 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9651
9516
  ]);
9652
9517
  for (const pkg of packages) {
9653
9518
  if (KNOWN_MALWARE.has(pkg)) {
9654
- issues.push({
9655
- pkg,
9656
- severity: "critical",
9657
- message: "Known malicious/compromised package (Shai-Hulud/typosquat)"
9658
- });
9519
+ issues.push({ pkg, severity: "critical", message: "Known malicious/compromised package (Shai-Hulud/typosquat)" });
9659
9520
  }
9660
9521
  }
9661
9522
  for (const [pkg, version] of Object.entries(allDeps)) {
9662
9523
  if (version?.startsWith("^")) {
9663
- issues.push({
9664
- pkg,
9665
- severity: "warning",
9666
- message: `Unpinned version (${version}) - allows minor updates`
9667
- });
9524
+ issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows minor updates` });
9668
9525
  } else if (version?.startsWith("~")) {
9669
- issues.push({
9670
- pkg,
9671
- severity: "warning",
9672
- message: `Unpinned version (${version}) - allows patch updates`
9673
- });
9526
+ issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows patch updates` });
9674
9527
  } else if (version === "*" || version === "latest") {
9675
- issues.push({
9676
- pkg,
9677
- severity: "critical",
9678
- message: `Extremely dangerous version (${version}) - allows any version`
9679
- });
9528
+ issues.push({ pkg, severity: "critical", message: `Extremely dangerous version (${version}) - allows any version` });
9680
9529
  }
9681
9530
  }
9682
9531
  if (checkRegistry || doAllChecks) {
9683
- const spinner = ora("Checking npm registry...").start();
9532
+ const spinner = ora3("Checking npm registry...").start();
9684
9533
  for (const pkg of packages.slice(0, 50)) {
9685
9534
  try {
9686
9535
  const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`);
9687
9536
  if (response.status === 404) {
9688
- issues.push({
9689
- pkg,
9690
- severity: "critical",
9691
- message: "Package NOT FOUND in npm registry (slopsquatting risk)"
9692
- });
9537
+ issues.push({ pkg, severity: "critical", message: "Package NOT FOUND in npm registry (slopsquatting risk)" });
9693
9538
  } else if (response.ok) {
9694
9539
  const data = await response.json();
9695
9540
  if ((checkAge || doAllChecks) && data.time?.created) {
9696
- const created = new Date(data.time.created);
9697
- const ageInDays = (Date.now() - created.getTime()) / (1e3 * 60 * 60 * 24);
9541
+ const ageInDays = (Date.now() - new Date(data.time.created).getTime()) / (1e3 * 60 * 60 * 24);
9698
9542
  if (ageInDays < 30) {
9699
- issues.push({
9700
- pkg,
9701
- severity: "warning",
9702
- message: `Very new package (${Math.floor(ageInDays)} days old)`
9703
- });
9543
+ issues.push({ pkg, severity: "warning", message: `Very new package (${Math.floor(ageInDays)} days old)` });
9704
9544
  }
9705
9545
  }
9706
9546
  }
@@ -9712,24 +9552,24 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9712
9552
  const criticals = issues.filter((i) => i.severity === "critical");
9713
9553
  const warnings = issues.filter((i) => i.severity === "warning");
9714
9554
  if (criticals.length > 0) {
9715
- console.log(chalk5.red.bold(`
9555
+ console.log(chalk7.red.bold(`
9716
9556
  Critical Issues (${criticals.length}):`));
9717
9557
  for (const issue of criticals) {
9718
- console.log(chalk5.red(` \u2717 ${issue.pkg}: ${issue.message}`));
9558
+ console.log(chalk7.red(` \u2717 ${issue.pkg}: ${issue.message}`));
9719
9559
  }
9720
9560
  }
9721
9561
  if (warnings.length > 0) {
9722
- console.log(chalk5.yellow.bold(`
9562
+ console.log(chalk7.yellow.bold(`
9723
9563
  Warnings (${warnings.length}):`));
9724
9564
  for (const issue of warnings.slice(0, 20)) {
9725
- console.log(chalk5.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
9565
+ console.log(chalk7.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
9726
9566
  }
9727
9567
  if (warnings.length > 20) {
9728
- console.log(chalk5.gray(` ... and ${warnings.length - 20} more`));
9568
+ console.log(chalk7.gray(` ... and ${warnings.length - 20} more`));
9729
9569
  }
9730
9570
  }
9731
9571
  if (issues.length === 0) {
9732
- console.log(chalk5.green("\u2713 No dependency issues found"));
9572
+ console.log(chalk7.green("\u2713 No dependency issues found"));
9733
9573
  }
9734
9574
  console.log();
9735
9575
  if (criticals.length > 0 || strictMode && warnings.length > 0) {
@@ -9742,7 +9582,7 @@ program.command("feedback").description("View pattern performance feedback (Laye
9742
9582
  const shouldReset = Boolean(options["reset"]);
9743
9583
  if (shouldReset) {
9744
9584
  await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
9745
- console.log(chalk5.green("Feedback data reset."));
9585
+ console.log(chalk7.green("Feedback data reset."));
9746
9586
  return;
9747
9587
  }
9748
9588
  const state = await loadFeedback2();
@@ -9754,20 +9594,20 @@ program.command("feedback").description("View pattern performance feedback (Laye
9754
9594
  console.log(generateReport2(state));
9755
9595
  return;
9756
9596
  }
9757
- console.log(chalk5.bold("\nPinata Feedback Report\n"));
9597
+ console.log(chalk7.bold("\nPinata Feedback Report\n"));
9758
9598
  console.log(`Total scans: ${state.totalScans}`);
9759
9599
  console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
9760
9600
  if (state.totalScans === 0) {
9761
- console.log(chalk5.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
9601
+ console.log(chalk7.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
9762
9602
  return;
9763
9603
  }
9764
9604
  const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
9765
9605
  if (patterns.length > 0) {
9766
- console.log(chalk5.bold("\nPattern Performance:"));
9606
+ console.log(chalk7.bold("\nPattern Performance:"));
9767
9607
  for (const p of patterns.slice(0, 15)) {
9768
9608
  const total = p.confirmedCount + p.unconfirmedCount;
9769
9609
  const precisionPct = (p.precision * 100).toFixed(0);
9770
- const color = p.precision >= 0.7 ? chalk5.green : p.precision >= 0.4 ? chalk5.yellow : chalk5.red;
9610
+ const color = p.precision >= 0.7 ? chalk7.green : p.precision >= 0.4 ? chalk7.yellow : chalk7.red;
9771
9611
  console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
9772
9612
  }
9773
9613
  }
@@ -9789,76 +9629,74 @@ Examples:
9789
9629
  case "anthropic-api-key": {
9790
9630
  const validation = validateApiKey2("anthropic", value);
9791
9631
  if (!validation.valid) {
9792
- console.log(chalk5.red(`Invalid API key: ${validation.error}`));
9632
+ console.log(chalk7.red(`Invalid API key: ${validation.error}`));
9793
9633
  process.exit(1);
9794
9634
  }
9795
9635
  setConfigValue2("anthropicApiKey", value);
9796
- console.log(chalk5.green(`Anthropic API key set: ${maskApiKey2(value)}`));
9636
+ console.log(chalk7.green(`Anthropic API key set: ${maskApiKey2(value)}`));
9797
9637
  break;
9798
9638
  }
9799
9639
  case "openai-api-key": {
9800
9640
  const validation = validateApiKey2("openai", value);
9801
9641
  if (!validation.valid) {
9802
- console.log(chalk5.red(`Invalid API key: ${validation.error}`));
9642
+ console.log(chalk7.red(`Invalid API key: ${validation.error}`));
9803
9643
  process.exit(1);
9804
9644
  }
9805
9645
  setConfigValue2("openaiApiKey", value);
9806
- console.log(chalk5.green(`OpenAI API key set: ${maskApiKey2(value)}`));
9646
+ console.log(chalk7.green(`OpenAI API key set: ${maskApiKey2(value)}`));
9807
9647
  break;
9808
9648
  }
9809
9649
  case "default-provider": {
9810
9650
  if (value !== "anthropic" && value !== "openai") {
9811
- console.log(chalk5.red("Provider must be 'anthropic' or 'openai'"));
9651
+ console.log(chalk7.red("Provider must be 'anthropic' or 'openai'"));
9812
9652
  process.exit(1);
9813
9653
  }
9814
9654
  setConfigValue2("defaultProvider", value);
9815
- console.log(chalk5.green(`Default provider set to: ${value}`));
9655
+ console.log(chalk7.green(`Default provider set to: ${value}`));
9816
9656
  break;
9817
9657
  }
9818
9658
  default:
9819
- console.log(chalk5.red(`Unknown config key: ${key}`));
9820
- console.log(chalk5.gray("Run 'pinata config set --help' for available keys"));
9659
+ console.log(chalk7.red(`Unknown config key: ${key}`));
9660
+ console.log(chalk7.gray("Run 'pinata config set --help' for available keys"));
9821
9661
  process.exit(1);
9822
9662
  }
9823
- console.log(chalk5.gray(`Config stored at: ${getConfigPath2()}`));
9663
+ console.log(chalk7.gray(`Config stored at: ${getConfigPath2()}`));
9824
9664
  });
9825
9665
  config.command("get <key>").description("Get a configuration value").action(async (key) => {
9826
9666
  const { loadConfig: loadConfig2, maskApiKey: maskApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9827
9667
  const cfg = loadConfig2();
9828
9668
  switch (key) {
9829
9669
  case "anthropic-api-key":
9830
- console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk5.gray("(not set)"));
9670
+ console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk7.gray("(not set)"));
9831
9671
  break;
9832
9672
  case "openai-api-key":
9833
- console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk5.gray("(not set)"));
9673
+ console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk7.gray("(not set)"));
9834
9674
  break;
9835
9675
  case "default-provider":
9836
- console.log(cfg.defaultProvider ?? chalk5.gray("anthropic (default)"));
9676
+ console.log(cfg.defaultProvider ?? chalk7.gray("anthropic (default)"));
9837
9677
  break;
9838
9678
  default:
9839
- console.log(chalk5.red(`Unknown config key: ${key}`));
9679
+ console.log(chalk7.red(`Unknown config key: ${key}`));
9840
9680
  process.exit(1);
9841
9681
  }
9842
9682
  });
9843
9683
  config.command("list").description("List all configuration values").action(async () => {
9844
9684
  const { loadConfig: loadConfig2, maskApiKey: maskApiKey2, getConfigPath: getConfigPath2, hasApiKey: hasApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9845
9685
  const cfg = loadConfig2();
9846
- console.log(chalk5.bold("Pinata Configuration"));
9847
- console.log(chalk5.gray(`Config file: ${getConfigPath2()}`));
9686
+ console.log(chalk7.bold("Pinata Configuration"));
9687
+ console.log(chalk7.gray(`Config file: ${getConfigPath2()}`));
9848
9688
  console.log();
9849
9689
  console.log("AI Providers:");
9850
- const anthropicStatus = hasApiKey2("anthropic") ? chalk5.green("configured") : chalk5.gray("not set");
9851
- const openaiStatus = hasApiKey2("openai") ? chalk5.green("configured") : chalk5.gray("not set");
9852
- console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ? chalk5.gray(`(${maskApiKey2(cfg.anthropicApiKey)})`) : ""}`);
9853
- console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ? chalk5.gray(`(${maskApiKey2(cfg.openaiApiKey)})`) : ""}`);
9690
+ const anthropicStatus = hasApiKey2("anthropic") ? chalk7.green("configured") : chalk7.gray("not set");
9691
+ const openaiStatus = hasApiKey2("openai") ? chalk7.green("configured") : chalk7.gray("not set");
9692
+ console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ? chalk7.gray(`(${maskApiKey2(cfg.anthropicApiKey)})`) : ""}`);
9693
+ console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ? chalk7.gray(`(${maskApiKey2(cfg.openaiApiKey)})`) : ""}`);
9854
9694
  console.log(` Default provider: ${cfg.defaultProvider ?? "anthropic"}`);
9855
9695
  console.log();
9856
9696
  if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
9857
- console.log(chalk5.yellow("No AI provider configured."));
9858
- console.log(chalk5.gray("To use AI features (explain, suggest-patterns, --ai flag):"));
9859
- console.log(chalk5.gray(" pinata config set anthropic-api-key sk-ant-xxx"));
9860
- console.log(chalk5.gray(" # or"));
9861
- console.log(chalk5.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
9697
+ console.log(chalk7.yellow("No AI provider configured."));
9698
+ console.log(chalk7.gray(" pinata config set anthropic-api-key sk-ant-xxx"));
9699
+ console.log(chalk7.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
9862
9700
  }
9863
9701
  });
9864
9702
  config.command("unset <key>").description("Remove a configuration value").action(async (key) => {
@@ -9866,18 +9704,18 @@ config.command("unset <key>").description("Remove a configuration value").action
9866
9704
  switch (key) {
9867
9705
  case "anthropic-api-key":
9868
9706
  deleteConfigValue2("anthropicApiKey");
9869
- console.log(chalk5.green("Anthropic API key removed"));
9707
+ console.log(chalk7.green("Anthropic API key removed"));
9870
9708
  break;
9871
9709
  case "openai-api-key":
9872
9710
  deleteConfigValue2("openaiApiKey");
9873
- console.log(chalk5.green("OpenAI API key removed"));
9711
+ console.log(chalk7.green("OpenAI API key removed"));
9874
9712
  break;
9875
9713
  case "default-provider":
9876
9714
  deleteConfigValue2("defaultProvider");
9877
- console.log(chalk5.green("Default provider reset to: anthropic"));
9715
+ console.log(chalk7.green("Default provider reset to: anthropic"));
9878
9716
  break;
9879
9717
  default:
9880
- console.log(chalk5.red(`Unknown config key: ${key}`));
9718
+ console.log(chalk7.red(`Unknown config key: ${key}`));
9881
9719
  process.exit(1);
9882
9720
  }
9883
9721
  });
@@ -9885,18 +9723,13 @@ var auth = program.command("auth").description("Manage API key authentication");
9885
9723
  auth.command("login").description("Set API key for Pinata Cloud").option("-k, --key <key>", "API key (or set PINATA_API_KEY env var)").action(async (options) => {
9886
9724
  const apiKey = options["key"] ?? process.env["PINATA_API_KEY"];
9887
9725
  if (!apiKey) {
9888
- console.log(chalk5.yellow("No API key provided."));
9889
- console.log();
9890
- console.log("Provide an API key using one of:");
9891
- console.log(chalk5.gray(" pinata auth login --key <your-api-key>"));
9892
- console.log(chalk5.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
9893
- console.log();
9894
- console.log("Get your API key at: https://app.pinata.dev/settings/api");
9726
+ console.log(chalk7.yellow("No API key provided."));
9727
+ console.log(chalk7.gray(" pinata auth login --key <your-api-key>"));
9728
+ console.log(chalk7.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
9895
9729
  process.exit(1);
9896
9730
  }
9897
9731
  if (apiKey.length < 20 || !apiKey.startsWith("pk_")) {
9898
- console.log(chalk5.red("Invalid API key format."));
9899
- console.log(chalk5.gray("Keys should start with 'pk_' and be at least 20 characters."));
9732
+ console.log(chalk7.red("Invalid API key format. Keys should start with 'pk_'."));
9900
9733
  process.exit(1);
9901
9734
  }
9902
9735
  const configDir = resolve(process.cwd(), ".pinata");
@@ -9905,19 +9738,13 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
9905
9738
  try {
9906
9739
  await mkdir4(configDir, { recursive: true });
9907
9740
  const maskedKey = `****${apiKey.slice(-8)}`;
9908
- const authData = {
9909
- configured: true,
9910
- keyId: maskedKey,
9911
- configuredAt: (/* @__PURE__ */ new Date()).toISOString()
9912
- };
9913
- await writeFileAsync(authPath, JSON.stringify(authData, null, 2), "utf8");
9741
+ await writeFileAsync(authPath, JSON.stringify({ configured: true, keyId: maskedKey, configuredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf8");
9914
9742
  const envPath = resolve(configDir, ".env");
9915
9743
  await writeFileAsync(envPath, `PINATA_API_KEY=${apiKey}
9916
9744
  `, { mode: 384 });
9917
- console.log(chalk5.green("API key configured successfully!"));
9918
- console.log(chalk5.gray(`Key ID: ${maskedKey}`));
9919
- console.log();
9920
- console.log(chalk5.yellow("Important: Add .pinata/.env to your .gitignore"));
9745
+ console.log(chalk7.green("API key configured successfully!"));
9746
+ console.log(chalk7.gray(`Key ID: ${maskedKey}`));
9747
+ console.log(chalk7.yellow("Important: Add .pinata/.env to your .gitignore"));
9921
9748
  } catch (error) {
9922
9749
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9923
9750
  process.exit(1);
@@ -9938,11 +9765,7 @@ auth.command("logout").description("Remove stored API key").action(async () => {
9938
9765
  await rm2(envPath);
9939
9766
  removed = true;
9940
9767
  }
9941
- if (removed) {
9942
- console.log(chalk5.green("API key removed successfully."));
9943
- } else {
9944
- console.log(chalk5.yellow("No stored API key found."));
9945
- }
9768
+ console.log(removed ? chalk7.green("API key removed successfully.") : chalk7.yellow("No stored API key found."));
9946
9769
  } catch (error) {
9947
9770
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9948
9771
  process.exit(1);
@@ -9951,19 +9774,19 @@ auth.command("logout").description("Remove stored API key").action(async () => {
9951
9774
  auth.command("status").description("Check authentication status").action(async () => {
9952
9775
  const authPath = resolve(process.cwd(), ".pinata", "auth.json");
9953
9776
  if (!existsSync(authPath)) {
9954
- console.log(chalk5.yellow("Not authenticated."));
9955
- console.log(chalk5.gray("Run: pinata auth login --key <your-api-key>"));
9777
+ console.log(chalk7.yellow("Not authenticated."));
9778
+ console.log(chalk7.gray("Run: pinata auth login --key <your-api-key>"));
9956
9779
  process.exit(0);
9957
9780
  }
9958
9781
  try {
9959
9782
  const { readFile: readFile7 } = await import('fs/promises');
9960
9783
  const authData = JSON.parse(await readFile7(authPath, "utf8"));
9961
- console.log(chalk5.green("Authenticated"));
9962
- console.log(chalk5.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
9963
- console.log(chalk5.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
9964
- } catch (error) {
9965
- console.log(chalk5.yellow("Authentication status unknown."));
9966
- console.log(chalk5.gray("Run: pinata auth login to reconfigure."));
9784
+ console.log(chalk7.green("Authenticated"));
9785
+ console.log(chalk7.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
9786
+ console.log(chalk7.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
9787
+ } catch {
9788
+ console.log(chalk7.yellow("Authentication status unknown."));
9789
+ console.log(chalk7.gray("Run: pinata auth login to reconfigure."));
9967
9790
  }
9968
9791
  });
9969
9792
  program.parse();