pinata-security-cli 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { z } from 'zod';
3
2
  import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
4
- import path, { dirname, resolve, join, basename, relative, extname } from 'path';
5
- import { existsSync, writeFileSync, readFileSync, chmodSync, mkdirSync } from 'fs';
3
+ import path, { dirname, resolve, relative, join, basename, extname } from 'path';
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 chalk6 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,18 +1478,18 @@ 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,
1515
1485
  timedOut
1516
1486
  });
1517
1487
  });
1518
- proc.on("error", (err3) => {
1488
+ proc.on("error", (err2) => {
1519
1489
  clearTimeout(timer);
1520
- resolve7({
1490
+ resolve9({
1521
1491
  stdout,
1522
- stderr: stderr + "\n" + err3.message,
1492
+ stderr: stderr + "\n" + err2.message,
1523
1493
  exitCode: 1,
1524
1494
  timedOut: false
1525
1495
  });
@@ -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(chalk6.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(chalk6.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(chalk6.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(chalk6.green(this.format(message)), ...args);
4634
4619
  }
4635
4620
  }
4636
4621
  /**
@@ -5573,6 +5558,64 @@ var SCORING_ADJUSTMENTS = [
5573
5558
  categoryId: "command-injection",
5574
5559
  lowerWeight: ["cli", "script"],
5575
5560
  higherWeight: ["web-server", "api", "serverless"]
5561
+ },
5562
+ // Connection failure handling less relevant for CLI
5563
+ {
5564
+ categoryId: "connection-failure",
5565
+ lowerWeight: ["cli", "script", "library"],
5566
+ higherWeight: ["web-server", "api"]
5567
+ },
5568
+ // Memory bloat less relevant for short-lived processes
5569
+ {
5570
+ categoryId: "memory-bloat",
5571
+ skip: ["cli", "script", "serverless"],
5572
+ higherWeight: ["web-server", "desktop"]
5573
+ },
5574
+ // Data race less relevant for single-threaded CLI
5575
+ {
5576
+ categoryId: "data-race",
5577
+ lowerWeight: ["cli", "script"],
5578
+ higherWeight: ["web-server", "api"]
5579
+ },
5580
+ // Network partition not relevant for CLI
5581
+ {
5582
+ categoryId: "network-partition",
5583
+ skip: ["cli", "script", "library", "frontend-spa"],
5584
+ higherWeight: ["web-server", "api"]
5585
+ },
5586
+ // Packet loss not relevant for CLI
5587
+ {
5588
+ categoryId: "packet-loss",
5589
+ skip: ["cli", "script", "library", "frontend-spa"],
5590
+ higherWeight: ["web-server", "api"]
5591
+ },
5592
+ // Thundering herd not relevant for CLI
5593
+ {
5594
+ categoryId: "thundering-herd",
5595
+ skip: ["cli", "script", "library", "frontend-spa"],
5596
+ higherWeight: ["web-server", "api"]
5597
+ },
5598
+ // Network timeout less relevant for CLI
5599
+ {
5600
+ categoryId: "network-timeout",
5601
+ lowerWeight: ["cli", "script"],
5602
+ higherWeight: ["web-server", "api"]
5603
+ },
5604
+ // High latency not relevant for CLI
5605
+ {
5606
+ categoryId: "high-latency",
5607
+ skip: ["cli", "script", "library"],
5608
+ higherWeight: ["web-server", "api"]
5609
+ },
5610
+ // Encoding mismatch less relevant for CLI
5611
+ {
5612
+ categoryId: "encoding-mismatch",
5613
+ lowerWeight: ["cli", "script"]
5614
+ },
5615
+ // Precision loss less relevant for CLI
5616
+ {
5617
+ categoryId: "precision-loss",
5618
+ lowerWeight: ["cli", "script"]
5576
5619
  }
5577
5620
  ];
5578
5621
  async function detectProjectType(projectPath) {
@@ -5705,7 +5748,32 @@ function getProjectTypeDescription(type) {
5705
5748
  }
5706
5749
  init_errors();
5707
5750
  init_result();
5708
- init_types();
5751
+ var SEVERITY_WEIGHTS = {
5752
+ critical: 4,
5753
+ high: 3,
5754
+ medium: 2,
5755
+ low: 1
5756
+ };
5757
+ var CONFIDENCE_WEIGHTS = {
5758
+ high: 1,
5759
+ medium: 0.3,
5760
+ low: 0.1
5761
+ };
5762
+ var PRIORITY_WEIGHTS = {
5763
+ P0: 3,
5764
+ P1: 2,
5765
+ P2: 1
5766
+ };
5767
+ var DEFAULT_TEST_PATTERNS = {
5768
+ python: ["test_*.py", "*_test.py", "tests/**/*.py", "test/**/*.py"],
5769
+ typescript: ["*.test.ts", "*.spec.ts", "__tests__/**/*.ts", "tests/**/*.ts"],
5770
+ javascript: ["*.test.js", "*.spec.js", "__tests__/**/*.js", "tests/**/*.js"],
5771
+ go: ["*_test.go"],
5772
+ java: ["*Test.java", "*Tests.java", "src/test/**/*.java"],
5773
+ rust: ["tests/**/*.rs"]
5774
+ };
5775
+
5776
+ // src/core/scanner/scanner.ts
5709
5777
  var DEFAULT_OPTIONS = {
5710
5778
  excludeDirs: [
5711
5779
  // Package managers
@@ -5770,7 +5838,7 @@ var Scanner = class {
5770
5838
  this.patternMatcher = new PatternMatcher();
5771
5839
  }
5772
5840
  /**
5773
- * Scan a directory for test coverage gaps
5841
+ * Scan a directory for security vulnerabilities
5774
5842
  *
5775
5843
  * @param targetDirectory Directory to scan
5776
5844
  * @param options Scan options
@@ -5905,25 +5973,36 @@ var Scanner = class {
5905
5973
  for (const domain of RISK_DOMAINS) {
5906
5974
  domainScores.set(domain, 100);
5907
5975
  }
5976
+ const gapsByCategory = /* @__PURE__ */ new Map();
5908
5977
  for (const gap of gaps) {
5909
- const severityWeight = SEVERITY_WEIGHTS[gap.severity];
5910
- const confidenceWeight = CONFIDENCE_WEIGHTS[gap.confidence];
5911
- const priorityWeight = PRIORITY_WEIGHTS[gap.priority];
5912
5978
  const projectTypeWeight = getCategoryWeight(gap.categoryId, projectType);
5913
- const basePenalty = 2;
5914
- const penalty = basePenalty * severityWeight * confidenceWeight * Math.sqrt(priorityWeight) * projectTypeWeight;
5915
- if (projectTypeWeight === 0) {
5916
- continue;
5917
- }
5979
+ if (projectTypeWeight === 0) continue;
5980
+ const existing = gapsByCategory.get(gap.categoryId) ?? [];
5981
+ existing.push(gap);
5982
+ gapsByCategory.set(gap.categoryId, existing);
5983
+ }
5984
+ for (const [categoryId, categoryGaps] of gapsByCategory) {
5985
+ const representative = categoryGaps[0];
5986
+ const severityWeight = SEVERITY_WEIGHTS[representative.severity];
5987
+ const projectTypeWeight = getCategoryWeight(categoryId, projectType);
5988
+ const maxConfidence = categoryGaps.reduce((max, g) => {
5989
+ const w = CONFIDENCE_WEIGHTS[g.confidence];
5990
+ return w > max ? w : max;
5991
+ }, 0);
5992
+ const count = categoryGaps.length;
5993
+ const countFactor = Math.log2(count + 1);
5994
+ const MAX_CATEGORY_PENALTY = 15;
5995
+ const rawPenalty = severityWeight * maxConfidence * countFactor * projectTypeWeight;
5996
+ const penalty = Math.min(rawPenalty, MAX_CATEGORY_PENALTY);
5918
5997
  baseScore -= penalty;
5919
- const currentDomainScore = domainScores.get(gap.domain) ?? 100;
5920
- domainScores.set(gap.domain, Math.max(0, currentDomainScore - penalty * 2));
5921
- if (penalty >= 5) {
5922
- const weightNote = projectTypeWeight !== 1 ? ` [${projectType} weight: ${projectTypeWeight}x]` : "";
5998
+ const currentDomainScore = domainScores.get(representative.domain) ?? 100;
5999
+ domainScores.set(representative.domain, Math.max(0, currentDomainScore - penalty * 1.5));
6000
+ if (penalty >= 3) {
6001
+ const weightNote = projectTypeWeight !== 1 ? ` [${projectType}: ${projectTypeWeight}x]` : "";
5923
6002
  penalties.push({
5924
- reason: `${gap.severity} ${gap.domain} gap: ${gap.categoryName}${weightNote}`,
6003
+ reason: `${representative.severity} ${representative.domain}: ${representative.categoryName} (${count} findings)${weightNote}`,
5925
6004
  points: Math.round(penalty),
5926
- categoryId: gap.categoryId
6005
+ categoryId
5927
6006
  });
5928
6007
  }
5929
6008
  }
@@ -6335,9 +6414,6 @@ function createScanner(categoryStore) {
6335
6414
  return new Scanner(categoryStore);
6336
6415
  }
6337
6416
 
6338
- // src/core/scanner/index.ts
6339
- init_types();
6340
-
6341
6417
  // src/core/index.ts
6342
6418
  var VERSION = "0.4.0";
6343
6419
 
@@ -6346,925 +6422,197 @@ init_errors();
6346
6422
  init_result();
6347
6423
  init_errors();
6348
6424
  init_result();
6349
- var TemplateRenderError = class extends PinataError {
6350
- constructor(message, context) {
6351
- super(message, "TEMPLATE_RENDER_ERROR", context);
6352
- this.name = "TemplateRenderError";
6353
- }
6425
+ var SEVERITY_COLORS = {
6426
+ critical: chalk6.red.bold,
6427
+ high: chalk6.red,
6428
+ medium: chalk6.yellow,
6429
+ low: chalk6.gray
6354
6430
  };
6355
- var TemplateSyntaxError = class extends PinataError {
6356
- constructor(message, context) {
6357
- super(message, "TEMPLATE_SYNTAX_ERROR", context);
6358
- this.name = "TemplateSyntaxError";
6359
- }
6431
+ var PRIORITY_COLORS = {
6432
+ P0: chalk6.red.bold,
6433
+ P1: chalk6.yellow,
6434
+ P2: chalk6.gray
6360
6435
  };
6361
- var CONDITIONAL_REGEX = /\{\{#if\s+([a-zA-Z][a-zA-Z0-9_.]*)\s*\}\}([\s\S]*?)(?:\{\{#else\}\}([\s\S]*?))?\{\{\/if\}\}/g;
6362
- var UNLESS_REGEX = /\{\{#unless\s+([a-zA-Z][a-zA-Z0-9_.]*)\s*\}\}([\s\S]*?)\{\{\/unless\}\}/g;
6363
- var EACH_REGEX = /\{\{#each\s+([a-zA-Z][a-zA-Z0-9_.]*)\s*\}\}([\s\S]*?)\{\{\/each\}\}/g;
6364
- var TemplateRenderer = class {
6365
- options;
6366
- constructor(options = {}) {
6367
- this.options = {
6368
- strict: options.strict ?? true,
6369
- allowUnresolved: options.allowUnresolved ?? false,
6370
- placeholderFormat: options.placeholderFormat ?? "mustache"
6371
- };
6436
+ var DOMAIN_COLORS = {
6437
+ security: chalk6.red,
6438
+ data: chalk6.blue,
6439
+ concurrency: chalk6.magenta,
6440
+ input: chalk6.cyan,
6441
+ resource: chalk6.yellow,
6442
+ reliability: chalk6.green,
6443
+ performance: chalk6.yellowBright,
6444
+ platform: chalk6.gray,
6445
+ business: chalk6.white,
6446
+ compliance: chalk6.blueBright
6447
+ };
6448
+ function formatTerminal(categories) {
6449
+ if (categories.length === 0) {
6450
+ return chalk6.yellow("No categories found matching the filters.");
6372
6451
  }
6373
- /**
6374
- * Validate template syntax for common errors
6375
- *
6376
- * @param template Template string to validate
6377
- * @returns Syntax validation result
6378
- */
6379
- validateSyntax(template) {
6380
- const errors = [];
6381
- if (this.hasUnclosedBlock(template, "if")) {
6382
- errors.push(
6383
- new TemplateSyntaxError("Unclosed {{#if}} block", {
6384
- hint: "Every {{#if variable}} must have a matching {{/if}}"
6385
- })
6386
- );
6387
- }
6388
- if (this.hasUnclosedBlock(template, "each")) {
6389
- errors.push(
6390
- new TemplateSyntaxError("Unclosed {{#each}} block", {
6391
- hint: "Every {{#each variable}} must have a matching {{/each}}"
6392
- })
6393
- );
6394
- }
6395
- if (this.hasUnclosedBlock(template, "unless")) {
6396
- errors.push(
6397
- new TemplateSyntaxError("Unclosed {{#unless}} block", {
6398
- hint: "Every {{#unless variable}} must have a matching {{/unless}}"
6399
- })
6400
- );
6452
+ const lines = [];
6453
+ lines.push(chalk6.bold.underline(`Found ${categories.length} categories:
6454
+ `));
6455
+ const byDomain = /* @__PURE__ */ new Map();
6456
+ for (const cat of categories) {
6457
+ const domain = cat.domain;
6458
+ if (!byDomain.has(domain)) {
6459
+ byDomain.set(domain, []);
6401
6460
  }
6402
- if (this.hasOrphanedClosingTag(template, "if")) {
6403
- errors.push(
6404
- new TemplateSyntaxError("Orphaned {{/if}} without matching {{#if}}", {
6405
- hint: "Remove the extra {{/if}} or add the opening {{#if variable}}"
6406
- })
6407
- );
6461
+ byDomain.get(domain).push(cat);
6462
+ }
6463
+ for (const [domain, domainCategories] of byDomain) {
6464
+ const domainColor = DOMAIN_COLORS[domain] ?? chalk6.white;
6465
+ lines.push(domainColor.bold(`
6466
+ ${domain.toUpperCase()} (${domainCategories.length})`));
6467
+ lines.push(chalk6.gray("\u2500".repeat(40)));
6468
+ for (const cat of domainCategories) {
6469
+ const priorityColor = PRIORITY_COLORS[cat.priority];
6470
+ const severityColor = SEVERITY_COLORS[cat.severity];
6471
+ const priority = priorityColor(`[${cat.priority}]`);
6472
+ const severity = severityColor(`${cat.severity}`);
6473
+ const level = chalk6.cyan(`${cat.level}`);
6474
+ const name = chalk6.white.bold(cat.name);
6475
+ const id = chalk6.gray(`(${cat.id})`);
6476
+ lines.push(` ${priority} ${name} ${id}`);
6477
+ lines.push(` ${severity} | ${level}`);
6478
+ const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
6479
+ lines.push(chalk6.gray(` ${desc}`));
6480
+ lines.push("");
6408
6481
  }
6409
- if (this.hasOrphanedClosingTag(template, "each")) {
6410
- errors.push(
6411
- new TemplateSyntaxError("Orphaned {{/each}} without matching {{#each}}", {
6412
- hint: "Remove the extra {{/each}} or add the opening {{#each variable}}"
6413
- })
6414
- );
6482
+ }
6483
+ lines.push(chalk6.gray("\u2500".repeat(40)));
6484
+ lines.push(formatStats(categories));
6485
+ return lines.join("\n");
6486
+ }
6487
+ function formatStats(categories) {
6488
+ const stats = {
6489
+ P0: 0,
6490
+ P1: 0,
6491
+ P2: 0,
6492
+ critical: 0,
6493
+ high: 0,
6494
+ medium: 0,
6495
+ low: 0
6496
+ };
6497
+ for (const cat of categories) {
6498
+ stats[cat.priority]++;
6499
+ stats[cat.severity]++;
6500
+ }
6501
+ const parts = [];
6502
+ if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
6503
+ if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
6504
+ if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
6505
+ parts.push(chalk6.gray("|"));
6506
+ if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
6507
+ if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
6508
+ if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
6509
+ if (stats.low > 0) parts.push(SEVERITY_COLORS.low(`${stats.low} low`));
6510
+ return parts.join(" ");
6511
+ }
6512
+ function formatJson(categories) {
6513
+ return JSON.stringify(categories, null, 2);
6514
+ }
6515
+ function formatMarkdown(categories) {
6516
+ if (categories.length === 0) {
6517
+ return "_No categories found matching the filters._";
6518
+ }
6519
+ const lines = [];
6520
+ lines.push(`# Categories (${categories.length})
6521
+ `);
6522
+ const byDomain = /* @__PURE__ */ new Map();
6523
+ for (const cat of categories) {
6524
+ const domain = cat.domain;
6525
+ if (!byDomain.has(domain)) {
6526
+ byDomain.set(domain, []);
6415
6527
  }
6416
- if (this.hasOrphanedClosingTag(template, "unless")) {
6417
- errors.push(
6418
- new TemplateSyntaxError("Orphaned {{/unless}} without matching {{#unless}}", {
6419
- hint: "Remove the extra {{/unless}} or add the opening {{#unless variable}}"
6420
- })
6421
- );
6528
+ byDomain.get(domain).push(cat);
6529
+ }
6530
+ for (const [domain, domainCategories] of byDomain) {
6531
+ lines.push(`
6532
+ ## ${domain.charAt(0).toUpperCase() + domain.slice(1)} (${domainCategories.length})
6533
+ `);
6534
+ for (const cat of domainCategories) {
6535
+ lines.push(`### ${cat.name}`);
6536
+ lines.push(`- **ID**: \`${cat.id}\``);
6537
+ lines.push(`- **Priority**: ${cat.priority}`);
6538
+ lines.push(`- **Severity**: ${cat.severity}`);
6539
+ lines.push(`- **Level**: ${cat.level}`);
6540
+ lines.push(`
6541
+ ${cat.description}
6542
+ `);
6422
6543
  }
6423
- const mismatchErrors = this.checkBlockNesting(template);
6424
- errors.push(...mismatchErrors);
6425
- return {
6426
- valid: errors.length === 0,
6427
- errors
6428
- };
6429
6544
  }
6430
- /**
6431
- * Check if template has an unclosed block of specified type
6432
- */
6433
- hasUnclosedBlock(template, blockType) {
6434
- const openRegex = new RegExp(`\\{\\{#${blockType}\\s+[^}]+\\}\\}`, "g");
6435
- const closeRegex = new RegExp(`\\{\\{/${blockType}\\}\\}`, "g");
6436
- const opens = (template.match(openRegex) || []).length;
6437
- const closes = (template.match(closeRegex) || []).length;
6438
- return opens > closes;
6545
+ return lines.join("\n");
6546
+ }
6547
+ function formatCategories(categories, format) {
6548
+ switch (format) {
6549
+ case "json":
6550
+ return formatJson(categories);
6551
+ case "markdown":
6552
+ return formatMarkdown(categories);
6553
+ case "terminal":
6554
+ default:
6555
+ return formatTerminal(categories);
6439
6556
  }
6440
- /**
6441
- * Check if template has orphaned closing tags
6442
- */
6443
- hasOrphanedClosingTag(template, blockType) {
6444
- const openRegex = new RegExp(`\\{\\{#${blockType}\\s+[^}]+\\}\\}`, "g");
6445
- const closeRegex = new RegExp(`\\{\\{/${blockType}\\}\\}`, "g");
6446
- const opens = (template.match(openRegex) || []).length;
6447
- const closes = (template.match(closeRegex) || []).length;
6448
- return closes > opens;
6557
+ }
6558
+ function isValidOutputFormat(format) {
6559
+ return ["terminal", "json", "markdown"].includes(format);
6560
+ }
6561
+ function formatError(error) {
6562
+ return chalk6.red(`Error: ${error.message}`);
6563
+ }
6564
+
6565
+ // src/ai/service.ts
6566
+ var DEFAULT_CONFIG = {
6567
+ provider: "anthropic",
6568
+ apiKey: "",
6569
+ model: "claude-sonnet-4-20250514",
6570
+ maxTokens: 1024,
6571
+ temperature: 0.3,
6572
+ timeoutMs: 3e4
6573
+ };
6574
+ var PROVIDER_MODELS = {
6575
+ anthropic: "claude-sonnet-4-20250514",
6576
+ openai: "gpt-4o",
6577
+ mock: "mock-model"
6578
+ };
6579
+ var PROVIDER_ENDPOINTS = {
6580
+ anthropic: "https://api.anthropic.com/v1/messages",
6581
+ openai: "https://api.openai.com/v1/chat/completions"};
6582
+ var AIService = class {
6583
+ config;
6584
+ constructor(config2 = {}) {
6585
+ this.config = {
6586
+ ...DEFAULT_CONFIG,
6587
+ ...config2,
6588
+ apiKey: config2.apiKey ?? this.getApiKeyFromEnv(config2.provider ?? "anthropic"),
6589
+ model: config2.model ?? PROVIDER_MODELS[config2.provider ?? "anthropic"]
6590
+ };
6449
6591
  }
6450
6592
  /**
6451
- * Check for improperly nested blocks
6593
+ * Get API key from environment variable
6594
+ * For config file support, use the sync version below
6452
6595
  */
6453
- checkBlockNesting(template) {
6454
- const errors = [];
6455
- const stack = [];
6456
- const blockPattern = /\{\{(#(?:if|each|unless)|\/(?:if|each|unless))\s*[^}]*\}\}/g;
6457
- let match;
6458
- while ((match = blockPattern.exec(template)) !== null) {
6459
- const tag = match[1] ?? "";
6460
- if (tag.startsWith("#")) {
6461
- const type = tag.slice(1).split(/\s/)[0] ?? "";
6462
- stack.push({ type, index: match.index });
6463
- } else if (tag.startsWith("/")) {
6464
- const type = tag.slice(1);
6465
- const last = stack.pop();
6466
- if (!last) {
6467
- continue;
6468
- }
6469
- if (last.type !== type) {
6470
- errors.push(
6471
- new TemplateSyntaxError(`Mismatched block: opened {{#${last.type}}} but closed with {{/${type}}}`, {
6472
- openedAt: last.index,
6473
- closedAt: match.index
6474
- })
6475
- );
6476
- }
6477
- }
6596
+ getApiKeyFromEnv(provider) {
6597
+ if (provider === "mock") return "mock-key";
6598
+ const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
6599
+ const envValue = process.env[envVar];
6600
+ if (envValue !== void 0 && envValue.length > 0) {
6601
+ return envValue;
6478
6602
  }
6479
- return errors;
6603
+ return this.getApiKeyFromConfig(provider);
6480
6604
  }
6481
6605
  /**
6482
- * Parse all placeholders from a template string
6483
- *
6484
- * @param template Template string to parse
6485
- * @returns Array of parsed placeholders
6486
- */
6487
- parsePlaceholders(template) {
6488
- const placeholders = [];
6489
- const regex = this.getPlaceholderRegex();
6490
- let match;
6491
- regex.lastIndex = 0;
6492
- while ((match = regex.exec(template)) !== null) {
6493
- const name = match[1] ?? "";
6494
- const pathSegments = name.split(".");
6495
- placeholders.push({
6496
- match: match[0],
6497
- name,
6498
- startIndex: match.index,
6499
- endIndex: match.index + match[0].length,
6500
- isNestedPath: pathSegments.length > 1,
6501
- pathSegments
6502
- });
6503
- }
6504
- return placeholders;
6505
- }
6506
- /**
6507
- * Parse conditional blocks from template
6508
- *
6509
- * @param template Template string to parse
6510
- * @returns Array of parsed conditionals
6511
- */
6512
- parseConditionals(template) {
6513
- const conditionals = [];
6514
- const regex = new RegExp(CONDITIONAL_REGEX.source, "g");
6515
- let match;
6516
- while ((match = regex.exec(template)) !== null) {
6517
- const falseBranch = match[3];
6518
- conditionals.push({
6519
- match: match[0],
6520
- variable: match[1] ?? "",
6521
- trueBranch: match[2] ?? "",
6522
- ...falseBranch !== void 0 && { falseBranch },
6523
- startIndex: match.index,
6524
- endIndex: match.index + match[0].length
6525
- });
6526
- }
6527
- return conditionals;
6528
- }
6529
- /**
6530
- * Parse loop blocks from template
6531
- *
6532
- * @param template Template string to parse
6533
- * @returns Array of parsed loops
6534
- */
6535
- parseLoops(template) {
6536
- const loops = [];
6537
- const regex = new RegExp(EACH_REGEX.source, "g");
6538
- let match;
6539
- while ((match = regex.exec(template)) !== null) {
6540
- loops.push({
6541
- match: match[0],
6542
- variable: match[1] ?? "",
6543
- body: match[2] ?? "",
6544
- startIndex: match.index,
6545
- endIndex: match.index + match[0].length
6546
- });
6547
- }
6548
- return loops;
6549
- }
6550
- /**
6551
- * Get unique variable names from template (including nested paths)
6552
- * Excludes loop-internal variables like {{this}}, {{@index}}, and item properties
6553
- *
6554
- * @param template Template string to analyze
6555
- * @param excludeLoopInternal Whether to exclude variables inside loop blocks
6556
- * @returns Unique variable names found in template
6557
- */
6558
- getVariableNames(template, excludeLoopInternal = true) {
6559
- let processedTemplate = template;
6560
- if (excludeLoopInternal) {
6561
- processedTemplate = processedTemplate.replace(EACH_REGEX, (match, variable) => {
6562
- return `{{${variable}}}`;
6563
- });
6564
- }
6565
- const placeholders = this.parsePlaceholders(processedTemplate);
6566
- const names = /* @__PURE__ */ new Set();
6567
- const loopSpecialVars = /* @__PURE__ */ new Set(["this", "@index", "@first", "@last"]);
6568
- for (const p of placeholders) {
6569
- if (loopSpecialVars.has(p.name)) {
6570
- continue;
6571
- }
6572
- names.add(p.name);
6573
- if (p.isNestedPath && p.pathSegments[0]) {
6574
- names.add(p.pathSegments[0]);
6575
- }
6576
- }
6577
- const conditionals = this.parseConditionals(processedTemplate);
6578
- for (const c of conditionals) {
6579
- names.add(c.variable);
6580
- const root = c.variable.split(".")[0];
6581
- if (root && root !== c.variable) {
6582
- names.add(root);
6583
- }
6584
- }
6585
- const loops = this.parseLoops(template);
6586
- for (const l of loops) {
6587
- names.add(l.variable);
6588
- const root = l.variable.split(".")[0];
6589
- if (root && root !== l.variable) {
6590
- names.add(root);
6591
- }
6592
- }
6593
- return [...names];
6594
- }
6595
- /**
6596
- * Get value from object using dot notation path
6597
- *
6598
- * @param obj Object to traverse
6599
- * @param path Dot-separated path (e.g., "user.address.city")
6600
- * @returns Value at path or undefined
6601
- */
6602
- getNestedValue(obj, path2) {
6603
- const segments = path2.split(".");
6604
- let current = obj;
6605
- for (const segment of segments) {
6606
- if (current === null || current === void 0) {
6607
- return void 0;
6608
- }
6609
- if (typeof current !== "object") {
6610
- return void 0;
6611
- }
6612
- current = current[segment];
6613
- }
6614
- return current;
6615
- }
6616
- /**
6617
- * Evaluate if a value is truthy for conditional blocks
6618
- *
6619
- * @param value Value to evaluate
6620
- * @returns True if value is truthy
6621
- */
6622
- isTruthy(value) {
6623
- if (value === null || value === void 0) {
6624
- return false;
6625
- }
6626
- if (typeof value === "boolean") {
6627
- return value;
6628
- }
6629
- if (typeof value === "number") {
6630
- return value !== 0;
6631
- }
6632
- if (typeof value === "string") {
6633
- return value.length > 0;
6634
- }
6635
- if (Array.isArray(value)) {
6636
- return value.length > 0;
6637
- }
6638
- if (typeof value === "object") {
6639
- return Object.keys(value).length > 0;
6640
- }
6641
- return Boolean(value);
6642
- }
6643
- /**
6644
- * Process conditional blocks in template
6645
- *
6646
- * @param template Template string
6647
- * @param values Variable values
6648
- * @returns Processed template with conditionals resolved
6649
- */
6650
- processConditionals(template, values) {
6651
- let result = template;
6652
- result = result.replace(CONDITIONAL_REGEX, (match, variable, trueBranch, falseBranch) => {
6653
- const value = this.getNestedValue(values, variable);
6654
- const condition = this.isTruthy(value);
6655
- return condition ? trueBranch : falseBranch ?? "";
6656
- });
6657
- result = result.replace(UNLESS_REGEX, (match, variable, content) => {
6658
- const value = this.getNestedValue(values, variable);
6659
- const condition = this.isTruthy(value);
6660
- return condition ? "" : content;
6661
- });
6662
- return result;
6663
- }
6664
- /**
6665
- * Process loop blocks in template
6666
- *
6667
- * @param template Template string
6668
- * @param values Variable values
6669
- * @returns Processed template with loops expanded
6670
- */
6671
- processLoops(template, values) {
6672
- let result = template;
6673
- result = result.replace(EACH_REGEX, (match, variable, body) => {
6674
- const arrayValue = this.getNestedValue(values, variable);
6675
- if (!Array.isArray(arrayValue)) {
6676
- return "";
6677
- }
6678
- const expanded = [];
6679
- for (let i = 0; i < arrayValue.length; i++) {
6680
- const item = arrayValue[i];
6681
- let iterationBody = body;
6682
- iterationBody = iterationBody.replace(/\{\{this\}\}/g, this.stringify(item));
6683
- iterationBody = iterationBody.replace(/\{\{@index\}\}/g, String(i));
6684
- iterationBody = iterationBody.replace(/\{\{@first\}\}/g, String(i === 0));
6685
- iterationBody = iterationBody.replace(/\{\{@last\}\}/g, String(i === arrayValue.length - 1));
6686
- if (item !== null && typeof item === "object" && !Array.isArray(item)) {
6687
- const itemObj = item;
6688
- iterationBody = this.processConditionals(iterationBody, itemObj);
6689
- for (const [key, value] of Object.entries(itemObj)) {
6690
- const propRegex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
6691
- iterationBody = iterationBody.replace(propRegex, this.stringify(value));
6692
- }
6693
- }
6694
- expanded.push(iterationBody);
6695
- }
6696
- return expanded.join("");
6697
- });
6698
- return result;
6699
- }
6700
- /**
6701
- * Validate provided variables against template requirements
6702
- *
6703
- * @param template Test template with variable definitions
6704
- * @param values Provided variable values
6705
- * @returns Validation result
6706
- */
6707
- validateVariables(template, values) {
6708
- const results = [];
6709
- const missingRequired = [];
6710
- const unknownVariables = [];
6711
- const typeErrors = [];
6712
- const usedInTemplate = new Set(this.getVariableNames(template.template));
6713
- const definedVariables = /* @__PURE__ */ new Map();
6714
- for (const v of template.variables) {
6715
- definedVariables.set(v.name, v);
6716
- }
6717
- for (const variable of template.variables) {
6718
- const result = {
6719
- name: variable.name,
6720
- valid: true,
6721
- errors: []
6722
- };
6723
- const value = values[variable.name];
6724
- const hasValue = variable.name in values;
6725
- if (variable.required && !hasValue && variable.defaultValue === void 0) {
6726
- result.valid = false;
6727
- result.errors.push(`Required variable '${variable.name}' is missing`);
6728
- missingRequired.push(variable.name);
6729
- }
6730
- if (hasValue && value !== void 0 && value !== null) {
6731
- const typeError = this.checkType(variable.name, value, variable.type);
6732
- if (typeError) {
6733
- result.valid = false;
6734
- result.errors.push(typeError);
6735
- typeErrors.push(typeError);
6736
- }
6737
- }
6738
- results.push(result);
6739
- }
6740
- const providedNames = Object.keys(values);
6741
- for (const name of providedNames) {
6742
- if (!definedVariables.has(name)) {
6743
- const isUsed = [...usedInTemplate].some((used) => used === name || used.startsWith(name + "."));
6744
- if (isUsed) {
6745
- results.push({
6746
- name,
6747
- valid: true,
6748
- errors: [`Variable '${name}' is used in template but not formally defined`]
6749
- });
6750
- } else {
6751
- unknownVariables.push(name);
6752
- }
6753
- }
6754
- }
6755
- for (const placeholder of usedInTemplate) {
6756
- const defined = definedVariables.get(placeholder);
6757
- const rootVar = placeholder.split(".")[0] ?? placeholder;
6758
- const hasValue = placeholder in values || rootVar in values;
6759
- if (!hasValue && !defined?.defaultValue) {
6760
- if (!defined) {
6761
- if (!placeholder.includes(".")) {
6762
- missingRequired.push(placeholder);
6763
- } else if (!(rootVar in values)) {
6764
- missingRequired.push(rootVar);
6765
- }
6766
- }
6767
- }
6768
- }
6769
- const valid = missingRequired.length === 0 && typeErrors.length === 0 && (unknownVariables.length === 0 || !this.options.strict);
6770
- return {
6771
- valid,
6772
- results,
6773
- missingRequired: [...new Set(missingRequired)],
6774
- unknownVariables,
6775
- typeErrors
6776
- };
6777
- }
6778
- /**
6779
- * Check if a value matches the expected type
6780
- */
6781
- checkType(name, value, expectedType) {
6782
- const actualType = this.getValueType(value);
6783
- if (actualType !== expectedType) {
6784
- return `Variable '${name}' expected type '${expectedType}' but got '${actualType}'`;
6785
- }
6786
- return null;
6787
- }
6788
- /**
6789
- * Determine the VariableType of a value
6790
- */
6791
- getValueType(value) {
6792
- if (value === null || value === void 0) {
6793
- return "string";
6794
- }
6795
- if (Array.isArray(value)) {
6796
- return "array";
6797
- }
6798
- if (typeof value === "object") {
6799
- return "object";
6800
- }
6801
- if (typeof value === "boolean") {
6802
- return "boolean";
6803
- }
6804
- if (typeof value === "number") {
6805
- return "number";
6806
- }
6807
- return "string";
6808
- }
6809
- /**
6810
- * Substitute variables in a template string
6811
- *
6812
- * @param template Template string with placeholders
6813
- * @param values Variable values to substitute
6814
- * @param variableDefs Optional variable definitions for defaults
6815
- * @returns Substitution result
6816
- */
6817
- substituteVariables(template, values, variableDefs) {
6818
- const substituted = [];
6819
- const unresolved = [];
6820
- const resolvedValues = /* @__PURE__ */ new Map();
6821
- if (variableDefs) {
6822
- for (const def of variableDefs) {
6823
- if (def.defaultValue !== void 0) {
6824
- resolvedValues.set(def.name, def.defaultValue);
6825
- }
6826
- }
6827
- }
6828
- for (const [key, value] of Object.entries(values)) {
6829
- if (value === void 0 || value === null) {
6830
- resolvedValues.set(key, null);
6831
- } else {
6832
- resolvedValues.set(key, value);
6833
- }
6834
- }
6835
- const combinedValues = {};
6836
- for (const [key, value] of resolvedValues) {
6837
- combinedValues[key] = value;
6838
- }
6839
- const regex = this.getPlaceholderRegex();
6840
- const content = template.replace(regex, (match, name) => {
6841
- let value;
6842
- let hasValue = false;
6843
- if (name.includes(".")) {
6844
- value = this.getNestedValue(combinedValues, name);
6845
- hasValue = value !== void 0;
6846
- } else {
6847
- hasValue = resolvedValues.has(name);
6848
- value = resolvedValues.get(name);
6849
- }
6850
- if (hasValue) {
6851
- substituted.push(name);
6852
- return this.stringify(value);
6853
- }
6854
- if (this.options.allowUnresolved) {
6855
- unresolved.push(name);
6856
- return match;
6857
- }
6858
- unresolved.push(name);
6859
- return match;
6860
- });
6861
- return {
6862
- content,
6863
- substituted: [...new Set(substituted)],
6864
- unresolved: [...new Set(unresolved)]
6865
- };
6866
- }
6867
- /**
6868
- * Convert a value to string for template substitution
6869
- */
6870
- stringify(value) {
6871
- if (value === null || value === void 0) {
6872
- return "";
6873
- }
6874
- if (typeof value === "string") {
6875
- return value;
6876
- }
6877
- if (typeof value === "number" || typeof value === "boolean") {
6878
- return String(value);
6879
- }
6880
- if (Array.isArray(value)) {
6881
- return JSON.stringify(value);
6882
- }
6883
- if (typeof value === "object") {
6884
- return JSON.stringify(value);
6885
- }
6886
- return String(value);
6887
- }
6888
- /**
6889
- * Process all template features: conditionals, loops, then variable substitution
6890
- *
6891
- * @param template Template string
6892
- * @param values Variable values
6893
- * @param variableDefs Optional variable definitions
6894
- * @returns Processed content with all features applied
6895
- */
6896
- processTemplate(template, values, variableDefs) {
6897
- const syntaxResult = this.validateSyntax(template);
6898
- if (!syntaxResult.valid && this.options.strict) {
6899
- return err(syntaxResult.errors[0] ?? new TemplateSyntaxError("Unknown syntax error"));
6900
- }
6901
- const combinedValues = {};
6902
- if (variableDefs) {
6903
- for (const def of variableDefs) {
6904
- if (def.defaultValue !== void 0) {
6905
- combinedValues[def.name] = def.defaultValue;
6906
- }
6907
- }
6908
- }
6909
- for (const [key, value] of Object.entries(values)) {
6910
- combinedValues[key] = value;
6911
- }
6912
- let processed = template;
6913
- processed = this.processLoops(processed, combinedValues);
6914
- processed = this.processConditionals(processed, combinedValues);
6915
- const result = this.substituteVariables(processed, combinedValues, variableDefs);
6916
- return ok(result);
6917
- }
6918
- /**
6919
- * Render a complete test template with variable substitution
6920
- *
6921
- * @param template Test template to render
6922
- * @param values Variable values to substitute
6923
- * @param options Optional render options
6924
- * @returns Render result or error
6925
- */
6926
- renderTemplate(template, values, options) {
6927
- const mergedOptions = { ...this.options, ...options };
6928
- const syntaxResult = this.validateSyntax(template.template);
6929
- if (!syntaxResult.valid && mergedOptions.strict) {
6930
- return err(syntaxResult.errors[0] ?? new TemplateSyntaxError("Unknown syntax error"));
6931
- }
6932
- const validation = this.validateVariables(template, values);
6933
- if (!validation.valid && mergedOptions.strict) {
6934
- const errors = [];
6935
- if (validation.missingRequired.length > 0) {
6936
- errors.push(`Missing required variables: ${validation.missingRequired.join(", ")}`);
6937
- }
6938
- if (validation.typeErrors.length > 0) {
6939
- errors.push(...validation.typeErrors);
6940
- }
6941
- if (validation.unknownVariables.length > 0) {
6942
- errors.push(`Unknown variables: ${validation.unknownVariables.join(", ")}`);
6943
- }
6944
- return err(
6945
- new TemplateRenderError("Template validation failed", {
6946
- errors,
6947
- validation
6948
- })
6949
- );
6950
- }
6951
- const processResult = this.processTemplate(template.template, values, template.variables);
6952
- if (!processResult.success) {
6953
- return processResult;
6954
- }
6955
- const { content, substituted, unresolved } = processResult.data;
6956
- if (unresolved.length > 0 && !mergedOptions.allowUnresolved && mergedOptions.strict) {
6957
- return err(
6958
- new TemplateRenderError("Unresolved template variables", {
6959
- unresolved
6960
- })
6961
- );
6962
- }
6963
- return ok({
6964
- content,
6965
- substituted,
6966
- unresolved,
6967
- imports: template.imports ?? [],
6968
- fixtures: template.fixtures ?? []
6969
- });
6970
- }
6971
- /**
6972
- * Render multiple templates at once
6973
- *
6974
- * @param templates Array of templates to render
6975
- * @param values Variable values (applied to all templates)
6976
- * @returns Array of render results
6977
- */
6978
- renderTemplates(templates, values) {
6979
- const results = [];
6980
- for (const template of templates) {
6981
- const result = this.renderTemplate(template, values);
6982
- if (!result.success) {
6983
- return err(
6984
- new TemplateRenderError(`Failed to render template '${template.id}'`, {
6985
- templateId: template.id,
6986
- originalError: result.error.message
6987
- })
6988
- );
6989
- }
6990
- results.push(result.data);
6991
- }
6992
- return ok(results);
6993
- }
6994
- /**
6995
- * Check if a template string contains nested placeholders
6996
- * (e.g., {{outer{{inner}}}})
6997
- *
6998
- * @param template Template string to check
6999
- * @returns True if nested placeholders detected
7000
- */
7001
- hasNestedPlaceholders(template) {
7002
- const nestedPattern = /\{\{[^{}]*\{\{/;
7003
- return nestedPattern.test(template);
7004
- }
7005
- /**
7006
- * Extract all imports from rendered templates
7007
- *
7008
- * @param results Array of render results
7009
- * @returns Deduplicated array of imports
7010
- */
7011
- collectImports(results) {
7012
- const imports = /* @__PURE__ */ new Set();
7013
- for (const result of results) {
7014
- for (const imp of result.imports) {
7015
- imports.add(imp);
7016
- }
7017
- }
7018
- return [...imports];
7019
- }
7020
- /**
7021
- * Extract all fixtures from rendered templates
7022
- *
7023
- * @param results Array of render results
7024
- * @returns Deduplicated array of fixtures
7025
- */
7026
- collectFixtures(results) {
7027
- const fixtures = /* @__PURE__ */ new Set();
7028
- for (const result of results) {
7029
- for (const fix of result.fixtures) {
7030
- fixtures.add(fix);
7031
- }
7032
- }
7033
- return [...fixtures];
7034
- }
7035
- /**
7036
- * Get the appropriate regex for the configured placeholder format
7037
- */
7038
- getPlaceholderRegex() {
7039
- switch (this.options.placeholderFormat) {
7040
- case "dollar":
7041
- return /\$\{([a-zA-Z][a-zA-Z0-9_.]*)\}/g;
7042
- case "percent":
7043
- return /%\(([a-zA-Z][a-zA-Z0-9_.]*)\)s/g;
7044
- case "mustache":
7045
- default:
7046
- return /\{\{([a-zA-Z][a-zA-Z0-9_.]*)\}\}/g;
7047
- }
7048
- }
7049
- /**
7050
- * Create a template from a string with variable definitions
7051
- *
7052
- * @param templateString Template content
7053
- * @param variables Variable definitions
7054
- * @param metadata Additional template metadata
7055
- * @returns TestTemplate object
7056
- */
7057
- static createTemplate(templateString, variables, metadata = {}) {
7058
- return {
7059
- id: metadata.id ?? "custom-template",
7060
- language: metadata.language ?? "typescript",
7061
- framework: metadata.framework ?? "jest",
7062
- template: templateString,
7063
- variables,
7064
- imports: metadata.imports,
7065
- fixtures: metadata.fixtures,
7066
- description: metadata.description
7067
- };
7068
- }
7069
- };
7070
- function createRenderer(options) {
7071
- return new TemplateRenderer(options);
7072
- }
7073
- var SEVERITY_COLORS = {
7074
- critical: chalk5.red.bold,
7075
- high: chalk5.red,
7076
- medium: chalk5.yellow,
7077
- low: chalk5.gray
7078
- };
7079
- var PRIORITY_COLORS = {
7080
- P0: chalk5.red.bold,
7081
- P1: chalk5.yellow,
7082
- P2: chalk5.gray
7083
- };
7084
- 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
7095
- };
7096
- function formatTerminal(categories) {
7097
- if (categories.length === 0) {
7098
- return chalk5.yellow("No categories found matching the filters.");
7099
- }
7100
- const lines = [];
7101
- lines.push(chalk5.bold.underline(`Found ${categories.length} categories:
7102
- `));
7103
- const byDomain = /* @__PURE__ */ new Map();
7104
- for (const cat of categories) {
7105
- const domain = cat.domain;
7106
- if (!byDomain.has(domain)) {
7107
- byDomain.set(domain, []);
7108
- }
7109
- byDomain.get(domain).push(cat);
7110
- }
7111
- for (const [domain, domainCategories] of byDomain) {
7112
- const domainColor = DOMAIN_COLORS[domain] ?? chalk5.white;
7113
- lines.push(domainColor.bold(`
7114
- ${domain.toUpperCase()} (${domainCategories.length})`));
7115
- lines.push(chalk5.gray("\u2500".repeat(40)));
7116
- for (const cat of domainCategories) {
7117
- const priorityColor = PRIORITY_COLORS[cat.priority];
7118
- const severityColor = SEVERITY_COLORS[cat.severity];
7119
- const priority = priorityColor(`[${cat.priority}]`);
7120
- 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})`);
7124
- lines.push(` ${priority} ${name} ${id}`);
7125
- lines.push(` ${severity} | ${level}`);
7126
- const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
7127
- lines.push(chalk5.gray(` ${desc}`));
7128
- lines.push("");
7129
- }
7130
- }
7131
- lines.push(chalk5.gray("\u2500".repeat(40)));
7132
- lines.push(formatStats(categories));
7133
- return lines.join("\n");
7134
- }
7135
- function formatStats(categories) {
7136
- const stats = {
7137
- P0: 0,
7138
- P1: 0,
7139
- P2: 0,
7140
- critical: 0,
7141
- high: 0,
7142
- medium: 0,
7143
- low: 0
7144
- };
7145
- for (const cat of categories) {
7146
- stats[cat.priority]++;
7147
- stats[cat.severity]++;
7148
- }
7149
- const parts = [];
7150
- if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
7151
- if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
7152
- if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
7153
- parts.push(chalk5.gray("|"));
7154
- if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
7155
- if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
7156
- if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
7157
- if (stats.low > 0) parts.push(SEVERITY_COLORS.low(`${stats.low} low`));
7158
- return parts.join(" ");
7159
- }
7160
- function formatJson(categories) {
7161
- return JSON.stringify(categories, null, 2);
7162
- }
7163
- function formatMarkdown(categories) {
7164
- if (categories.length === 0) {
7165
- return "_No categories found matching the filters._";
7166
- }
7167
- const lines = [];
7168
- lines.push(`# Categories (${categories.length})
7169
- `);
7170
- const byDomain = /* @__PURE__ */ new Map();
7171
- for (const cat of categories) {
7172
- const domain = cat.domain;
7173
- if (!byDomain.has(domain)) {
7174
- byDomain.set(domain, []);
7175
- }
7176
- byDomain.get(domain).push(cat);
7177
- }
7178
- for (const [domain, domainCategories] of byDomain) {
7179
- lines.push(`
7180
- ## ${domain.charAt(0).toUpperCase() + domain.slice(1)} (${domainCategories.length})
7181
- `);
7182
- for (const cat of domainCategories) {
7183
- lines.push(`### ${cat.name}`);
7184
- lines.push(`- **ID**: \`${cat.id}\``);
7185
- lines.push(`- **Priority**: ${cat.priority}`);
7186
- lines.push(`- **Severity**: ${cat.severity}`);
7187
- lines.push(`- **Level**: ${cat.level}`);
7188
- lines.push(`
7189
- ${cat.description}
7190
- `);
7191
- }
7192
- }
7193
- return lines.join("\n");
7194
- }
7195
- function formatCategories(categories, format) {
7196
- switch (format) {
7197
- case "json":
7198
- return formatJson(categories);
7199
- case "markdown":
7200
- return formatMarkdown(categories);
7201
- case "terminal":
7202
- default:
7203
- return formatTerminal(categories);
7204
- }
7205
- }
7206
- function isValidOutputFormat(format) {
7207
- return ["terminal", "json", "markdown"].includes(format);
7208
- }
7209
- function formatError(error) {
7210
- return chalk5.red(`Error: ${error.message}`);
7211
- }
7212
-
7213
- // src/cli/generate-formatters.ts
7214
- init_errors();
7215
- init_result();
7216
-
7217
- // src/ai/service.ts
7218
- var DEFAULT_CONFIG = {
7219
- provider: "anthropic",
7220
- apiKey: "",
7221
- model: "claude-sonnet-4-20250514",
7222
- maxTokens: 1024,
7223
- temperature: 0.3,
7224
- timeoutMs: 3e4
7225
- };
7226
- var PROVIDER_MODELS = {
7227
- anthropic: "claude-sonnet-4-20250514",
7228
- openai: "gpt-4o",
7229
- mock: "mock-model"
7230
- };
7231
- var PROVIDER_ENDPOINTS = {
7232
- anthropic: "https://api.anthropic.com/v1/messages",
7233
- openai: "https://api.openai.com/v1/chat/completions"};
7234
- var AIService = class {
7235
- config;
7236
- constructor(config2 = {}) {
7237
- this.config = {
7238
- ...DEFAULT_CONFIG,
7239
- ...config2,
7240
- apiKey: config2.apiKey ?? this.getApiKeyFromEnv(config2.provider ?? "anthropic"),
7241
- model: config2.model ?? PROVIDER_MODELS[config2.provider ?? "anthropic"]
7242
- };
7243
- }
7244
- /**
7245
- * Get API key from environment variable
7246
- * For config file support, use the sync version below
7247
- */
7248
- getApiKeyFromEnv(provider) {
7249
- if (provider === "mock") return "mock-key";
7250
- const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
7251
- const envValue = process.env[envVar];
7252
- if (envValue !== void 0 && envValue.length > 0) {
7253
- return envValue;
7254
- }
7255
- return this.getApiKeyFromConfig(provider);
7256
- }
7257
- /**
7258
- * Read API key from config file synchronously
7259
- * Uses require() for sync file access in constructor context
6606
+ * Read API key from config file synchronously
6607
+ * Uses require() for sync file access in constructor context
7260
6608
  */
7261
6609
  getApiKeyFromConfig(provider) {
7262
6610
  try {
7263
- const { existsSync: existsSync5, readFileSync: readFileSync3 } = __require("fs");
6611
+ const { existsSync: existsSync7, readFileSync: readFileSync3 } = __require("fs");
7264
6612
  const { homedir: homedir3 } = __require("os");
7265
6613
  const { join: join5 } = __require("path");
7266
6614
  const configPath = join5(homedir3(), ".pinata", "config.json");
7267
- if (!existsSync5(configPath)) {
6615
+ if (!existsSync7(configPath)) {
7268
6616
  return "";
7269
6617
  }
7270
6618
  const content = readFileSync3(configPath, "utf-8");
@@ -7481,541 +6829,42 @@ var AIService = class {
7481
6829
  confidence: 0.8
7482
6830
  },
7483
6831
  {
7484
- name: "functionName",
7485
- value: "get_user",
7486
- reasoning: "Extracted from the code snippet",
7487
- confidence: 0.9
7488
- }
7489
- ]
7490
- });
7491
- } else if (content.includes("pattern") || content.includes("regex")) {
7492
- response = JSON.stringify({
7493
- suggestions: [
7494
- {
7495
- id: "custom-sql-pattern",
7496
- pattern: "execute\\s*\\(.*\\+",
7497
- description: "Detects SQL execution with string concatenation",
7498
- confidence: "medium",
7499
- matchExample: "cursor.execute(query + user_input)",
7500
- safeExample: "cursor.execute(query, (user_input,))",
7501
- reasoning: "String concatenation in SQL queries is a common injection vector"
7502
- }
7503
- ]
7504
- });
7505
- }
7506
- return {
7507
- success: true,
7508
- data: response,
7509
- usage: { inputTokens: 100, outputTokens: 50 },
7510
- durationMs: Date.now() - startTime
7511
- };
7512
- }
7513
- };
7514
- function createAIService(config2) {
7515
- return new AIService(config2);
7516
- }
7517
-
7518
- // src/ai/template-filler.ts
7519
- var SYSTEM_PROMPT = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
7520
- Given a code snippet and a list of template variables, suggest appropriate values for each variable.
7521
-
7522
- For each variable, analyze:
7523
- 1. The code snippet to extract relevant information (class names, function names, etc.)
7524
- 2. The variable description to understand what's needed
7525
- 3. The variable type to ensure correct formatting
7526
-
7527
- Always respond with valid JSON matching this structure:
7528
- {
7529
- "suggestions": [
7530
- {
7531
- "name": "variableName",
7532
- "value": "suggested value",
7533
- "reasoning": "why this value was chosen",
7534
- "confidence": 0.0-1.0
7535
- }
7536
- ]
7537
- }
7538
-
7539
- For arrays, use: "value": ["item1", "item2"]
7540
- For booleans, use: "value": true or "value": false
7541
- For numbers, use: "value": 42`;
7542
- async function suggestVariables(request, config2) {
7543
- const ai = createAIService(config2);
7544
- if (!ai.isConfigured()) {
7545
- return {
7546
- success: true,
7547
- data: extractVariablesFromCode(request),
7548
- durationMs: 0
7549
- };
7550
- }
7551
- const prompt = buildVariablePrompt(request);
7552
- const startTime = Date.now();
7553
- const response = await ai.completeJSON({
7554
- systemPrompt: SYSTEM_PROMPT,
7555
- messages: [{ role: "user", content: prompt }],
7556
- maxTokens: 1024,
7557
- temperature: 0.2
7558
- });
7559
- if (!response.success || !response.data) {
7560
- return {
7561
- success: true,
7562
- data: extractVariablesFromCode(request),
7563
- durationMs: Date.now() - startTime
7564
- };
7565
- }
7566
- const suggestions = /* @__PURE__ */ new Map();
7567
- const unfilled = [];
7568
- const values = { ...request.existingValues };
7569
- const suggestionsList = response.data.suggestions ?? [];
7570
- for (const suggestion of suggestionsList) {
7571
- suggestions.set(suggestion.name, suggestion);
7572
- if (!(suggestion.name in values)) {
7573
- values[suggestion.name] = suggestion.value;
7574
- }
7575
- }
7576
- for (const variable of request.variables) {
7577
- if (!suggestions.has(variable.name) && !(variable.name in values)) {
7578
- if (variable.defaultValue !== void 0) {
7579
- values[variable.name] = variable.defaultValue;
7580
- } else {
7581
- unfilled.push(variable.name);
7582
- }
7583
- }
7584
- }
7585
- const result = {
7586
- success: true,
7587
- data: { suggestions, unfilled, values },
7588
- durationMs: response.durationMs
7589
- };
7590
- if (response.usage) {
7591
- result.usage = response.usage;
7592
- }
7593
- return result;
7594
- }
7595
- function buildVariablePrompt(request) {
7596
- const parts = [];
7597
- parts.push("Analyze this code and suggest values for the template variables:\n");
7598
- parts.push("**Code:**");
7599
- parts.push("```");
7600
- parts.push(request.codeSnippet);
7601
- parts.push("```\n");
7602
- parts.push(`**File:** ${request.filePath}
7603
- `);
7604
- if (request.gap) {
7605
- parts.push(`**Category:** ${request.gap.categoryName}`);
7606
- parts.push(`**Line:** ${request.gap.lineStart}
7607
- `);
7608
- }
7609
- parts.push("**Variables to fill:**");
7610
- for (const variable of request.variables) {
7611
- const required = variable.required ? " (required)" : " (optional)";
7612
- const defaultVal = variable.defaultValue !== void 0 ? ` [default: ${JSON.stringify(variable.defaultValue)}]` : "";
7613
- parts.push(`- ${variable.name} (${variable.type})${required}${defaultVal}: ${variable.description}`);
7614
- }
7615
- if (request.existingValues && Object.keys(request.existingValues).length > 0) {
7616
- parts.push("\n**Already provided:**");
7617
- for (const [name, value] of Object.entries(request.existingValues)) {
7618
- parts.push(`- ${name}: ${JSON.stringify(value)}`);
7619
- }
7620
- }
7621
- parts.push("\nExtract appropriate values from the code context.");
7622
- return parts.join("\n");
7623
- }
7624
- function extractVariablesFromCode(request) {
7625
- const suggestions = /* @__PURE__ */ new Map();
7626
- const unfilled = [];
7627
- const values = { ...request.existingValues };
7628
- const code = request.codeSnippet;
7629
- const filePath = request.filePath;
7630
- for (const variable of request.variables) {
7631
- if (variable.name in values) continue;
7632
- let value = void 0;
7633
- let reasoning = "";
7634
- let confidence = 0;
7635
- switch (variable.name.toLowerCase()) {
7636
- case "classname":
7637
- case "class_name": {
7638
- const classMatch = code.match(/class\s+(\w+)/);
7639
- if (classMatch) {
7640
- value = classMatch[1];
7641
- reasoning = "Extracted from class definition in code";
7642
- confidence = 0.9;
7643
- } else {
7644
- const fileName = filePath.split("/").pop()?.replace(/\.\w+$/, "") ?? "";
7645
- value = toPascalCase(fileName);
7646
- reasoning = "Inferred from file name";
7647
- confidence = 0.6;
7648
- }
7649
- break;
7650
- }
7651
- case "functionname":
7652
- case "function_name":
7653
- case "methodname": {
7654
- const funcMatch = code.match(/(?:def|function|async function)\s+(\w+)/);
7655
- if (funcMatch) {
7656
- value = funcMatch[1];
7657
- reasoning = "Extracted from function definition";
7658
- confidence = 0.9;
7659
- }
7660
- break;
7661
- }
7662
- case "modulepath":
7663
- case "module_path": {
7664
- value = filePath.replace(/\.[jt]sx?$/, "").replace(/\.py$/, "").replace(/\//g, ".").replace(/^\.+/, "");
7665
- reasoning = "Derived from file path";
7666
- confidence = 0.7;
7667
- break;
7668
- }
7669
- case "tablename":
7670
- case "table_name": {
7671
- const tableMatch = code.match(/(?:FROM|INTO|UPDATE)\s+(\w+)/i);
7672
- if (tableMatch) {
7673
- value = tableMatch[1];
7674
- reasoning = "Extracted from SQL statement";
7675
- confidence = 0.8;
7676
- } else {
7677
- value = "users";
7678
- reasoning = "Default table name";
7679
- confidence = 0.3;
7680
- }
7681
- break;
7682
- }
7683
- case "exceptionclass":
7684
- case "exception_class": {
7685
- value = "ValueError";
7686
- reasoning = "Common exception for input validation";
7687
- confidence = 0.5;
7688
- break;
7689
- }
7690
- case "dbclient":
7691
- case "db_client": {
7692
- const clientMatch = code.match(/(db|conn|connection|client|cursor)\s*[=.]/i);
7693
- if (clientMatch) {
7694
- value = clientMatch[1];
7695
- reasoning = "Extracted from code";
7696
- confidence = 0.7;
7697
- } else {
7698
- value = "db";
7699
- reasoning = "Default database client name";
7700
- confidence = 0.4;
7701
- }
7702
- break;
7703
- }
7704
- case "functioncall":
7705
- case "function_call": {
7706
- const funcName = values["functionName"] ?? values["function_name"];
7707
- if (typeof funcName === "string" && funcName.length > 0) {
7708
- value = `${funcName}(user_input)`;
7709
- reasoning = "Constructed from function name";
7710
- confidence = 0.6;
7711
- }
7712
- break;
7713
- }
7714
- case "fixtures": {
7715
- value = "db_session";
7716
- reasoning = "Common pytest fixture";
7717
- confidence = 0.5;
7718
- break;
7719
- }
7720
- default:
7721
- if (variable.defaultValue !== void 0) {
7722
- value = variable.defaultValue;
7723
- reasoning = "Using default value";
7724
- confidence = 1;
7725
- }
7726
- }
7727
- if (value !== void 0) {
7728
- suggestions.set(variable.name, {
7729
- name: variable.name,
7730
- value,
7731
- reasoning,
7732
- confidence
7733
- });
7734
- values[variable.name] = value;
7735
- } else if (variable.required) {
7736
- unfilled.push(variable.name);
7737
- }
7738
- }
7739
- return { suggestions, unfilled, values };
7740
- }
7741
- function toPascalCase(str) {
7742
- return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
7743
- }
7744
-
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("");
7771
- }
7772
- lines.push(test.result.content);
7773
- lines.push("");
7774
- lines.push(chalk5.gray("\u2500".repeat(60)));
7775
- }
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)"));
7785
- }
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)
6832
+ name: "functionName",
6833
+ value: "get_user",
6834
+ reasoning: "Extracted from the code snippet",
6835
+ confidence: 0.9
6836
+ }
6837
+ ]
6838
+ });
6839
+ } else if (content.includes("pattern") || content.includes("regex")) {
6840
+ response = JSON.stringify({
6841
+ suggestions: [
6842
+ {
6843
+ id: "custom-sql-pattern",
6844
+ pattern: "execute\\s*\\(.*\\+",
6845
+ description: "Detects SQL execution with string concatenation",
6846
+ confidence: "medium",
6847
+ matchExample: "cursor.execute(query + user_input)",
6848
+ safeExample: "cursor.execute(query, (user_input,))",
6849
+ reasoning: "String concatenation in SQL queries is a common injection vector"
6850
+ }
6851
+ ]
7970
6852
  });
7971
6853
  }
6854
+ return {
6855
+ success: true,
6856
+ data: response,
6857
+ usage: { inputTokens: 100, outputTokens: 50 },
6858
+ durationMs: Date.now() - startTime
6859
+ };
7972
6860
  }
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");
6861
+ };
6862
+ function createAIService(config2) {
6863
+ return new AIService(config2);
8015
6864
  }
8016
6865
 
8017
6866
  // src/ai/explainer.ts
8018
- var SYSTEM_PROMPT2 = `You are a security expert explaining code vulnerabilities to developers.
6867
+ var SYSTEM_PROMPT = `You are a security expert explaining code vulnerabilities to developers.
8019
6868
  Your explanations should be:
8020
6869
  - Clear and actionable
8021
6870
  - Focused on the specific code pattern
@@ -8042,13 +6891,30 @@ async function explainGap(gap, category, config2) {
8042
6891
  }
8043
6892
  const prompt = buildExplainPrompt(gap);
8044
6893
  const response = await ai.completeJSON({
8045
- systemPrompt: SYSTEM_PROMPT2,
6894
+ systemPrompt: SYSTEM_PROMPT,
8046
6895
  messages: [{ role: "user", content: prompt }],
8047
6896
  maxTokens: 1024,
8048
6897
  temperature: 0.3
8049
6898
  });
8050
6899
  return response;
8051
6900
  }
6901
+ async function explainGaps(gaps, categories, config2) {
6902
+ const results = /* @__PURE__ */ new Map();
6903
+ const BATCH_SIZE = 5;
6904
+ for (let i = 0; i < gaps.length; i += BATCH_SIZE) {
6905
+ const batch = gaps.slice(i, i + BATCH_SIZE);
6906
+ const promises = batch.map(async (gap) => {
6907
+ const category = categories?.get(gap.categoryId);
6908
+ const result = await explainGap(gap, category, config2);
6909
+ return { key: `${gap.filePath}:${gap.lineStart}:${gap.categoryId}`, result };
6910
+ });
6911
+ const batchResults = await Promise.all(promises);
6912
+ for (const { key, result } of batchResults) {
6913
+ results.set(key, result);
6914
+ }
6915
+ }
6916
+ return results;
6917
+ }
8052
6918
  function buildExplainPrompt(gap, category) {
8053
6919
  const parts = [];
8054
6920
  parts.push(`Explain this security finding:
@@ -8109,7 +6975,7 @@ function generateFallbackExplanation(gap) {
8109
6975
  }
8110
6976
 
8111
6977
  // src/ai/pattern-suggester.ts
8112
- var SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
6978
+ var SYSTEM_PROMPT2 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
8113
6979
  Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
8114
6980
 
8115
6981
  Your patterns should:
@@ -8148,7 +7014,7 @@ async function suggestPatterns(request, config2) {
8148
7014
  }
8149
7015
  const prompt = buildPatternPrompt(request);
8150
7016
  const response = await ai.completeJSON({
8151
- systemPrompt: SYSTEM_PROMPT3,
7017
+ systemPrompt: SYSTEM_PROMPT2,
8152
7018
  messages: [{ role: "user", content: prompt }],
8153
7019
  maxTokens: 2048,
8154
7020
  temperature: 0.3
@@ -8277,73 +7143,89 @@ function hasRedosPotential(pattern) {
8277
7143
 
8278
7144
  // src/cli/index.ts
8279
7145
  init_results_cache();
7146
+ var __filename2 = fileURLToPath(import.meta.url);
7147
+ var __dirname2 = dirname(__filename2);
7148
+ function getDefinitionsPath() {
7149
+ const candidates = [
7150
+ resolve(__dirname2, "../../src/categories/definitions"),
7151
+ resolve(process.cwd(), "src/categories/definitions"),
7152
+ resolve(__dirname2, "../categories/definitions")
7153
+ ];
7154
+ for (const candidate of candidates) {
7155
+ if (existsSync(candidate)) {
7156
+ return candidate;
7157
+ }
7158
+ }
7159
+ return candidates[0];
7160
+ }
7161
+ init_results_cache();
8280
7162
  var SEVERITY_COLORS2 = {
8281
- critical: chalk5.red.bold,
8282
- high: chalk5.red,
8283
- medium: chalk5.yellow,
8284
- low: chalk5.gray
7163
+ critical: chalk6.red.bold,
7164
+ high: chalk6.red,
7165
+ medium: chalk6.yellow,
7166
+ low: chalk6.gray
8285
7167
  };
8286
7168
  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
7169
+ security: chalk6.red,
7170
+ data: chalk6.blue,
7171
+ concurrency: chalk6.magenta,
7172
+ input: chalk6.cyan,
7173
+ resource: chalk6.yellow,
7174
+ reliability: chalk6.green,
7175
+ performance: chalk6.yellowBright,
7176
+ platform: chalk6.gray,
7177
+ business: chalk6.white,
7178
+ compliance: chalk6.blueBright
8297
7179
  };
8298
7180
  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
7181
+ A: chalk6.green.bold,
7182
+ B: chalk6.green,
7183
+ C: chalk6.yellow,
7184
+ D: chalk6.red,
7185
+ F: chalk6.red.bold
8304
7186
  };
8305
7187
  var BANNER = `
8306
- ${chalk5.cyan(" ____ _ _ ")}
8307
- ${chalk5.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
8308
- ${chalk5.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
8309
- ${chalk5.cyan("| __/| | | | | (_| | || (_| |")}
8310
- ${chalk5.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
7188
+ ${chalk6.cyan(" ____ _ _ ")}
7189
+ ${chalk6.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
7190
+ ${chalk6.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
7191
+ ${chalk6.cyan("| __/| | | | | (_| | || (_| |")}
7192
+ ${chalk6.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
8311
7193
  `;
8312
7194
  function formatScanTerminal(result, basePath) {
8313
7195
  const lines = [];
8314
7196
  lines.push(BANNER);
8315
- lines.push(chalk5.gray(`Analyzing: ${result.targetDirectory}`));
7197
+ lines.push(chalk6.gray(`Analyzing: ${result.targetDirectory}`));
8316
7198
  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)}`));
7199
+ lines.push(chalk6.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
7200
+ lines.push(chalk6.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
8319
7201
  lines.push("");
8320
7202
  lines.push(formatScoreBox(result.score));
8321
7203
  lines.push("");
8322
- lines.push(chalk5.bold("Domain Coverage:"));
7204
+ lines.push(chalk6.bold("Domain Coverage:"));
8323
7205
  lines.push(formatDomainCoverage(result.coverage));
8324
7206
  lines.push("");
8325
7207
  if (result.gaps.length > 0) {
8326
7208
  lines.push(formatGapsSummary(result.gaps, basePath));
8327
7209
  lines.push("");
8328
7210
  } else {
8329
- lines.push(chalk5.green.bold("No gaps detected! Your codebase has good test coverage."));
7211
+ lines.push(chalk6.green.bold("No vulnerabilities detected."));
8330
7212
  lines.push("");
8331
7213
  }
8332
7214
  if (result.gaps.length > 0) {
8333
- lines.push(chalk5.gray("Run `pinata generate --gaps` to create tests for these gaps."));
7215
+ lines.push(chalk6.gray("Run `pinata generate --gaps` to create tests for these gaps."));
8334
7216
  }
8335
- lines.push(chalk5.gray(`
7217
+ lines.push(chalk6.gray(`
8336
7218
  Scan completed in ${result.durationMs}ms`));
8337
7219
  return lines.join("\n");
8338
7220
  }
8339
7221
  function formatScoreBox(score) {
8340
- const gradeColor = GRADE_COLORS[score.grade] ?? chalk5.white;
7222
+ const gradeColor = GRADE_COLORS[score.grade] ?? chalk6.white;
8341
7223
  const scoreStr = `Pinata Score: ${score.overall}/100 ${gradeColor(`(${score.grade})`)}`;
8342
7224
  const boxWidth = 60;
8343
7225
  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");
7226
+ const top = chalk6.cyan("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
7227
+ const middle = chalk6.cyan("\u2551") + " ".repeat(padding) + scoreStr + " ".repeat(boxWidth - padding - scoreStr.length) + chalk6.cyan("\u2551");
7228
+ const bottom = chalk6.cyan("\u255A" + "\u2550".repeat(boxWidth) + "\u255D");
8347
7229
  return `${top}
8348
7230
  ${middle}
8349
7231
  ${bottom}`;
@@ -8358,14 +7240,14 @@ function formatDomainCoverage(coverage) {
8358
7240
  }
8359
7241
  const percent = domainCoverage.coveragePercent;
8360
7242
  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;
7243
+ const bar = chalk6.green("\u2588".repeat(filledWidth)) + chalk6.gray("\u2591".repeat(barWidth - filledWidth));
7244
+ const domainColor = DOMAIN_COLORS2[domain] ?? chalk6.white;
8363
7245
  const domainName = domain.padEnd(15);
8364
7246
  const stats = `${domainCoverage.categoriesCovered}/${domainCoverage.categoriesScanned} categories`;
8365
7247
  lines.push(` ${domainColor(domainName)} ${bar} ${percent.toString().padStart(3)}% (${stats})`);
8366
7248
  }
8367
7249
  if (lines.length === 0) {
8368
- lines.push(chalk5.gray(" No domain coverage data available."));
7250
+ lines.push(chalk6.gray(" No domain coverage data available."));
8369
7251
  }
8370
7252
  return lines.join("\n");
8371
7253
  }
@@ -8376,48 +7258,48 @@ function formatGapsSummary(gaps, basePath) {
8376
7258
  const medium = gaps.filter((g) => g.severity === "medium");
8377
7259
  const low = gaps.filter((g) => g.severity === "low");
8378
7260
  if (critical.length > 0) {
8379
- lines.push(chalk5.red.bold(`
7261
+ lines.push(chalk6.red.bold(`
8380
7262
  Critical Gaps (${critical.length}):`));
8381
7263
  for (const gap of critical.slice(0, 5)) {
8382
7264
  lines.push(formatGapLine(gap, basePath, "critical"));
8383
7265
  }
8384
7266
  if (critical.length > 5) {
8385
- lines.push(chalk5.gray(` ... and ${critical.length - 5} more critical gaps`));
7267
+ lines.push(chalk6.gray(` ... and ${critical.length - 5} more critical gaps`));
8386
7268
  }
8387
7269
  }
8388
7270
  if (high.length > 0) {
8389
- lines.push(chalk5.red(`
7271
+ lines.push(chalk6.red(`
8390
7272
  High Severity Gaps (${high.length}):`));
8391
7273
  for (const gap of high.slice(0, 5)) {
8392
7274
  lines.push(formatGapLine(gap, basePath, "high"));
8393
7275
  }
8394
7276
  if (high.length > 5) {
8395
- lines.push(chalk5.gray(` ... and ${high.length - 5} more high severity gaps`));
7277
+ lines.push(chalk6.gray(` ... and ${high.length - 5} more high severity gaps`));
8396
7278
  }
8397
7279
  }
8398
7280
  if (medium.length > 0) {
8399
- lines.push(chalk5.yellow(`
7281
+ lines.push(chalk6.yellow(`
8400
7282
  Medium Severity Gaps (${medium.length}):`));
8401
7283
  for (const gap of medium.slice(0, 3)) {
8402
7284
  lines.push(formatGapLine(gap, basePath, "medium"));
8403
7285
  }
8404
7286
  if (medium.length > 3) {
8405
- lines.push(chalk5.gray(` ... and ${medium.length - 3} more medium severity gaps`));
7287
+ lines.push(chalk6.gray(` ... and ${medium.length - 3} more medium severity gaps`));
8406
7288
  }
8407
7289
  }
8408
7290
  if (low.length > 0) {
8409
- lines.push(chalk5.gray(`
7291
+ lines.push(chalk6.gray(`
8410
7292
  Low Severity: ${low.length} gaps`));
8411
7293
  }
8412
7294
  return lines.join("\n");
8413
7295
  }
8414
7296
  function formatGapLine(gap, basePath, severity) {
8415
- const severityColor = SEVERITY_COLORS2[severity] ?? chalk5.white;
7297
+ const severityColor = SEVERITY_COLORS2[severity] ?? chalk6.white;
8416
7298
  const icon = severity === "critical" ? "\u26D4" : severity === "high" ? "\u{1F534}" : severity === "medium" ? "\u{1F7E1}" : "\u26AA";
8417
7299
  const relPath = relative(basePath, gap.filePath);
8418
7300
  const location = `${relPath}:${gap.lineStart}`;
8419
7301
  const confidence = gap.confidence.toUpperCase();
8420
- return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk5.cyan(location.padEnd(30))} ${chalk5.gray(confidence)} confidence`;
7302
+ return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk6.cyan(location.padEnd(30))} ${chalk6.gray(confidence)} confidence`;
8421
7303
  }
8422
7304
  function formatLanguages(result) {
8423
7305
  const languages = [];
@@ -8520,7 +7402,7 @@ _...and ${gaps.length - 10} more ${severity} gaps_
8520
7402
  }
8521
7403
  } else {
8522
7404
  lines.push("## No Gaps Detected\n");
8523
- lines.push("Your codebase has good test coverage.\n");
7405
+ lines.push("No vulnerabilities detected.\n");
8524
7406
  }
8525
7407
  lines.push("## Summary\n");
8526
7408
  lines.push(`- Total Gaps: ${result.summary.totalGaps}`);
@@ -8577,7 +7459,7 @@ function buildSarifResults(result, basePath) {
8577
7459
  ruleId: gap.categoryId,
8578
7460
  level: sarifLevel(gap.severity),
8579
7461
  message: {
8580
- text: `Missing test coverage for ${gap.categoryName}`
7462
+ text: `Potential vulnerability: ${gap.categoryName}`
8581
7463
  },
8582
7464
  locations: [
8583
7465
  {
@@ -8640,637 +7522,1023 @@ function isValidScanOutputFormat(format) {
8640
7522
  return ["terminal", "json", "markdown", "sarif", "html", "junit-xml"].includes(format);
8641
7523
  }
8642
7524
 
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;
7525
+ // src/cli/commands/analyze.ts
7526
+ function registerAnalyzeCommand(program2) {
7527
+ program2.command("analyze [path]").description("Scan codebase for security vulnerabilities").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) => {
7528
+ const isQuiet = Boolean(options["quiet"]);
7529
+ const isVerbose = Boolean(options["verbose"]);
7530
+ if (isQuiet) {
7531
+ logger.configure({ level: "error" });
7532
+ } else if (isVerbose) {
7533
+ logger.configure({ level: "debug" });
8658
7534
  }
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
- }
7535
+ const targetDirectory = resolve(targetPath ?? process.cwd());
7536
+ if (!existsSync(targetDirectory)) {
7537
+ console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
7538
+ process.exit(1);
8703
7539
  }
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));
7540
+ const outputFormat = String(options["output"] ?? "terminal");
7541
+ if (!isValidScanOutputFormat(outputFormat)) {
7542
+ console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
8723
7543
  process.exit(1);
8724
7544
  }
8725
- if (spinner) {
8726
- spinner.text = `Loaded ${loadResult.data} categories. Scanning...`;
7545
+ const validSeverities = ["critical", "high", "medium", "low"];
7546
+ const minSeverity = String(options["severity"] ?? "low");
7547
+ if (!validSeverities.includes(minSeverity)) {
7548
+ console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
7549
+ process.exit(1);
8727
7550
  }
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;
7551
+ const validConfidences = ["high", "medium", "low"];
7552
+ const minConfidence = String(options["confidence"] ?? "high");
7553
+ if (!validConfidences.includes(minConfidence)) {
7554
+ console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
7555
+ process.exit(1);
8737
7556
  }
8738
- if (excludeDirs) {
8739
- scanOptions.excludeDirs = excludeDirs;
7557
+ const domainsStr = options["domains"];
7558
+ let domains = [];
7559
+ if (domainsStr) {
7560
+ const domainList = domainsStr.split(",").map((d) => d.trim());
7561
+ for (const domain of domainList) {
7562
+ if (!RISK_DOMAINS.includes(domain)) {
7563
+ console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
7564
+ process.exit(1);
7565
+ }
7566
+ }
7567
+ domains = domainList;
8740
7568
  }
8741
- const scanResult = await scanner.scanDirectory(targetDirectory, scanOptions);
8742
- if (!scanResult.success) {
8743
- spinner?.fail("Scan failed");
8744
- console.error(formatError(scanResult.error));
7569
+ const excludeStr = options["exclude"];
7570
+ const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
7571
+ const failOn = options["failOn"];
7572
+ if (failOn && !["critical", "high", "medium"].includes(failOn)) {
7573
+ console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
8745
7574
  process.exit(1);
8746
7575
  }
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"));
7576
+ const showSpinner = outputFormat === "terminal" && !isQuiet;
7577
+ const spinner = showSpinner ? ora3("Loading categories...").start() : null;
7578
+ try {
7579
+ const store = createCategoryStore();
7580
+ const definitionsPath = getDefinitionsPath();
7581
+ logger.debug(`Loading categories from: ${definitionsPath}`);
7582
+ const loadResult = await store.loadFromDirectory(definitionsPath);
7583
+ if (!loadResult.success) {
7584
+ spinner?.fail("Failed to load categories");
7585
+ console.error(formatError(loadResult.error));
7586
+ process.exit(1);
7587
+ }
7588
+ if (spinner) {
7589
+ spinner.text = `Loaded ${loadResult.data} categories. Scanning...`;
7590
+ }
7591
+ const scanner = createScanner(store);
7592
+ const scanOptions = {
7593
+ minSeverity,
7594
+ minConfidence,
7595
+ detectTestFiles: true
7596
+ };
7597
+ if (domains.length > 0) {
7598
+ scanOptions.domains = domains;
7599
+ }
7600
+ if (excludeDirs) {
7601
+ scanOptions.excludeDirs = excludeDirs;
7602
+ }
7603
+ const scanResult = await scanner.scanDirectory(targetDirectory, scanOptions);
7604
+ if (!scanResult.success) {
7605
+ spinner?.fail("Scan failed");
7606
+ console.error(formatError(scanResult.error));
7607
+ process.exit(1);
7608
+ }
7609
+ spinner?.stop();
7610
+ const shouldVerify = Boolean(options["verify"]);
7611
+ if (shouldVerify && scanResult.data.gaps.length > 0) {
7612
+ const { hasApiKey: hasApiKey2, setConfigValue: setConfigValue2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
7613
+ const { createInterface } = await import('readline');
7614
+ let provider = "anthropic";
7615
+ if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
7616
+ spinner?.stop();
7617
+ console.log(chalk6.yellow("\nAI verification requires an API key."));
7618
+ console.log(chalk6.gray("Get one at: https://console.anthropic.com/settings/keys"));
7619
+ console.log(chalk6.gray("Or: https://platform.openai.com/api-keys\n"));
7620
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
7621
+ const askQuestion = (question) => new Promise((resolve9) => rl.question(question, (answer) => resolve9(answer.trim())));
7622
+ const apiKey = await askQuestion(chalk6.cyan("Enter your Anthropic or OpenAI API key: "));
7623
+ rl.close();
7624
+ if (!apiKey) {
7625
+ console.log(chalk6.red("No API key provided. Skipping AI verification."));
8773
7626
  } else {
8774
- setConfigValue2("openaiApiKey", apiKey);
8775
- provider = "openai";
8776
- console.log(chalk5.green("OpenAI API key saved to ~/.pinata/config.json\n"));
7627
+ if (apiKey.startsWith("sk-ant-")) {
7628
+ setConfigValue2("anthropicApiKey", apiKey);
7629
+ provider = "anthropic";
7630
+ console.log(chalk6.green("Anthropic API key saved to ~/.pinata/config.json\n"));
7631
+ } else {
7632
+ setConfigValue2("openaiApiKey", apiKey);
7633
+ provider = "openai";
7634
+ console.log(chalk6.green("OpenAI API key saved to ~/.pinata/config.json\n"));
7635
+ }
8777
7636
  }
7637
+ } else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
7638
+ provider = "openai";
8778
7639
  }
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)}...`));
7640
+ if (hasApiKey2(provider)) {
7641
+ const verifySpinner = showSpinner ? ora3("Verifying gaps with AI...").start() : null;
7642
+ try {
7643
+ const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
7644
+ const { readFile: readFile7 } = await import('fs/promises');
7645
+ const apiKey = getApiKey2(provider);
7646
+ const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
7647
+ const { verified, dismissed, stats } = await verifier.verifyAll(
7648
+ scanResult.data.gaps,
7649
+ async (path2) => readFile7(path2, "utf-8")
7650
+ );
7651
+ scanResult.data.gaps = verified;
7652
+ const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
7653
+ let deduction = 0;
7654
+ for (const gap of verified) {
7655
+ deduction += severityWeights[gap.severity] ?? 1;
7656
+ }
7657
+ const newOverall = Math.max(0, 100 - deduction);
7658
+ const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
7659
+ scanResult.data.score.overall = newOverall;
7660
+ scanResult.data.score.grade = newGrade;
7661
+ verifySpinner?.succeed(
7662
+ `AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
7663
+ );
7664
+ if (isVerbose && dismissed.length > 0) {
7665
+ console.log(chalk6.gray("\nDismissed as false positives:"));
7666
+ for (const { gap, reason } of dismissed.slice(0, 5)) {
7667
+ console.log(chalk6.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
7668
+ console.log(chalk6.gray(` Reason: ${reason.slice(0, 100)}...`));
7669
+ }
7670
+ if (dismissed.length > 5) {
7671
+ console.log(chalk6.gray(` ... and ${dismissed.length - 5} more`));
7672
+ }
8812
7673
  }
8813
- if (dismissed.length > 5) {
8814
- console.log(chalk5.gray(` ... and ${dismissed.length - 5} more`));
7674
+ } catch (error) {
7675
+ verifySpinner?.fail("AI verification failed (results unverified)");
7676
+ if (isVerbose) {
7677
+ console.error(chalk6.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
8815
7678
  }
8816
7679
  }
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
7680
  }
8823
7681
  }
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}`));
7682
+ const shouldExecute = Boolean(options["execute"]);
7683
+ const isDryRun = Boolean(options["dryRun"]);
7684
+ if (shouldExecute && scanResult.data.gaps.length > 0) {
7685
+ const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
7686
+ const { readFile: readFile7 } = await import('fs/promises');
7687
+ const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
7688
+ if (testableGaps.length === 0) {
7689
+ console.log(chalk6.yellow("\nNo dynamically testable gaps found."));
8840
7690
  } 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 {
7691
+ const runner = createRunner2(void 0, isDryRun);
7692
+ const initResult = await runner.initialize();
7693
+ if (!initResult.ready) {
7694
+ console.log(chalk6.red(`
7695
+ Dynamic execution unavailable: ${initResult.error}`));
7696
+ } else {
7697
+ const fileContents = /* @__PURE__ */ new Map();
7698
+ for (const gap of testableGaps) {
7699
+ if (!fileContents.has(gap.filePath)) {
7700
+ try {
7701
+ fileContents.set(gap.filePath, await readFile7(gap.filePath, "utf-8"));
7702
+ } catch {
7703
+ }
8847
7704
  }
8848
7705
  }
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;
7706
+ const executionSummary = await runner.executeAll(testableGaps, fileContents);
7707
+ for (const result of executionSummary.results) {
7708
+ const gap = scanResult.data.gaps.find(
7709
+ (g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
7710
+ );
7711
+ if (gap && result.status === "confirmed") {
7712
+ gap.confirmed = true;
7713
+ gap.evidence = result.evidence;
7714
+ }
8858
7715
  }
8859
- }
8860
- if (executionSummary.confirmed > 0) {
8861
- console.log(chalk5.red.bold(`
7716
+ if (executionSummary.confirmed > 0) {
7717
+ console.log(chalk6.red.bold(`
8862
7718
  \u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
7719
+ }
8863
7720
  }
8864
7721
  }
8865
7722
  }
7723
+ const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
7724
+ if (!cacheResult.success) {
7725
+ logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
7726
+ }
7727
+ const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
7728
+ const outputFile = options["outputFile"];
7729
+ if (outputFile) {
7730
+ writeFileSync(resolve(outputFile), output, "utf-8");
7731
+ logger.info(`Results written to: ${resolve(outputFile)}`);
7732
+ } else {
7733
+ console.log(output);
7734
+ }
7735
+ if (isVerbose && scanResult.data.warnings.length > 0) {
7736
+ console.error("\nWarnings:");
7737
+ for (const warning of scanResult.data.warnings) {
7738
+ console.error(` - ${warning}`);
7739
+ }
7740
+ }
7741
+ if (failOn) {
7742
+ const severityOrder = { critical: 3, high: 2, medium: 1 };
7743
+ const failLevel = severityOrder[failOn] ?? 0;
7744
+ const hasFailingGaps = scanResult.data.gaps.some((gap) => (severityOrder[gap.severity] ?? 0) >= failLevel);
7745
+ if (hasFailingGaps) {
7746
+ process.exit(1);
7747
+ }
7748
+ }
7749
+ process.exit(0);
7750
+ } catch (error) {
7751
+ spinner?.fail("Analysis failed");
7752
+ console.error(formatError(error instanceof Error ? error : new Error(String(error))));
7753
+ process.exit(1);
7754
+ }
7755
+ });
7756
+ }
7757
+ init_results_cache();
7758
+ var FRAMEWORK_INDICATORS = {
7759
+ vitest: {
7760
+ deps: [],
7761
+ devDeps: ["vitest"],
7762
+ files: ["vitest.config.ts", "vitest.config.js", "vitest.config.mts"]
7763
+ },
7764
+ jest: {
7765
+ deps: [],
7766
+ devDeps: ["jest", "@jest/core", "ts-jest"],
7767
+ files: ["jest.config.ts", "jest.config.js", "jest.config.mjs"]
7768
+ },
7769
+ pytest: {
7770
+ deps: ["pytest"],
7771
+ devDeps: ["pytest"],
7772
+ files: ["pytest.ini", "pyproject.toml", "setup.cfg", "conftest.py"]
7773
+ },
7774
+ "go-test": {
7775
+ deps: [],
7776
+ devDeps: [],
7777
+ files: ["go.mod"]
7778
+ },
7779
+ mocha: {
7780
+ deps: [],
7781
+ devDeps: ["mocha"],
7782
+ files: [".mocharc.yml", ".mocharc.js"]
7783
+ }
7784
+ };
7785
+ var FRAMEWORK_DEFAULTS = {
7786
+ vitest: {
7787
+ name: "vitest",
7788
+ importStyle: 'import { describe, it, expect, beforeEach } from "vitest";',
7789
+ runner: "npx vitest run"
7790
+ },
7791
+ jest: {
7792
+ name: "jest",
7793
+ importStyle: "// jest globals auto-imported",
7794
+ runner: "npx jest"
7795
+ },
7796
+ pytest: {
7797
+ name: "pytest",
7798
+ importStyle: "import pytest",
7799
+ runner: "pytest"
7800
+ },
7801
+ "go-test": {
7802
+ name: "go-test",
7803
+ importStyle: 'import "testing"',
7804
+ runner: "go test ./..."
7805
+ },
7806
+ mocha: {
7807
+ name: "mocha",
7808
+ importStyle: 'import { describe, it } from "mocha"; import { expect } from "chai";',
7809
+ runner: "npx mocha"
7810
+ }
7811
+ };
7812
+ var EXT_TO_LANG = {
7813
+ ".ts": "typescript",
7814
+ ".tsx": "typescript",
7815
+ ".js": "javascript",
7816
+ ".jsx": "javascript",
7817
+ ".py": "python",
7818
+ ".go": "go",
7819
+ ".java": "java",
7820
+ ".rs": "rust"
7821
+ };
7822
+ var WEB_FRAMEWORK_PATTERNS = [
7823
+ [/from\s+["']express["']|require\s*\(\s*["']express["']\)/, "express"],
7824
+ [/from\s+["']fastify["']/, "fastify"],
7825
+ [/from\s+["']koa["']/, "koa"],
7826
+ [/from\s+["']@nestjs\//, "nestjs"],
7827
+ [/from\s+["']next["']|from\s+["']next\//, "nextjs"],
7828
+ [/from\s+flask\s+import|import\s+flask/, "flask"],
7829
+ [/from\s+django|import\s+django/, "django"],
7830
+ [/from\s+fastapi|import\s+fastapi/, "fastapi"],
7831
+ [/"github\.com\/gin-gonic\/gin"/, "gin"],
7832
+ [/"github\.com\/gofiber\/fiber"/, "fiber"]
7833
+ ];
7834
+ var DB_PATTERNS = [
7835
+ [/prisma|@prisma\/client/, "postgres"],
7836
+ [/pg\b|postgres|postgresql/, "postgres"],
7837
+ [/mysql2?["'\s]|from\s+["']mysql/, "mysql"],
7838
+ [/mongoose|mongodb|MongoClient/, "mongodb"],
7839
+ [/sqlite3|better-sqlite/, "sqlite"],
7840
+ [/sqlalchemy|psycopg2/, "postgres"],
7841
+ [/pymysql|mysqlclient/, "mysql"]
7842
+ ];
7843
+ function extractFunction(source, targetLine, language) {
7844
+ const lines = source.split("\n");
7845
+ const idx = targetLine - 1;
7846
+ if (idx < 0 || idx >= lines.length) {
7847
+ const start = Math.max(0, idx - 10);
7848
+ const end = Math.min(lines.length, idx + 10);
7849
+ return { body: lines.slice(start, end).join("\n"), name: void 0 };
7850
+ }
7851
+ if (language === "python") {
7852
+ return extractPythonFunction(lines, idx);
7853
+ }
7854
+ return extractBraceFunction(lines, idx);
7855
+ }
7856
+ function findBalancedEnd(lines, startLine) {
7857
+ let depth = 0;
7858
+ let started = false;
7859
+ let endIdx = startLine;
7860
+ for (let i = startLine; i < lines.length; i++) {
7861
+ const line = lines[i];
7862
+ for (const ch of line) {
7863
+ if (ch === "{") {
7864
+ depth++;
7865
+ started = true;
7866
+ }
7867
+ if (ch === "}") depth--;
7868
+ if (started && depth === 0) {
7869
+ endIdx = i;
7870
+ return { endIdx, started };
7871
+ }
7872
+ }
7873
+ }
7874
+ return { endIdx, started };
7875
+ }
7876
+ var SIGNATURE_PATTERN = /^(export\s+)?(async\s+)?function\s|^(export\s+)?(const|let|var)\s+\w+\s*=|^\w+\s*\(|^(public|private|protected)\s/;
7877
+ var NAME_PATTERN = /function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=|(\w+)\s*\(/;
7878
+ function extractBraceFunction(lines, targetIdx) {
7879
+ let startIdx = targetIdx;
7880
+ let braceDepth = 0;
7881
+ let foundOpenBrace = false;
7882
+ for (let i = targetIdx; i >= 0; i--) {
7883
+ const line = lines[i];
7884
+ for (let j = line.length - 1; j >= 0; j--) {
7885
+ if (line[j] === "}") braceDepth++;
7886
+ if (line[j] === "{") {
7887
+ braceDepth--;
7888
+ if (braceDepth < 0) {
7889
+ startIdx = i;
7890
+ foundOpenBrace = true;
7891
+ break;
7892
+ }
7893
+ }
8866
7894
  }
8867
- const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
8868
- if (!cacheResult.success) {
8869
- logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
7895
+ if (foundOpenBrace) break;
7896
+ }
7897
+ let sigStart = startIdx;
7898
+ for (let i = startIdx; i >= Math.max(0, startIdx - 5); i--) {
7899
+ const line = lines[i].trim();
7900
+ if (line.match(SIGNATURE_PATTERN)) {
7901
+ sigStart = i;
7902
+ break;
8870
7903
  }
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}`);
8877
- } else {
8878
- console.log(output);
7904
+ }
7905
+ const { endIdx } = findBalancedEnd(lines, sigStart);
7906
+ const body = lines.slice(sigStart, endIdx + 1).join("\n");
7907
+ const sigLine = lines[sigStart]?.trim() ?? "";
7908
+ const nameMatch = sigLine.match(NAME_PATTERN);
7909
+ const name = nameMatch?.[1] ?? nameMatch?.[2] ?? nameMatch?.[3];
7910
+ return { body, name };
7911
+ }
7912
+ var PYTHON_DEF_PATTERN = /^(\s*)def\s+\w+|^(\s*)class\s+\w+|^(\s*)async\s+def\s+\w+/;
7913
+ var PYTHON_INDENT_PATTERN = /^(\s*)/;
7914
+ var PYTHON_NAME_PATTERN = /def\s+(\w+)|class\s+(\w+)/;
7915
+ function extractPythonFunction(lines, targetIdx) {
7916
+ let startIdx = targetIdx;
7917
+ for (let i = targetIdx; i >= 0; i--) {
7918
+ if (lines[i].match(PYTHON_DEF_PATTERN)) {
7919
+ startIdx = i;
7920
+ break;
7921
+ }
7922
+ }
7923
+ const indent = (lines[startIdx].match(PYTHON_INDENT_PATTERN) ?? ["", ""])[1].length;
7924
+ let endIdx = targetIdx;
7925
+ for (let i = startIdx + 1; i < lines.length; i++) {
7926
+ const line = lines[i];
7927
+ const trimmedLine = line.trim();
7928
+ if (trimmedLine === "") continue;
7929
+ const lineIndent = (line.match(PYTHON_INDENT_PATTERN) ?? ["", ""])[1].length;
7930
+ if (lineIndent <= indent && trimmedLine !== "") {
7931
+ endIdx = i - 1;
7932
+ break;
8879
7933
  }
8880
- if (isVerbose && scanResult.data.warnings.length > 0) {
8881
- console.error("\nWarnings:");
8882
- for (const warning of scanResult.data.warnings) {
8883
- console.error(` - ${warning}`);
7934
+ endIdx = i;
7935
+ }
7936
+ const body = lines.slice(startIdx, endIdx + 1).join("\n");
7937
+ const nameMatch = lines[startIdx].match(PYTHON_NAME_PATTERN);
7938
+ const name = nameMatch?.[1] ?? nameMatch?.[2];
7939
+ return { body, name };
7940
+ }
7941
+ var REQUIRE_PATTERN = /^const\s+\w+\s*=\s*require\(/;
7942
+ function extractImports(source, language) {
7943
+ const lines = source.split("\n");
7944
+ const imports = [];
7945
+ for (const line of lines) {
7946
+ const trimmed = line.trim();
7947
+ if (language === "python") {
7948
+ if (trimmed.startsWith("import ") || trimmed.startsWith("from ")) {
7949
+ imports.push(trimmed);
7950
+ }
7951
+ } else if (language === "typescript" || language === "javascript") {
7952
+ if (trimmed.startsWith("import ") || trimmed.match(REQUIRE_PATTERN)) {
7953
+ imports.push(trimmed);
7954
+ }
7955
+ } else if (language === "go") {
7956
+ if (trimmed.startsWith("import ") || trimmed.startsWith('"')) {
7957
+ imports.push(trimmed);
7958
+ }
7959
+ } else if (language === "java") {
7960
+ if (trimmed.startsWith("import ")) {
7961
+ imports.push(trimmed);
8884
7962
  }
8885
7963
  }
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);
7964
+ }
7965
+ return imports;
7966
+ }
7967
+ async function detectTestFramework(projectRoot, language) {
7968
+ if (language === "typescript" || language === "javascript") {
7969
+ const pkgPath = resolve(projectRoot, "package.json");
7970
+ if (existsSync(pkgPath)) {
7971
+ try {
7972
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
7973
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7974
+ for (const [framework, indicators] of Object.entries(FRAMEWORK_INDICATORS)) {
7975
+ for (const dep of [...indicators.deps, ...indicators.devDeps]) {
7976
+ if (dep in allDeps) {
7977
+ return FRAMEWORK_DEFAULTS[framework];
7978
+ }
7979
+ }
7980
+ }
7981
+ } catch {
8904
7982
  }
8905
7983
  }
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);
7984
+ for (const [framework, indicators] of Object.entries(FRAMEWORK_INDICATORS)) {
7985
+ for (const file of indicators.files) {
7986
+ if (existsSync(resolve(projectRoot, file))) {
7987
+ return FRAMEWORK_DEFAULTS[framework];
7988
+ }
7989
+ }
7990
+ }
7991
+ return language === "typescript" ? FRAMEWORK_DEFAULTS["vitest"] : FRAMEWORK_DEFAULTS["jest"];
8911
7992
  }
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" });
7993
+ if (language === "python") return FRAMEWORK_DEFAULTS["pytest"];
7994
+ if (language === "go") return FRAMEWORK_DEFAULTS["go-test"];
7995
+ return FRAMEWORK_DEFAULTS["vitest"];
7996
+ }
7997
+ async function findExistingTest(filePath, projectRoot) {
7998
+ const base = basename(filePath, extname(filePath));
7999
+ const dir = dirname(filePath);
8000
+ const rel = relative(projectRoot, dir);
8001
+ const candidates = [
8002
+ resolve(dir, `${base}.test${extname(filePath)}`),
8003
+ resolve(dir, `${base}.spec${extname(filePath)}`),
8004
+ resolve(dir, `__tests__/${base}${extname(filePath)}`),
8005
+ resolve(projectRoot, "tests", rel, `${base}.test${extname(filePath)}`),
8006
+ resolve(projectRoot, "test", rel, `${base}.test${extname(filePath)}`)
8007
+ ];
8008
+ for (const candidate of candidates) {
8009
+ if (existsSync(candidate)) {
8010
+ try {
8011
+ const content = await readFile(candidate, "utf-8");
8012
+ return content.split("\n").slice(0, 50).join("\n");
8013
+ } catch {
8014
+ }
8015
+ }
8924
8016
  }
8925
- if (!["terminal", "json"].includes(outputFormat)) {
8926
- console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
8927
- process.exit(1);
8017
+ return void 0;
8018
+ }
8019
+ function suggestTestPath(gap, projectRoot, language) {
8020
+ relative(projectRoot, gap.filePath);
8021
+ const base = basename(gap.filePath, extname(gap.filePath));
8022
+ const ext = language === "python" ? ".py" : extname(gap.filePath);
8023
+ const safeCategory = gap.categoryId.replace(/[^a-z0-9-]/g, "-");
8024
+ return resolve(projectRoot, "tests", "security", `${safeCategory}-${base}.test${ext}`);
8025
+ }
8026
+ function detectFromCode(source, patterns) {
8027
+ for (const [pattern, name] of patterns) {
8028
+ if (pattern.test(source)) return name;
8928
8029
  }
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);
8030
+ return void 0;
8031
+ }
8032
+ async function extractTestContext(gap, projectRoot) {
8033
+ const ext = extname(gap.filePath);
8034
+ const language = EXT_TO_LANG[ext] ?? "unknown";
8035
+ const fileSource = await readFile(gap.filePath, "utf-8");
8036
+ const { body: functionBody, name: functionName } = extractFunction(fileSource, gap.lineStart, language);
8037
+ const imports = extractImports(fileSource, language);
8038
+ const testFramework = await detectTestFramework(projectRoot, language);
8039
+ const webFramework = detectFromCode(fileSource, WEB_FRAMEWORK_PATTERNS);
8040
+ const dbType = detectFromCode(fileSource, DB_PATTERNS);
8041
+ const existingTestSample = await findExistingTest(gap.filePath, projectRoot);
8042
+ const suggestedTestPath = suggestTestPath(gap, projectRoot, language);
8043
+ return {
8044
+ gap,
8045
+ fileSource,
8046
+ functionBody,
8047
+ functionName,
8048
+ imports,
8049
+ language,
8050
+ testFramework,
8051
+ suggestedTestPath,
8052
+ webFramework,
8053
+ dbType,
8054
+ existingTestSample,
8055
+ projectRoot
8056
+ };
8057
+ }
8058
+ async function extractTestContexts(gaps, projectRoot) {
8059
+ const contexts = [];
8060
+ for (const gap of gaps) {
8061
+ try {
8062
+ const ctx = await extractTestContext(gap, projectRoot);
8063
+ contexts.push(ctx);
8064
+ } catch {
8065
+ }
8937
8066
  }
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);
8067
+ return contexts;
8068
+ }
8069
+
8070
+ // src/testgen/generator.ts
8071
+ function buildGenerationPrompt(ctx) {
8072
+ const parts = [];
8073
+ parts.push(`Generate a complete, runnable ${ctx.testFramework.name} test file for this security vulnerability.`);
8074
+ parts.push("");
8075
+ parts.push("## Vulnerability");
8076
+ parts.push(`Type: ${ctx.gap.categoryId}`);
8077
+ parts.push(`Severity: ${ctx.gap.severity}`);
8078
+ parts.push(`File: ${ctx.gap.filePath}:${ctx.gap.lineStart}`);
8079
+ parts.push(`Pattern: ${ctx.gap.patternId}`);
8080
+ parts.push("");
8081
+ parts.push("## Vulnerable Code");
8082
+ parts.push("```");
8083
+ parts.push(ctx.functionBody);
8084
+ parts.push("```");
8085
+ parts.push("");
8086
+ if (ctx.functionName) {
8087
+ parts.push(`Function name: ${ctx.functionName}`);
8943
8088
  }
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);
8089
+ parts.push("## File Imports");
8090
+ parts.push("```");
8091
+ parts.push(ctx.imports.join("\n"));
8092
+ parts.push("```");
8093
+ parts.push("");
8094
+ parts.push("## Context");
8095
+ parts.push(`Language: ${ctx.language}`);
8096
+ parts.push(`Test framework: ${ctx.testFramework.name}`);
8097
+ if (ctx.webFramework) parts.push(`Web framework: ${ctx.webFramework}`);
8098
+ if (ctx.dbType) parts.push(`Database: ${ctx.dbType}`);
8099
+ parts.push("");
8100
+ if (ctx.existingTestSample) {
8101
+ parts.push("## Existing Test Style (match this style)");
8102
+ parts.push("```");
8103
+ parts.push(ctx.existingTestSample.slice(0, 1500));
8104
+ parts.push("```");
8105
+ parts.push("");
8106
+ }
8107
+ parts.push("## Requirements");
8108
+ parts.push("1. Output ONLY the complete test file. No explanations, no markdown fences.");
8109
+ parts.push("2. Use real imports that resolve in this project.");
8110
+ parts.push("3. The test MUST FAIL when run against the current vulnerable code.");
8111
+ parts.push("4. Include at least 5 attack payloads specific to this vulnerability type.");
8112
+ parts.push("5. Include at least one boundary/edge case (empty string, null, very long input, unicode).");
8113
+ parts.push("6. If testing an HTTP endpoint, use supertest or direct function calls.");
8114
+ parts.push("7. Test the specific vulnerable code path, not a generic function.");
8115
+ parts.push("8. Each test should have a clear assertion that proves the vulnerability exists or is mitigated.");
8116
+ return parts.join("\n");
8117
+ }
8118
+ function buildPropertyPrompt(ctx) {
8119
+ const parts = [];
8120
+ parts.push(`Generate a property-based test using fast-check (TypeScript) or hypothesis (Python) for this security vulnerability.`);
8121
+ parts.push("");
8122
+ parts.push("## Vulnerability");
8123
+ parts.push(`Type: ${ctx.gap.categoryId}`);
8124
+ parts.push(`File: ${ctx.gap.filePath}:${ctx.gap.lineStart}`);
8125
+ parts.push("");
8126
+ parts.push("## Vulnerable Code");
8127
+ parts.push("```");
8128
+ parts.push(ctx.functionBody);
8129
+ parts.push("```");
8130
+ parts.push("");
8131
+ parts.push("## Requirements");
8132
+ parts.push("1. Output ONLY the complete test file. No explanations.");
8133
+ parts.push("2. Express a security INVARIANT as a property.");
8134
+ parts.push("3. The property should hold for ALL inputs, not just specific payloads.");
8135
+ parts.push("4. Use fast-check for TypeScript/JavaScript or hypothesis for Python.");
8136
+ parts.push("5. Example invariant: 'for all strings s, the output of sanitize(s) never contains <script>'");
8137
+ parts.push(`6. Test framework: ${ctx.testFramework.name}`);
8138
+ const invariantHints = {
8139
+ "sql-injection": "user input should never appear unescaped in the SQL query string",
8140
+ "xss": "user input should never appear as raw HTML in the output",
8141
+ "command-injection": "user input should never be passed to a shell command unescaped",
8142
+ "path-traversal": "resolved file path should always stay within the allowed directory",
8143
+ "ssrf": "user-supplied URL should never resolve to a private/internal IP",
8144
+ "xxe": "XML parsing should never resolve external entities",
8145
+ "deserialization": "deserialized objects should only be of expected types",
8146
+ "hardcoded-secrets": "no string matching secret patterns should exist in source"
8147
+ };
8148
+ const hint = invariantHints[ctx.gap.categoryId];
8149
+ if (hint) {
8150
+ parts.push(`7. Invariant hint: "${hint}"`);
8951
8151
  }
8952
- const severityOrder = {
8953
- critical: 4,
8954
- high: 3,
8955
- medium: 2,
8956
- low: 1
8152
+ return parts.join("\n");
8153
+ }
8154
+ async function generateTest(ctx, callAI) {
8155
+ const systemPrompt = [
8156
+ "You are a senior security engineer writing adversarial tests.",
8157
+ "You write tests that BREAK code, not tests that pass.",
8158
+ "Your tests must be complete, runnable files with real imports.",
8159
+ "Output ONLY code. No markdown fences. No explanations.",
8160
+ "The test must FAIL against vulnerable code and PASS after a fix."
8161
+ ].join(" ");
8162
+ const prompt = buildGenerationPrompt(ctx);
8163
+ const content = await callAI(prompt, systemPrompt);
8164
+ const cleaned = stripMarkdownFences(content);
8165
+ return {
8166
+ filePath: ctx.suggestedTestPath,
8167
+ content: cleaned,
8168
+ categoryId: ctx.gap.categoryId,
8169
+ description: `Security test for ${ctx.gap.categoryId} in ${ctx.functionName ?? "unknown function"} at ${ctx.gap.filePath}:${ctx.gap.lineStart}`,
8170
+ isPropertyBased: false
8957
8171
  };
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);
8172
+ }
8173
+ async function generatePropertyTest(ctx, callAI) {
8174
+ const systemPrompt = [
8175
+ "You are a formal verification expert writing property-based tests.",
8176
+ "Express security invariants that must hold for ALL inputs.",
8177
+ "Use fast-check for TypeScript/JavaScript or hypothesis for Python.",
8178
+ "Output ONLY code. No markdown fences. No explanations."
8179
+ ].join(" ");
8180
+ const prompt = buildPropertyPrompt(ctx);
8181
+ const content = await callAI(prompt, systemPrompt);
8182
+ const cleaned = stripMarkdownFences(content);
8183
+ const ext = ctx.language === "python" ? ".py" : ".ts";
8184
+ const propPath = ctx.suggestedTestPath.replace(/\.test\.(ts|js|py)$/, `.prop${ext}`);
8185
+ return {
8186
+ filePath: propPath,
8187
+ content: cleaned,
8188
+ categoryId: ctx.gap.categoryId,
8189
+ description: `Property-based security invariant for ${ctx.gap.categoryId}`,
8190
+ isPropertyBased: true
8191
+ };
8192
+ }
8193
+ function stripMarkdownFences(content) {
8194
+ let result = content.trim();
8195
+ if (result.startsWith("```")) {
8196
+ const firstNewline = result.indexOf("\n");
8197
+ if (firstNewline !== -1) {
8198
+ result = result.slice(firstNewline + 1);
8979
8199
  }
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);
8200
+ }
8201
+ if (result.endsWith("```")) {
8202
+ result = result.slice(0, -3).trimEnd();
8203
+ }
8204
+ return result;
8205
+ }
8206
+
8207
+ // src/cli/commands/generate.ts
8208
+ function registerGenerateCommand(program2) {
8209
+ program2.command("generate").description("Generate adversarial security tests for detected vulnerabilities").option("--gaps", "Generate tests for all detected 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("--write", "Write test files to disk").option("--property", "Also generate property-based tests (fast-check/hypothesis)").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) => {
8210
+ const isQuiet = Boolean(options["quiet"]);
8211
+ const isVerbose = Boolean(options["verbose"]);
8212
+ const shouldWrite = Boolean(options["write"]);
8213
+ const withProperty = Boolean(options["property"]);
8214
+ const aiProvider = String(options["aiProvider"] ?? "anthropic");
8215
+ const outputFormat = String(options["output"] ?? "terminal");
8216
+ if (isQuiet) {
8217
+ logger.configure({ level: "error" });
8218
+ } else if (isVerbose) {
8219
+ logger.configure({ level: "debug" });
8989
8220
  }
8990
- if (spinner) {
8991
- spinner.text = `Found ${gaps.length} gaps. Loading categories...`;
8221
+ if (!["terminal", "json"].includes(outputFormat)) {
8222
+ console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
8223
+ process.exit(1);
8992
8224
  }
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));
8225
+ const hasGaps = Boolean(options["gaps"]);
8226
+ const categoryId = options["category"];
8227
+ const domainFilter = options["domain"];
8228
+ if (!hasGaps && !categoryId && !domainFilter) {
8229
+ console.error(formatError(new Error("Specify what to generate: --gaps (all gaps), --category <id>, or --domain <domain>")));
8999
8230
  process.exit(1);
9000
8231
  }
9001
- if (spinner) {
9002
- spinner.text = `Generating tests for ${gaps.length} gaps...`;
8232
+ if (domainFilter && !RISK_DOMAINS.includes(domainFilter)) {
8233
+ console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid: ${RISK_DOMAINS.join(", ")}`)));
8234
+ process.exit(1);
9003
8235
  }
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);
8236
+ const validSeverities = ["critical", "high", "medium", "low"];
8237
+ const minSeverity = String(options["severity"] ?? "medium");
8238
+ if (!validSeverities.includes(minSeverity)) {
8239
+ console.error(formatError(new Error(`Invalid severity: ${minSeverity}`)));
8240
+ process.exit(1);
9012
8241
  }
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;
8242
+ const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
8243
+ const showSpinner = outputFormat === "terminal" && !isQuiet;
8244
+ const spinner = showSpinner ? ora3("Loading scan results...").start() : null;
8245
+ try {
8246
+ const projectRoot = process.cwd();
8247
+ const cacheResult = await loadScanResults(projectRoot);
8248
+ if (!cacheResult.success) {
8249
+ spinner?.fail("No cached results");
8250
+ console.error(formatError(cacheResult.error));
8251
+ console.error(chalk6.yellow("\nRun `pinata analyze` first."));
8252
+ process.exit(1);
9018
8253
  }
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];
8254
+ let gaps = cacheResult.data.gaps;
8255
+ if (categoryId) {
8256
+ gaps = gaps.filter((g) => g.categoryId === categoryId);
8257
+ }
8258
+ if (domainFilter) {
8259
+ gaps = gaps.filter((g) => g.domain === domainFilter);
8260
+ }
8261
+ gaps = gaps.filter((g) => (severityOrder[g.severity] ?? 0) >= (severityOrder[minSeverity] ?? 0));
8262
+ const seen = /* @__PURE__ */ new Set();
8263
+ gaps = gaps.filter((g) => {
8264
+ const key = `${g.categoryId}:${g.filePath}`;
8265
+ if (seen.has(key)) return false;
8266
+ seen.add(key);
8267
+ return true;
8268
+ });
8269
+ if (gaps.length === 0) {
8270
+ spinner?.succeed("No gaps match filters");
8271
+ console.log(chalk6.yellow("\nNo gaps to generate tests for."));
8272
+ process.exit(0);
8273
+ }
8274
+ if (spinner) {
8275
+ spinner.text = `Extracting context for ${gaps.length} findings...`;
8276
+ }
8277
+ const contexts = await extractTestContexts(gaps, projectRoot);
8278
+ if (contexts.length === 0) {
8279
+ spinner?.fail("Failed to extract context from any finding");
8280
+ process.exit(1);
8281
+ }
8282
+ if (spinner) {
8283
+ spinner.text = `Generating tests for ${contexts.length} findings with AI...`;
8284
+ }
8285
+ const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8286
+ if (!hasApiKey2(aiProvider)) {
8287
+ spinner?.fail("No API key configured");
8288
+ console.error(chalk6.yellow(`
8289
+ AI test generation requires an API key.`));
8290
+ console.error(chalk6.gray(` pinata config set ${aiProvider === "anthropic" ? "anthropic-api-key" : "openai-api-key"} YOUR_KEY`));
8291
+ process.exit(1);
8292
+ }
8293
+ const apiKey = getApiKey2(aiProvider) ?? "";
8294
+ const callAI = buildAICaller(aiProvider, apiKey);
8295
+ const generated = [];
8296
+ const errors = [];
8297
+ for (let i = 0; i < contexts.length; i++) {
8298
+ const ctx = contexts[i];
8299
+ if (spinner) {
8300
+ spinner.text = `Generating test ${i + 1}/${contexts.length}: ${ctx.gap.categoryId} in ${relative(projectRoot, ctx.gap.filePath)}`;
9036
8301
  }
9037
- if (!template) {
9038
- errors.push(`No templates available for ${catId}`);
9039
- continue;
8302
+ try {
8303
+ const test = await generateTest(ctx, callAI);
8304
+ generated.push(test);
8305
+ if (withProperty) {
8306
+ try {
8307
+ const propTest = await generatePropertyTest(ctx, callAI);
8308
+ generated.push(propTest);
8309
+ } catch (err2) {
8310
+ errors.push(`Property test failed for ${ctx.gap.categoryId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
8311
+ }
8312
+ }
8313
+ } catch (err2) {
8314
+ errors.push(`Failed ${ctx.gap.categoryId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
9040
8315
  }
9041
- let variables;
9042
- if (useAI) {
9043
- variables = await extractVariablesWithAI(gap, template.variables, {
9044
- provider: aiProvider
9045
- });
9046
- } else {
9047
- variables = extractVariablesFromGap(gap);
8316
+ }
8317
+ spinner?.stop();
8318
+ if (generated.length === 0) {
8319
+ console.log(chalk6.red("Failed to generate any tests."));
8320
+ for (const error of errors) {
8321
+ console.error(chalk6.gray(` ${error}`));
9048
8322
  }
9049
- const renderResult = renderer.renderTemplate(template, variables);
9050
- if (!renderResult.success) {
9051
- errors.push(`Failed to render ${catId}: ${renderResult.error.message}`);
9052
- continue;
8323
+ process.exit(1);
8324
+ }
8325
+ if (outputFormat === "json") {
8326
+ console.log(JSON.stringify({
8327
+ generated: generated.map((t) => ({
8328
+ filePath: relative(projectRoot, t.filePath),
8329
+ categoryId: t.categoryId,
8330
+ description: t.description,
8331
+ isPropertyBased: t.isPropertyBased,
8332
+ lines: t.content.split("\n").length
8333
+ })),
8334
+ errors
8335
+ }, null, 2));
8336
+ } else {
8337
+ console.log();
8338
+ console.log(chalk6.bold(`Generated ${generated.length} test file${generated.length === 1 ? "" : "s"}`));
8339
+ console.log();
8340
+ for (const test of generated) {
8341
+ const relPath = relative(projectRoot, test.filePath);
8342
+ const badge = test.isPropertyBased ? chalk6.magenta(" [property]") : "";
8343
+ console.log(` ${chalk6.green("+")} ${relPath}${badge}`);
8344
+ console.log(chalk6.gray(` ${test.description}`));
9053
8345
  }
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
- });
8346
+ console.log();
9062
8347
  }
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}`));
8348
+ if (shouldWrite) {
8349
+ let written = 0;
8350
+ for (const test of generated) {
8351
+ try {
8352
+ await mkdir(dirname(test.filePath), { recursive: true });
8353
+ await writeFile(test.filePath, test.content, "utf-8");
8354
+ written++;
8355
+ } catch (err2) {
8356
+ errors.push(`Write failed: ${relative(projectRoot, test.filePath)}: ${err2 instanceof Error ? err2.message : String(err2)}`);
8357
+ }
8358
+ }
8359
+ console.log(chalk6.green(`Wrote ${written} test file${written === 1 ? "" : "s"}`));
8360
+ } else {
8361
+ console.log(chalk6.gray("Dry run. Use --write to save test files to disk."));
9074
8362
  }
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);
8363
+ if (errors.length > 0 && isVerbose) {
8364
+ console.error(chalk6.yellow("\nWarnings:"));
8365
+ for (const error of errors) {
8366
+ console.error(chalk6.gray(` ${error}`));
8367
+ }
9086
8368
  }
9087
- console.log(formatWriteSummary(writeResult.data, cached.targetDirectory));
9088
- if (writeResult.data.failed.length > 0) {
9089
- process.exit(1);
8369
+ process.exit(0);
8370
+ } catch (error) {
8371
+ spinner?.fail("Generation failed");
8372
+ console.error(formatError(error instanceof Error ? error : new Error(String(error))));
8373
+ process.exit(1);
8374
+ }
8375
+ });
8376
+ }
8377
+ function buildAICaller(provider, apiKey) {
8378
+ return async (prompt, systemPrompt) => {
8379
+ if (provider === "anthropic") {
8380
+ const response2 = await fetch("https://api.anthropic.com/v1/messages", {
8381
+ method: "POST",
8382
+ headers: {
8383
+ "Content-Type": "application/json",
8384
+ "x-api-key": apiKey,
8385
+ "anthropic-version": "2023-06-01"
8386
+ },
8387
+ body: JSON.stringify({
8388
+ model: "claude-sonnet-4-20250514",
8389
+ max_tokens: 4096,
8390
+ system: systemPrompt,
8391
+ messages: [{ role: "user", content: prompt }]
8392
+ })
8393
+ });
8394
+ if (!response2.ok) {
8395
+ const body = await response2.text();
8396
+ throw new Error(`Anthropic API error: ${response2.status} - ${body}`);
9090
8397
  }
8398
+ const data2 = await response2.json();
8399
+ return data2.content[0]?.text ?? "";
9091
8400
  }
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
- });
8401
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
8402
+ method: "POST",
8403
+ headers: {
8404
+ "Content-Type": "application/json",
8405
+ Authorization: `Bearer ${apiKey}`
8406
+ },
8407
+ body: JSON.stringify({
8408
+ model: "gpt-4o",
8409
+ max_tokens: 4096,
8410
+ messages: [
8411
+ { role: "system", content: systemPrompt },
8412
+ { role: "user", content: prompt }
8413
+ ]
8414
+ })
8415
+ });
8416
+ if (!response.ok) {
8417
+ const body = await response.text();
8418
+ throw new Error(`OpenAI API error: ${response.status} - ${body}`);
8419
+ }
8420
+ const data = await response.json();
8421
+ return data.choices[0]?.message?.content ?? "";
8422
+ };
8423
+ }
8424
+
8425
+ // src/cli/index.ts
8426
+ var program = new Command();
8427
+ program.name("pinata").description("AI-powered security vulnerability detection").version(VERSION);
8428
+ registerAnalyzeCommand(program);
8429
+ registerGenerateCommand(program);
9099
8430
  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
8431
  const isQuiet = Boolean(options["quiet"]);
9101
8432
  const isVerbose = Boolean(options["verbose"]);
8433
+ const topN = parseInt(String(options["top"] ?? "5"), 10);
8434
+ const categoryFilter = options["category"];
8435
+ const domainFilter = options["domain"];
9102
8436
  const useAI = Boolean(options["ai"]);
9103
8437
  const aiProvider = String(options["aiProvider"] ?? "anthropic");
9104
8438
  const outputFormat = String(options["output"] ?? "terminal");
9105
- const topN = parseInt(String(options["top"] ?? "5"), 10);
9106
8439
  if (isQuiet) {
9107
8440
  logger.configure({ level: "error" });
9108
8441
  } else if (isVerbose) {
9109
8442
  logger.configure({ level: "debug" });
9110
8443
  }
9111
8444
  if (!["terminal", "json", "markdown"].includes(outputFormat)) {
9112
- console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown`)));
8445
+ console.error(formatError(new Error(`Invalid format: ${outputFormat}. Use: terminal, json, markdown`)));
9113
8446
  process.exit(1);
9114
8447
  }
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."));
8448
+ const projectRoot = process.cwd();
8449
+ const cacheResult = await loadScanResults(projectRoot);
8450
+ if (!cacheResult.success) {
8451
+ console.error(formatError(cacheResult.error));
8452
+ console.error(chalk6.yellow("\nRun `pinata analyze` first to scan for gaps."));
8453
+ process.exit(1);
8454
+ }
8455
+ const cached = cacheResult.data;
8456
+ let gaps = cached.gaps;
8457
+ if (categoryFilter) {
8458
+ gaps = gaps.filter((g) => g.categoryId === categoryFilter);
8459
+ }
8460
+ if (domainFilter) {
8461
+ if (!RISK_DOMAINS.includes(domainFilter)) {
8462
+ console.error(formatError(new Error(`Invalid domain: ${domainFilter}`)));
9124
8463
  process.exit(1);
9125
8464
  }
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);
8465
+ gaps = gaps.filter((g) => g.domain === domainFilter);
8466
+ }
8467
+ gaps = gaps.slice(0, topN);
8468
+ if (gaps.length === 0) {
8469
+ console.log(chalk6.yellow("No gaps to explain."));
8470
+ process.exit(0);
8471
+ }
8472
+ let explanations;
8473
+ if (useAI) {
8474
+ const spinner = ora3("Generating AI explanations...").start();
8475
+ try {
8476
+ const aiConfig = { provider: aiProvider };
8477
+ const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8478
+ if (hasApiKey2(aiProvider)) {
8479
+ const key = getApiKey2(aiProvider);
8480
+ if (key) {
8481
+ aiConfig.apiKey = key;
8482
+ }
9138
8483
  }
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(`
8484
+ const resultMap = await explainGaps(gaps, void 0, aiConfig);
8485
+ explanations = gaps.map((g) => {
8486
+ const key = `${g.categoryId}:${g.filePath}:${g.lineStart}`;
8487
+ const result = resultMap.get(key);
8488
+ if (result?.success && result.data) {
8489
+ return result.data;
8490
+ }
8491
+ return generateFallbackExplanation(g);
8492
+ });
8493
+ spinner.succeed(`Generated ${explanations.length} explanations`);
8494
+ } catch (error) {
8495
+ spinner.fail("AI explanation failed");
8496
+ console.error(chalk6.yellow(`
9156
8497
  Set ${aiProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} for AI explanations.
9157
8498
  `));
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
- }
8499
+ explanations = gaps.map((g) => generateFallbackExplanation(g));
9184
8500
  }
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}
8501
+ } else {
8502
+ explanations = gaps.map((g) => generateFallbackExplanation(g));
8503
+ }
8504
+ if (outputFormat === "json") {
8505
+ const output = gaps.map((g, i) => ({ gap: { categoryId: g.categoryId, severity: g.severity, filePath: g.filePath, lineStart: g.lineStart }, ...explanations[i] }));
8506
+ console.log(JSON.stringify(output, null, 2));
8507
+ } else if (outputFormat === "markdown") {
8508
+ console.log("# Gap Explanations\n");
8509
+ for (let i = 0; i < explanations.length; i++) {
8510
+ const exp = explanations[i];
8511
+ const gap = gaps[i];
8512
+ console.log(`## ${exp.summary}
9219
8513
  `);
9220
- console.log(`### How to Fix
9221
- ${explanation.remediation}
8514
+ console.log(`**Severity**: ${gap.severity} | **Category**: ${gap.categoryId}
9222
8515
  `);
9223
- if (explanation.safeExample) {
9224
- console.log(`### Safe Example
9225
- \`\`\`
9226
- ${explanation.safeExample}
9227
- \`\`\`
8516
+ console.log(exp.explanation);
8517
+ if (exp.remediation) {
8518
+ console.log(`
8519
+ **Remediation**: ${exp.remediation}
9228
8520
  `);
9229
- }
9230
- console.log("---\n");
9231
8521
  }
9232
- } else {
8522
+ console.log("---\n");
8523
+ }
8524
+ } else {
8525
+ console.log();
8526
+ for (let i = 0; i < explanations.length; i++) {
8527
+ const exp = explanations[i];
8528
+ const gap = gaps[i];
8529
+ const severityColor = gap.severity === "critical" ? chalk6.red : gap.severity === "high" ? chalk6.yellow : chalk6.blue;
8530
+ console.log(`${severityColor.bold(`[${gap.severity.toUpperCase()}]`)} ${chalk6.bold(exp.summary)}`);
8531
+ console.log(chalk6.gray(` Category: ${gap.categoryId} | ${gap.filePath}:${gap.lineStart}`));
9233
8532
  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
- }
8533
+ for (const line of exp.explanation.split("\n")) {
8534
+ console.log(` ${line}`);
8535
+ }
8536
+ if (exp.remediation) {
9265
8537
  console.log();
9266
- console.log(chalk5.gray("\u2500".repeat(60)));
8538
+ console.log(chalk6.green(` Fix: ${exp.remediation}`));
9267
8539
  }
8540
+ console.log();
9268
8541
  }
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
8542
  }
9275
8543
  });
9276
8544
  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 +8546,73 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
9278
8546
  const language = String(options["language"]);
9279
8547
  const aiProvider = String(options["aiProvider"] ?? "anthropic");
9280
8548
  const outputFormat = String(options["output"] ?? "terminal");
9281
- const codeSnippets = options["code"];
9282
8549
  const filePath = options["file"];
9283
- let vulnerableCode = [...codeSnippets];
8550
+ const codeSnippets = options["code"] ?? [];
8551
+ const samples = [...codeSnippets];
9284
8552
  if (filePath) {
8553
+ const { readFile: readFile7 } = await import('fs/promises');
9285
8554
  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)];
8555
+ const content = await readFile7(resolve(filePath), "utf-8");
8556
+ samples.push(...content.split("\n---\n").filter((s) => s.trim()));
9289
8557
  } catch (error) {
9290
8558
  console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
9291
8559
  process.exit(1);
9292
8560
  }
9293
8561
  }
9294
- if (vulnerableCode.length === 0) {
8562
+ if (samples.length === 0) {
9295
8563
  console.error(formatError(new Error("Provide code samples via --code or --file")));
9296
8564
  process.exit(1);
9297
8565
  }
9298
- const spinner = ora("Generating pattern suggestions...").start();
8566
+ const spinner = ora3("Generating pattern suggestions with AI...").start();
9299
8567
  try {
8568
+ const aiConfig = { provider: aiProvider };
8569
+ const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8570
+ if (hasApiKey2(aiProvider)) {
8571
+ const key = getApiKey2(aiProvider);
8572
+ if (key) {
8573
+ aiConfig.apiKey = key;
8574
+ }
8575
+ }
9300
8576
  const result = await suggestPatterns(
9301
- {
9302
- category: categoryId,
9303
- language,
9304
- vulnerableCode,
9305
- maxSuggestions: 5
9306
- },
9307
- { provider: aiProvider }
8577
+ { category: categoryId, vulnerableCode: samples, language },
8578
+ aiConfig
9308
8579
  );
9309
- spinner.stop();
9310
- if (!result.success) {
9311
- console.error(formatError(new Error(result.error ?? "Failed to generate patterns")));
8580
+ if (!result.success || !result.data) {
8581
+ spinner.fail("Pattern suggestion failed");
8582
+ console.error(chalk6.red(result.error ?? "Unknown error"));
9312
8583
  process.exit(1);
9313
8584
  }
9314
- const { suggestions, rejected } = result.data ?? { suggestions: [], rejected: [] };
8585
+ const suggestions = result.data.suggestions;
8586
+ spinner.succeed(`Generated ${suggestions.length} pattern suggestions`);
9315
8587
  if (outputFormat === "json") {
9316
- console.log(JSON.stringify({ suggestions, rejected }, null, 2));
8588
+ console.log(JSON.stringify(suggestions, null, 2));
9317
8589
  } 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}`);
8590
+ for (const s of suggestions) {
8591
+ console.log("---");
8592
+ console.log(`id: ${s.id}`);
8593
+ console.log(`pattern: "${s.pattern}"`);
8594
+ console.log(`confidence: ${s.confidence}`);
8595
+ console.log(`description: "${s.description}"`);
8596
+ console.log(`matchExample: "${s.matchExample}"`);
8597
+ console.log(`safeExample: "${s.safeExample}"`);
9329
8598
  console.log();
9330
8599
  }
9331
8600
  } else {
9332
8601
  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) {
8602
+ console.log(chalk6.bold(`Suggested Patterns for ${categoryId}`));
8603
+ console.log();
8604
+ for (const s of suggestions) {
8605
+ const confColor = s.confidence === "high" ? chalk6.green : s.confidence === "medium" ? chalk6.yellow : chalk6.red;
8606
+ console.log(` ${chalk6.cyan(s.id)} [${confColor(s.confidence)}]`);
8607
+ console.log(` Pattern: ${chalk6.gray(s.pattern)}`);
8608
+ console.log(` ${s.description}`);
8609
+ console.log(` Match: ${chalk6.gray(s.matchExample.slice(0, 80))}`);
8610
+ console.log(` Safe: ${chalk6.gray(s.safeExample.slice(0, 80))}`);
9361
8611
  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
8612
  }
9367
8613
  }
9368
- process.exit(0);
9369
8614
  } catch (error) {
9370
- spinner.fail("Pattern generation failed");
8615
+ spinner.fail("Pattern suggestion failed");
9371
8616
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9372
8617
  process.exit(1);
9373
8618
  }
@@ -9422,9 +8667,7 @@ program.command("search <query>").description("Search category taxonomy by name,
9422
8667
  }
9423
8668
  const langFilter = options["language"];
9424
8669
  if (langFilter) {
9425
- results = results.filter(
9426
- (cat) => cat.applicableLanguages.includes(langFilter)
9427
- );
8670
+ results = results.filter((cat) => cat.applicableLanguages.includes(langFilter));
9428
8671
  }
9429
8672
  if (outputFormat === "json") {
9430
8673
  console.log(JSON.stringify(results, null, 2));
@@ -9446,19 +8689,18 @@ ${cat.description}
9446
8689
  }
9447
8690
  } else {
9448
8691
  console.log();
9449
- console.log(chalk5.bold(`Search Results for "${query}"`));
9450
- console.log(chalk5.gray(`Found ${results.length} matching categories.`));
8692
+ console.log(chalk6.bold(`Search Results for "${query}"`));
8693
+ console.log(chalk6.gray(`Found ${results.length} matching categories.`));
9451
8694
  console.log();
9452
8695
  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."));
8696
+ console.log(chalk6.yellow("No categories match your search."));
9455
8697
  } else {
9456
8698
  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)}`);
8699
+ const domainColor = cat.domain === "security" ? chalk6.red : chalk6.blue;
8700
+ console.log(` ${chalk6.cyan(cat.id)} - ${chalk6.bold(cat.name)}`);
9459
8701
  console.log(` ${domainColor(cat.domain)} | ${cat.level} | ${cat.priority}`);
9460
8702
  if (options["verbose"]) {
9461
- console.log(` ${chalk5.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
8703
+ console.log(` ${chalk6.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
9462
8704
  }
9463
8705
  console.log();
9464
8706
  }
@@ -9492,21 +8734,17 @@ program.command("list").description("List all categories").option("-d, --domain
9492
8734
  process.exit(1);
9493
8735
  }
9494
8736
  const priorityFilter = options["priority"];
9495
- const validPriorities = ["P0", "P1", "P2"];
9496
- if (priorityFilter !== void 0 && !validPriorities.includes(priorityFilter)) {
8737
+ if (priorityFilter !== void 0 && !["P0", "P1", "P2"].includes(priorityFilter)) {
9497
8738
  console.error(formatError(new Error(`Invalid priority: ${priorityFilter}. Use: P0, P1, P2`)));
9498
8739
  process.exit(1);
9499
8740
  }
9500
- logger.debug("Loading categories...");
9501
8741
  const store = createCategoryStore();
9502
8742
  const definitionsPath = getDefinitionsPath();
9503
- logger.debug(`Loading from: ${definitionsPath}`);
9504
8743
  const loadResult = await store.loadFromDirectory(definitionsPath);
9505
8744
  if (!loadResult.success) {
9506
8745
  console.error(formatError(loadResult.error));
9507
8746
  process.exit(1);
9508
8747
  }
9509
- logger.debug(`Loaded ${loadResult.data} categories`);
9510
8748
  const filter = {};
9511
8749
  if (domainFilter) {
9512
8750
  filter.domain = domainFilter;
@@ -9530,54 +8768,42 @@ program.command("init").description("Initialize Pinata configuration in project"
9530
8768
  const configPath = resolve(process.cwd(), ".pinata.yml");
9531
8769
  const cacheDir = resolve(process.cwd(), ".pinata");
9532
8770
  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."));
8771
+ console.log(chalk6.yellow("Configuration file already exists at .pinata.yml"));
8772
+ console.log(chalk6.gray("Use --force to overwrite."));
9535
8773
  process.exit(0);
9536
8774
  }
9537
8775
  const defaultConfig = `# Pinata Configuration
9538
8776
  # https://github.com/pinata/pinata
9539
8777
 
9540
- # Paths to analyze
9541
8778
  include:
9542
8779
  - "src/**/*.ts"
9543
8780
  - "src/**/*.tsx"
9544
8781
  - "src/**/*.py"
9545
8782
  - "src/**/*.js"
9546
8783
 
9547
- # Paths to exclude from analysis
9548
8784
  exclude:
9549
8785
  - "node_modules/**"
9550
8786
  - "dist/**"
9551
8787
  - "build/**"
9552
8788
  - "**/*.test.ts"
9553
8789
  - "**/*.spec.ts"
9554
- - "**/test/**"
9555
- - "**/tests/**"
9556
- - "**/__tests__/**"
9557
8790
 
9558
- # Risk domains to analyze
9559
- # Options: security, data, concurrency, input, resource, reliability, performance, platform, business, compliance
9560
8791
  domains:
9561
8792
  - security
9562
8793
  - data
9563
8794
  - concurrency
9564
8795
  - input
9565
8796
 
9566
- # Minimum severity to report
9567
- # Options: critical, high, medium, low
9568
8797
  minSeverity: medium
9569
8798
 
9570
- # Output configuration
9571
8799
  output:
9572
- format: terminal # terminal, json, markdown, sarif, html
8800
+ format: terminal
9573
8801
  color: true
9574
8802
 
9575
- # Test generation settings
9576
8803
  generate:
9577
8804
  outputDir: tests/generated
9578
- framework: auto # auto, pytest, jest, vitest, mocha
8805
+ framework: auto
9579
8806
 
9580
- # Fail CI if gaps exceed thresholds
9581
8807
  thresholds:
9582
8808
  critical: 0
9583
8809
  high: 5
@@ -9586,25 +8812,23 @@ thresholds:
9586
8812
  const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
9587
8813
  try {
9588
8814
  await writeFileAsync(configPath, defaultConfig, "utf8");
9589
- console.log(chalk5.green("Created .pinata.yml"));
8815
+ console.log(chalk6.green("Created .pinata.yml"));
9590
8816
  await mkdir4(cacheDir, { recursive: true });
9591
- console.log(chalk5.green("Created .pinata/ directory"));
8817
+ console.log(chalk6.green("Created .pinata/ directory"));
9592
8818
  const gitignorePath = resolve(process.cwd(), ".gitignore");
9593
8819
  if (existsSync(gitignorePath)) {
9594
8820
  const { readFile: readFile7, appendFile } = await import('fs/promises');
9595
8821
  const gitignore = await readFile7(gitignorePath, "utf8");
9596
8822
  if (!gitignore.includes(".pinata/")) {
9597
8823
  await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
9598
- console.log(chalk5.green("Added .pinata/ to .gitignore"));
8824
+ console.log(chalk6.green("Added .pinata/ to .gitignore"));
9599
8825
  }
9600
8826
  }
9601
8827
  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"));
8828
+ console.log(chalk6.bold("Pinata initialized successfully!"));
8829
+ console.log(chalk6.gray(" 1. Review and customize .pinata.yml"));
8830
+ console.log(chalk6.gray(" 2. Run: pinata analyze"));
8831
+ console.log(chalk6.gray(" 3. Generate tests: pinata generate"));
9608
8832
  } catch (error) {
9609
8833
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9610
8834
  process.exit(1);
@@ -9617,18 +8841,15 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9617
8841
  const checkAge = Boolean(options["checkAge"]);
9618
8842
  const strictMode = Boolean(options["strict"]);
9619
8843
  const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
9620
- console.log(chalk5.bold("\nPinata Dependency Audit\n"));
8844
+ console.log(chalk6.bold("\nPinata Dependency Audit\n"));
9621
8845
  if (!existsSync(packagePath)) {
9622
- console.error(chalk5.red(`Error: ${packagePath} not found`));
8846
+ console.error(chalk6.red(`Error: ${packagePath} not found`));
9623
8847
  process.exit(1);
9624
8848
  }
9625
8849
  const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
9626
- const allDeps = {
9627
- ...packageJson.dependencies,
9628
- ...packageJson.devDependencies
9629
- };
8850
+ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
9630
8851
  const packages = Object.keys(allDeps);
9631
- console.log(chalk5.gray(`Found ${packages.length} dependencies
8852
+ console.log(chalk6.gray(`Found ${packages.length} dependencies
9632
8853
  `));
9633
8854
  const issues = [];
9634
8855
  const KNOWN_MALWARE = /* @__PURE__ */ new Set([
@@ -9651,56 +8872,31 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9651
8872
  ]);
9652
8873
  for (const pkg of packages) {
9653
8874
  if (KNOWN_MALWARE.has(pkg)) {
9654
- issues.push({
9655
- pkg,
9656
- severity: "critical",
9657
- message: "Known malicious/compromised package (Shai-Hulud/typosquat)"
9658
- });
8875
+ issues.push({ pkg, severity: "critical", message: "Known malicious/compromised package (Shai-Hulud/typosquat)" });
9659
8876
  }
9660
8877
  }
9661
8878
  for (const [pkg, version] of Object.entries(allDeps)) {
9662
8879
  if (version?.startsWith("^")) {
9663
- issues.push({
9664
- pkg,
9665
- severity: "warning",
9666
- message: `Unpinned version (${version}) - allows minor updates`
9667
- });
8880
+ issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows minor updates` });
9668
8881
  } else if (version?.startsWith("~")) {
9669
- issues.push({
9670
- pkg,
9671
- severity: "warning",
9672
- message: `Unpinned version (${version}) - allows patch updates`
9673
- });
8882
+ issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows patch updates` });
9674
8883
  } else if (version === "*" || version === "latest") {
9675
- issues.push({
9676
- pkg,
9677
- severity: "critical",
9678
- message: `Extremely dangerous version (${version}) - allows any version`
9679
- });
8884
+ issues.push({ pkg, severity: "critical", message: `Extremely dangerous version (${version}) - allows any version` });
9680
8885
  }
9681
8886
  }
9682
8887
  if (checkRegistry || doAllChecks) {
9683
- const spinner = ora("Checking npm registry...").start();
8888
+ const spinner = ora3("Checking npm registry...").start();
9684
8889
  for (const pkg of packages.slice(0, 50)) {
9685
8890
  try {
9686
8891
  const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`);
9687
8892
  if (response.status === 404) {
9688
- issues.push({
9689
- pkg,
9690
- severity: "critical",
9691
- message: "Package NOT FOUND in npm registry (slopsquatting risk)"
9692
- });
8893
+ issues.push({ pkg, severity: "critical", message: "Package NOT FOUND in npm registry (slopsquatting risk)" });
9693
8894
  } else if (response.ok) {
9694
8895
  const data = await response.json();
9695
8896
  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);
8897
+ const ageInDays = (Date.now() - new Date(data.time.created).getTime()) / (1e3 * 60 * 60 * 24);
9698
8898
  if (ageInDays < 30) {
9699
- issues.push({
9700
- pkg,
9701
- severity: "warning",
9702
- message: `Very new package (${Math.floor(ageInDays)} days old)`
9703
- });
8899
+ issues.push({ pkg, severity: "warning", message: `Very new package (${Math.floor(ageInDays)} days old)` });
9704
8900
  }
9705
8901
  }
9706
8902
  }
@@ -9712,24 +8908,24 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9712
8908
  const criticals = issues.filter((i) => i.severity === "critical");
9713
8909
  const warnings = issues.filter((i) => i.severity === "warning");
9714
8910
  if (criticals.length > 0) {
9715
- console.log(chalk5.red.bold(`
8911
+ console.log(chalk6.red.bold(`
9716
8912
  Critical Issues (${criticals.length}):`));
9717
8913
  for (const issue of criticals) {
9718
- console.log(chalk5.red(` \u2717 ${issue.pkg}: ${issue.message}`));
8914
+ console.log(chalk6.red(` \u2717 ${issue.pkg}: ${issue.message}`));
9719
8915
  }
9720
8916
  }
9721
8917
  if (warnings.length > 0) {
9722
- console.log(chalk5.yellow.bold(`
8918
+ console.log(chalk6.yellow.bold(`
9723
8919
  Warnings (${warnings.length}):`));
9724
8920
  for (const issue of warnings.slice(0, 20)) {
9725
- console.log(chalk5.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
8921
+ console.log(chalk6.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
9726
8922
  }
9727
8923
  if (warnings.length > 20) {
9728
- console.log(chalk5.gray(` ... and ${warnings.length - 20} more`));
8924
+ console.log(chalk6.gray(` ... and ${warnings.length - 20} more`));
9729
8925
  }
9730
8926
  }
9731
8927
  if (issues.length === 0) {
9732
- console.log(chalk5.green("\u2713 No dependency issues found"));
8928
+ console.log(chalk6.green("\u2713 No dependency issues found"));
9733
8929
  }
9734
8930
  console.log();
9735
8931
  if (criticals.length > 0 || strictMode && warnings.length > 0) {
@@ -9742,7 +8938,7 @@ program.command("feedback").description("View pattern performance feedback (Laye
9742
8938
  const shouldReset = Boolean(options["reset"]);
9743
8939
  if (shouldReset) {
9744
8940
  await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
9745
- console.log(chalk5.green("Feedback data reset."));
8941
+ console.log(chalk6.green("Feedback data reset."));
9746
8942
  return;
9747
8943
  }
9748
8944
  const state = await loadFeedback2();
@@ -9754,20 +8950,20 @@ program.command("feedback").description("View pattern performance feedback (Laye
9754
8950
  console.log(generateReport2(state));
9755
8951
  return;
9756
8952
  }
9757
- console.log(chalk5.bold("\nPinata Feedback Report\n"));
8953
+ console.log(chalk6.bold("\nPinata Feedback Report\n"));
9758
8954
  console.log(`Total scans: ${state.totalScans}`);
9759
8955
  console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
9760
8956
  if (state.totalScans === 0) {
9761
- console.log(chalk5.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
8957
+ console.log(chalk6.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
9762
8958
  return;
9763
8959
  }
9764
8960
  const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
9765
8961
  if (patterns.length > 0) {
9766
- console.log(chalk5.bold("\nPattern Performance:"));
8962
+ console.log(chalk6.bold("\nPattern Performance:"));
9767
8963
  for (const p of patterns.slice(0, 15)) {
9768
8964
  const total = p.confirmedCount + p.unconfirmedCount;
9769
8965
  const precisionPct = (p.precision * 100).toFixed(0);
9770
- const color = p.precision >= 0.7 ? chalk5.green : p.precision >= 0.4 ? chalk5.yellow : chalk5.red;
8966
+ const color = p.precision >= 0.7 ? chalk6.green : p.precision >= 0.4 ? chalk6.yellow : chalk6.red;
9771
8967
  console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
9772
8968
  }
9773
8969
  }
@@ -9789,76 +8985,74 @@ Examples:
9789
8985
  case "anthropic-api-key": {
9790
8986
  const validation = validateApiKey2("anthropic", value);
9791
8987
  if (!validation.valid) {
9792
- console.log(chalk5.red(`Invalid API key: ${validation.error}`));
8988
+ console.log(chalk6.red(`Invalid API key: ${validation.error}`));
9793
8989
  process.exit(1);
9794
8990
  }
9795
8991
  setConfigValue2("anthropicApiKey", value);
9796
- console.log(chalk5.green(`Anthropic API key set: ${maskApiKey2(value)}`));
8992
+ console.log(chalk6.green(`Anthropic API key set: ${maskApiKey2(value)}`));
9797
8993
  break;
9798
8994
  }
9799
8995
  case "openai-api-key": {
9800
8996
  const validation = validateApiKey2("openai", value);
9801
8997
  if (!validation.valid) {
9802
- console.log(chalk5.red(`Invalid API key: ${validation.error}`));
8998
+ console.log(chalk6.red(`Invalid API key: ${validation.error}`));
9803
8999
  process.exit(1);
9804
9000
  }
9805
9001
  setConfigValue2("openaiApiKey", value);
9806
- console.log(chalk5.green(`OpenAI API key set: ${maskApiKey2(value)}`));
9002
+ console.log(chalk6.green(`OpenAI API key set: ${maskApiKey2(value)}`));
9807
9003
  break;
9808
9004
  }
9809
9005
  case "default-provider": {
9810
9006
  if (value !== "anthropic" && value !== "openai") {
9811
- console.log(chalk5.red("Provider must be 'anthropic' or 'openai'"));
9007
+ console.log(chalk6.red("Provider must be 'anthropic' or 'openai'"));
9812
9008
  process.exit(1);
9813
9009
  }
9814
9010
  setConfigValue2("defaultProvider", value);
9815
- console.log(chalk5.green(`Default provider set to: ${value}`));
9011
+ console.log(chalk6.green(`Default provider set to: ${value}`));
9816
9012
  break;
9817
9013
  }
9818
9014
  default:
9819
- console.log(chalk5.red(`Unknown config key: ${key}`));
9820
- console.log(chalk5.gray("Run 'pinata config set --help' for available keys"));
9015
+ console.log(chalk6.red(`Unknown config key: ${key}`));
9016
+ console.log(chalk6.gray("Run 'pinata config set --help' for available keys"));
9821
9017
  process.exit(1);
9822
9018
  }
9823
- console.log(chalk5.gray(`Config stored at: ${getConfigPath2()}`));
9019
+ console.log(chalk6.gray(`Config stored at: ${getConfigPath2()}`));
9824
9020
  });
9825
9021
  config.command("get <key>").description("Get a configuration value").action(async (key) => {
9826
9022
  const { loadConfig: loadConfig2, maskApiKey: maskApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9827
9023
  const cfg = loadConfig2();
9828
9024
  switch (key) {
9829
9025
  case "anthropic-api-key":
9830
- console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk5.gray("(not set)"));
9026
+ console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk6.gray("(not set)"));
9831
9027
  break;
9832
9028
  case "openai-api-key":
9833
- console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk5.gray("(not set)"));
9029
+ console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk6.gray("(not set)"));
9834
9030
  break;
9835
9031
  case "default-provider":
9836
- console.log(cfg.defaultProvider ?? chalk5.gray("anthropic (default)"));
9032
+ console.log(cfg.defaultProvider ?? chalk6.gray("anthropic (default)"));
9837
9033
  break;
9838
9034
  default:
9839
- console.log(chalk5.red(`Unknown config key: ${key}`));
9035
+ console.log(chalk6.red(`Unknown config key: ${key}`));
9840
9036
  process.exit(1);
9841
9037
  }
9842
9038
  });
9843
9039
  config.command("list").description("List all configuration values").action(async () => {
9844
9040
  const { loadConfig: loadConfig2, maskApiKey: maskApiKey2, getConfigPath: getConfigPath2, hasApiKey: hasApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9845
9041
  const cfg = loadConfig2();
9846
- console.log(chalk5.bold("Pinata Configuration"));
9847
- console.log(chalk5.gray(`Config file: ${getConfigPath2()}`));
9042
+ console.log(chalk6.bold("Pinata Configuration"));
9043
+ console.log(chalk6.gray(`Config file: ${getConfigPath2()}`));
9848
9044
  console.log();
9849
9045
  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)})`) : ""}`);
9046
+ const anthropicStatus = hasApiKey2("anthropic") ? chalk6.green("configured") : chalk6.gray("not set");
9047
+ const openaiStatus = hasApiKey2("openai") ? chalk6.green("configured") : chalk6.gray("not set");
9048
+ console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ? chalk6.gray(`(${maskApiKey2(cfg.anthropicApiKey)})`) : ""}`);
9049
+ console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ? chalk6.gray(`(${maskApiKey2(cfg.openaiApiKey)})`) : ""}`);
9854
9050
  console.log(` Default provider: ${cfg.defaultProvider ?? "anthropic"}`);
9855
9051
  console.log();
9856
9052
  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"));
9053
+ console.log(chalk6.yellow("No AI provider configured."));
9054
+ console.log(chalk6.gray(" pinata config set anthropic-api-key sk-ant-xxx"));
9055
+ console.log(chalk6.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
9862
9056
  }
9863
9057
  });
9864
9058
  config.command("unset <key>").description("Remove a configuration value").action(async (key) => {
@@ -9866,18 +9060,18 @@ config.command("unset <key>").description("Remove a configuration value").action
9866
9060
  switch (key) {
9867
9061
  case "anthropic-api-key":
9868
9062
  deleteConfigValue2("anthropicApiKey");
9869
- console.log(chalk5.green("Anthropic API key removed"));
9063
+ console.log(chalk6.green("Anthropic API key removed"));
9870
9064
  break;
9871
9065
  case "openai-api-key":
9872
9066
  deleteConfigValue2("openaiApiKey");
9873
- console.log(chalk5.green("OpenAI API key removed"));
9067
+ console.log(chalk6.green("OpenAI API key removed"));
9874
9068
  break;
9875
9069
  case "default-provider":
9876
9070
  deleteConfigValue2("defaultProvider");
9877
- console.log(chalk5.green("Default provider reset to: anthropic"));
9071
+ console.log(chalk6.green("Default provider reset to: anthropic"));
9878
9072
  break;
9879
9073
  default:
9880
- console.log(chalk5.red(`Unknown config key: ${key}`));
9074
+ console.log(chalk6.red(`Unknown config key: ${key}`));
9881
9075
  process.exit(1);
9882
9076
  }
9883
9077
  });
@@ -9885,18 +9079,13 @@ var auth = program.command("auth").description("Manage API key authentication");
9885
9079
  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
9080
  const apiKey = options["key"] ?? process.env["PINATA_API_KEY"];
9887
9081
  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");
9082
+ console.log(chalk6.yellow("No API key provided."));
9083
+ console.log(chalk6.gray(" pinata auth login --key <your-api-key>"));
9084
+ console.log(chalk6.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
9895
9085
  process.exit(1);
9896
9086
  }
9897
9087
  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."));
9088
+ console.log(chalk6.red("Invalid API key format. Keys should start with 'pk_'."));
9900
9089
  process.exit(1);
9901
9090
  }
9902
9091
  const configDir = resolve(process.cwd(), ".pinata");
@@ -9905,19 +9094,13 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
9905
9094
  try {
9906
9095
  await mkdir4(configDir, { recursive: true });
9907
9096
  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");
9097
+ await writeFileAsync(authPath, JSON.stringify({ configured: true, keyId: maskedKey, configuredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf8");
9914
9098
  const envPath = resolve(configDir, ".env");
9915
9099
  await writeFileAsync(envPath, `PINATA_API_KEY=${apiKey}
9916
9100
  `, { 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"));
9101
+ console.log(chalk6.green("API key configured successfully!"));
9102
+ console.log(chalk6.gray(`Key ID: ${maskedKey}`));
9103
+ console.log(chalk6.yellow("Important: Add .pinata/.env to your .gitignore"));
9921
9104
  } catch (error) {
9922
9105
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9923
9106
  process.exit(1);
@@ -9938,11 +9121,7 @@ auth.command("logout").description("Remove stored API key").action(async () => {
9938
9121
  await rm2(envPath);
9939
9122
  removed = true;
9940
9123
  }
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
- }
9124
+ console.log(removed ? chalk6.green("API key removed successfully.") : chalk6.yellow("No stored API key found."));
9946
9125
  } catch (error) {
9947
9126
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9948
9127
  process.exit(1);
@@ -9951,19 +9130,19 @@ auth.command("logout").description("Remove stored API key").action(async () => {
9951
9130
  auth.command("status").description("Check authentication status").action(async () => {
9952
9131
  const authPath = resolve(process.cwd(), ".pinata", "auth.json");
9953
9132
  if (!existsSync(authPath)) {
9954
- console.log(chalk5.yellow("Not authenticated."));
9955
- console.log(chalk5.gray("Run: pinata auth login --key <your-api-key>"));
9133
+ console.log(chalk6.yellow("Not authenticated."));
9134
+ console.log(chalk6.gray("Run: pinata auth login --key <your-api-key>"));
9956
9135
  process.exit(0);
9957
9136
  }
9958
9137
  try {
9959
9138
  const { readFile: readFile7 } = await import('fs/promises');
9960
9139
  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."));
9140
+ console.log(chalk6.green("Authenticated"));
9141
+ console.log(chalk6.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
9142
+ console.log(chalk6.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
9143
+ } catch {
9144
+ console.log(chalk6.yellow("Authentication status unknown."));
9145
+ console.log(chalk6.gray("Run: pinata auth login to reconfigure."));
9967
9146
  }
9968
9147
  });
9969
9148
  program.parse();