pinata-security-cli 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { z } from 'zod';
3
- import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
2
+ import fs, { readFile, mkdir, writeFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
4
3
  import path, { dirname, resolve, join, basename, relative, extname } from 'path';
5
- import { existsSync, writeFileSync, readFileSync, chmodSync, mkdirSync } from 'fs';
4
+ import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
6
5
  import { homedir, tmpdir } from 'os';
6
+ import { z } from 'zod';
7
7
  import { spawn } from 'child_process';
8
8
  import { useState } from 'react';
9
9
  import { render, useApp, useInput, Box, Text } from 'ink';
10
10
  import Spinner from 'ink-spinner';
11
11
  import { jsx, jsxs } from 'react/jsx-runtime';
12
- import { fileURLToPath } from 'url';
13
- import chalk5 from 'chalk';
12
+ import chalk7 from 'chalk';
14
13
  import { Command } from 'commander';
15
- import ora from 'ora';
14
+ import ora3 from 'ora';
16
15
  import YAML from 'yaml';
16
+ import { fileURLToPath } from 'url';
17
17
  import { Query, Parser, Language } from 'web-tree-sitter';
18
18
  import { minimatch } from 'minimatch';
19
19
 
@@ -125,35 +125,6 @@ var init_result = __esm({
125
125
  "src/lib/result.ts"() {
126
126
  }
127
127
  });
128
- var SEVERITY_WEIGHTS, CONFIDENCE_WEIGHTS, PRIORITY_WEIGHTS, DEFAULT_TEST_PATTERNS;
129
- var init_types = __esm({
130
- "src/core/scanner/types.ts"() {
131
- SEVERITY_WEIGHTS = {
132
- critical: 4,
133
- high: 3,
134
- medium: 2,
135
- low: 1
136
- };
137
- CONFIDENCE_WEIGHTS = {
138
- high: 1,
139
- medium: 0.7,
140
- low: 0.4
141
- };
142
- PRIORITY_WEIGHTS = {
143
- P0: 3,
144
- P1: 2,
145
- P2: 1
146
- };
147
- DEFAULT_TEST_PATTERNS = {
148
- python: ["test_*.py", "*_test.py", "tests/**/*.py", "test/**/*.py"],
149
- typescript: ["*.test.ts", "*.spec.ts", "__tests__/**/*.ts", "tests/**/*.ts"],
150
- javascript: ["*.test.js", "*.spec.js", "__tests__/**/*.js", "tests/**/*.js"],
151
- go: ["*_test.go"],
152
- java: ["*Test.java", "*Tests.java", "src/test/**/*.java"],
153
- rust: ["tests/**/*.rs"]
154
- };
155
- }
156
- });
157
128
  function getCachePath(projectRoot) {
158
129
  return resolve(projectRoot, CACHE_DIR, CACHE_FILE);
159
130
  }
@@ -850,7 +821,6 @@ var init_config = __esm({
850
821
  var SKIP_PATTERNS, BATCH_PROMPT, SINGLE_ITEM_TEMPLATE, AIVerifier;
851
822
  var init_ai_verifier = __esm({
852
823
  "src/core/verifier/ai-verifier.ts"() {
853
- init_types();
854
824
  SKIP_PATTERNS = {
855
825
  paths: [
856
826
  /\.test\.(ts|js|tsx|jsx)$/,
@@ -1255,7 +1225,7 @@ function isTestable(categoryId) {
1255
1225
  return TESTABLE_VULNERABILITIES.includes(categoryId);
1256
1226
  }
1257
1227
  var DEFAULT_SANDBOX_CONFIG, TESTABLE_VULNERABILITIES;
1258
- var init_types2 = __esm({
1228
+ var init_types = __esm({
1259
1229
  "src/execution/types.ts"() {
1260
1230
  DEFAULT_SANDBOX_CONFIG = {
1261
1231
  image: "pinata-sandbox:latest",
@@ -1285,7 +1255,7 @@ function createSandbox(config2) {
1285
1255
  var Sandbox;
1286
1256
  var init_sandbox = __esm({
1287
1257
  "src/execution/sandbox.ts"() {
1288
- init_types2();
1258
+ init_types();
1289
1259
  Sandbox = class {
1290
1260
  config;
1291
1261
  tempDir = null;
@@ -1488,7 +1458,7 @@ export default defineConfig({
1488
1458
  * Execute a command and capture output
1489
1459
  */
1490
1460
  exec(command, args, options = {}) {
1491
- return new Promise((resolve6) => {
1461
+ return new Promise((resolve9) => {
1492
1462
  let stdout = "";
1493
1463
  let stderr = "";
1494
1464
  let timedOut = false;
@@ -1508,7 +1478,7 @@ export default defineConfig({
1508
1478
  }, timeout);
1509
1479
  proc.on("close", (code) => {
1510
1480
  clearTimeout(timer);
1511
- resolve6({
1481
+ resolve9({
1512
1482
  stdout,
1513
1483
  stderr,
1514
1484
  exitCode: code ?? 1,
@@ -1517,7 +1487,7 @@ export default defineConfig({
1517
1487
  });
1518
1488
  proc.on("error", (err3) => {
1519
1489
  clearTimeout(timer);
1520
- resolve6({
1490
+ resolve9({
1521
1491
  stdout,
1522
1492
  stderr: stderr + "\n" + err3.message,
1523
1493
  exitCode: 1,
@@ -2776,7 +2746,7 @@ var init_runner = __esm({
2776
2746
  init_sandbox();
2777
2747
  init_results();
2778
2748
  init_generator();
2779
- init_types2();
2749
+ init_types();
2780
2750
  ExecutionRunner = class {
2781
2751
  sandbox;
2782
2752
  dryRun;
@@ -2983,53 +2953,35 @@ ${code}
2983
2953
  Generate 3-5 targeted payloads that would exploit THIS SPECIFIC code.
2984
2954
  Consider the exact variable names, function calls, and data flow shown above.`;
2985
2955
  }
2956
+ function matchFirst(code, patterns) {
2957
+ for (const [keywords, name] of patterns) {
2958
+ if (keywords.some((k) => code.includes(k))) {
2959
+ return name;
2960
+ }
2961
+ }
2962
+ return void 0;
2963
+ }
2986
2964
  function extractTechStack(code, filePath) {
2965
+ const ext = "." + (filePath.split(".").pop() ?? "");
2987
2966
  const hints = {
2988
- language: detectLanguage2(filePath)
2967
+ language: LANGUAGE_EXTENSIONS[ext] ?? "unknown",
2968
+ hasEscaping: ESCAPING_KEYWORDS.some((k) => code.includes(k)),
2969
+ hasWaf: WAF_KEYWORDS.some((k) => code.includes(k))
2989
2970
  };
2990
- if (code.includes("express") || code.includes("app.get") || code.includes("app.post")) {
2991
- hints.framework = "express";
2992
- } else if (code.includes("fastify")) {
2993
- hints.framework = "fastify";
2994
- } else if (code.includes("django") || code.includes("from django")) {
2995
- hints.framework = "django";
2996
- } else if (code.includes("flask") || code.includes("from flask")) {
2997
- hints.framework = "flask";
2998
- } else if (code.includes("gin.") || code.includes("fiber.")) {
2999
- hints.framework = code.includes("gin.") ? "gin" : "fiber";
3000
- }
3001
- if (code.includes("postgres") || code.includes("pg.") || code.includes("$1")) {
3002
- hints.database = "postgres";
3003
- } else if (code.includes("mysql") || code.includes("?") && code.includes("query")) {
3004
- hints.database = "mysql";
3005
- } else if (code.includes("mongodb") || code.includes("mongoose") || code.includes("$where")) {
3006
- hints.database = "mongodb";
3007
- } else if (code.includes("sqlite") || code.includes("sqlite3")) {
3008
- hints.database = "sqlite";
3009
- }
3010
- if (code.includes("prisma")) {
3011
- hints.orm = "prisma";
3012
- } else if (code.includes("sequelize")) {
3013
- hints.orm = "sequelize";
3014
- } else if (code.includes("typeorm") || code.includes("TypeORM")) {
3015
- hints.orm = "typeorm";
3016
- } else if (code.includes("sqlalchemy") || code.includes("SQLAlchemy")) {
3017
- hints.orm = "sqlalchemy";
3018
- }
3019
- hints.hasEscaping = code.includes("escape") || code.includes("sanitize") || code.includes("DOMPurify") || code.includes("htmlspecialchars");
3020
- hints.hasWaf = code.includes("waf") || code.includes("WAF") || code.includes("cloudflare") || code.includes("akamai");
2971
+ const framework = matchFirst(code, FRAMEWORK_PATTERNS);
2972
+ if (framework) {
2973
+ hints.framework = framework;
2974
+ }
2975
+ const database = matchFirst(code, DATABASE_PATTERNS);
2976
+ if (database) {
2977
+ hints.database = database;
2978
+ }
2979
+ const orm = matchFirst(code, ORM_PATTERNS);
2980
+ if (orm) {
2981
+ hints.orm = orm;
2982
+ }
3021
2983
  return hints;
3022
2984
  }
3023
- function detectLanguage2(filePath) {
3024
- if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
3025
- if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
3026
- if (filePath.endsWith(".py")) return "python";
3027
- if (filePath.endsWith(".go")) return "go";
3028
- if (filePath.endsWith(".java")) return "java";
3029
- if (filePath.endsWith(".rb")) return "ruby";
3030
- if (filePath.endsWith(".php")) return "php";
3031
- return "unknown";
3032
- }
3033
2985
  function parseAiPayloadResponse(response) {
3034
2986
  try {
3035
2987
  const jsonMatch = response.match(/\{[\s\S]*"payloads"[\s\S]*\}/);
@@ -3080,7 +3032,7 @@ function getFallbackPayloads(context, maxPayloads = 10) {
3080
3032
  }
3081
3033
  return [...new Set(payloads)].slice(0, maxPayloads);
3082
3034
  }
3083
- var AI_PAYLOAD_SYSTEM_PROMPT;
3035
+ var AI_PAYLOAD_SYSTEM_PROMPT, FRAMEWORK_PATTERNS, DATABASE_PATTERNS, ORM_PATTERNS, ESCAPING_KEYWORDS, WAF_KEYWORDS, LANGUAGE_EXTENSIONS;
3084
3036
  var init_ai_payloads = __esm({
3085
3037
  "src/execution/ai-payloads.ts"() {
3086
3038
  init_payloads();
@@ -3119,6 +3071,39 @@ Return payloads in JSON format:
3119
3071
  }
3120
3072
  ]
3121
3073
  }`;
3074
+ FRAMEWORK_PATTERNS = [
3075
+ [["express", "app.get", "app.post"], "express"],
3076
+ [["fastify"], "fastify"],
3077
+ [["django", "from django"], "django"],
3078
+ [["flask", "from flask"], "flask"],
3079
+ [["gin."], "gin"],
3080
+ [["fiber."], "fiber"]
3081
+ ];
3082
+ DATABASE_PATTERNS = [
3083
+ [["postgres", "pg."], "postgres"],
3084
+ [["mysql"], "mysql"],
3085
+ [["mongodb", "mongoose", "$where"], "mongodb"],
3086
+ [["sqlite", "sqlite3"], "sqlite"]
3087
+ ];
3088
+ ORM_PATTERNS = [
3089
+ [["prisma"], "prisma"],
3090
+ [["sequelize"], "sequelize"],
3091
+ [["typeorm", "TypeORM"], "typeorm"],
3092
+ [["sqlalchemy", "SQLAlchemy"], "sqlalchemy"]
3093
+ ];
3094
+ ESCAPING_KEYWORDS = ["escape", "sanitize", "DOMPurify", "htmlspecialchars"];
3095
+ WAF_KEYWORDS = ["waf", "WAF", "cloudflare", "akamai"];
3096
+ LANGUAGE_EXTENSIONS = {
3097
+ ".ts": "typescript",
3098
+ ".tsx": "typescript",
3099
+ ".js": "javascript",
3100
+ ".jsx": "javascript",
3101
+ ".py": "python",
3102
+ ".go": "go",
3103
+ ".java": "java",
3104
+ ".rb": "ruby",
3105
+ ".php": "php"
3106
+ };
3122
3107
  }
3123
3108
  });
3124
3109
 
@@ -3441,7 +3426,7 @@ __export(execution_exports, {
3441
3426
  });
3442
3427
  var init_execution = __esm({
3443
3428
  "src/execution/index.ts"() {
3444
- init_types2();
3429
+ init_types();
3445
3430
  init_sandbox();
3446
3431
  init_runner();
3447
3432
  init_results();
@@ -3792,7 +3777,7 @@ function suggestConfidence(precision) {
3792
3777
  return "low";
3793
3778
  }
3794
3779
  var EMPTY_FEEDBACK_STATE, CONFIDENCE_THRESHOLDS;
3795
- var init_types3 = __esm({
3780
+ var init_types2 = __esm({
3796
3781
  "src/feedback/types.ts"() {
3797
3782
  EMPTY_FEEDBACK_STATE = {
3798
3783
  version: 1,
@@ -3926,7 +3911,7 @@ function generateReport(state) {
3926
3911
  var FEEDBACK_DIR, FEEDBACK_FILE;
3927
3912
  var init_store = __esm({
3928
3913
  "src/feedback/store.ts"() {
3929
- init_types3();
3914
+ init_types2();
3930
3915
  FEEDBACK_DIR = join(homedir(), ".pinata");
3931
3916
  FEEDBACK_FILE = join(FEEDBACK_DIR, "feedback.json");
3932
3917
  }
@@ -3948,7 +3933,7 @@ __export(feedback_exports, {
3948
3933
  });
3949
3934
  var init_feedback = __esm({
3950
3935
  "src/feedback/index.ts"() {
3951
- init_types3();
3936
+ init_types2();
3952
3937
  init_store();
3953
3938
  }
3954
3939
  });
@@ -4598,7 +4583,7 @@ var Logger = class _Logger {
4598
4583
  */
4599
4584
  debug(message, ...args) {
4600
4585
  if (this.shouldLog("debug")) {
4601
- console.debug(chalk5.gray(this.format(message)), ...args);
4586
+ console.debug(chalk7.gray(this.format(message)), ...args);
4602
4587
  }
4603
4588
  }
4604
4589
  /**
@@ -4614,7 +4599,7 @@ var Logger = class _Logger {
4614
4599
  */
4615
4600
  warn(message, ...args) {
4616
4601
  if (this.shouldLog("warn")) {
4617
- console.warn(chalk5.yellow(this.format(message)), ...args);
4602
+ console.warn(chalk7.yellow(this.format(message)), ...args);
4618
4603
  }
4619
4604
  }
4620
4605
  /**
@@ -4622,7 +4607,7 @@ var Logger = class _Logger {
4622
4607
  */
4623
4608
  error(message, ...args) {
4624
4609
  if (this.shouldLog("error")) {
4625
- console.error(chalk5.red(this.format(message)), ...args);
4610
+ console.error(chalk7.red(this.format(message)), ...args);
4626
4611
  }
4627
4612
  }
4628
4613
  /**
@@ -4630,7 +4615,7 @@ var Logger = class _Logger {
4630
4615
  */
4631
4616
  success(message, ...args) {
4632
4617
  if (this.shouldLog("info")) {
4633
- console.info(chalk5.green(this.format(message)), ...args);
4618
+ console.info(chalk7.green(this.format(message)), ...args);
4634
4619
  }
4635
4620
  }
4636
4621
  /**
@@ -5353,9 +5338,384 @@ function detectLanguage(filePath) {
5353
5338
  const ext = extname(filePath).toLowerCase();
5354
5339
  return EXTENSION_TO_LANGUAGE[ext] ?? null;
5355
5340
  }
5341
+ var DETECTION_PATTERNS = [
5342
+ // CLI Detection
5343
+ {
5344
+ type: "cli",
5345
+ packageJson: {
5346
+ hasField: ["bin"]
5347
+ },
5348
+ weight: 10
5349
+ },
5350
+ {
5351
+ type: "cli",
5352
+ files: ["src/cli.ts", "src/cli/index.ts", "cli/index.ts"],
5353
+ weight: 5
5354
+ },
5355
+ {
5356
+ type: "cli",
5357
+ packageJson: {
5358
+ dependencies: ["commander", "yargs", "meow", "oclif", "inquirer", "prompts"]
5359
+ },
5360
+ weight: 3
5361
+ },
5362
+ // Web Server Detection
5363
+ {
5364
+ type: "web-server",
5365
+ packageJson: {
5366
+ dependencies: ["express", "fastify", "koa", "hapi", "@hapi/hapi", "restify"]
5367
+ },
5368
+ weight: 10
5369
+ },
5370
+ {
5371
+ type: "web-server",
5372
+ files: ["server.ts", "server.js", "app.ts", "app.js", "src/server.ts"],
5373
+ weight: 3
5374
+ },
5375
+ // API Detection
5376
+ {
5377
+ type: "api",
5378
+ files: ["routes/", "handlers/", "controllers/", "api/"],
5379
+ weight: 5
5380
+ },
5381
+ {
5382
+ type: "api",
5383
+ files: ["openapi.yaml", "openapi.json", "swagger.yaml", "swagger.json"],
5384
+ weight: 8
5385
+ },
5386
+ {
5387
+ type: "api",
5388
+ packageJson: {
5389
+ dependencies: ["@nestjs/core", "trpc", "@trpc/server"]
5390
+ },
5391
+ weight: 10
5392
+ },
5393
+ // Library Detection
5394
+ {
5395
+ type: "library",
5396
+ packageJson: {
5397
+ hasField: ["exports", "main", "module", "types"]
5398
+ },
5399
+ weight: 3
5400
+ },
5401
+ {
5402
+ type: "library",
5403
+ files: ["tsup.config.ts", "rollup.config.js", "vite.config.ts"],
5404
+ weight: 2
5405
+ },
5406
+ // Frontend SPA Detection
5407
+ {
5408
+ type: "frontend-spa",
5409
+ packageJson: {
5410
+ dependencies: ["react", "vue", "angular", "svelte", "@angular/core"]
5411
+ },
5412
+ weight: 5
5413
+ },
5414
+ {
5415
+ type: "frontend-spa",
5416
+ files: ["src/App.tsx", "src/App.vue", "src/app/app.component.ts"],
5417
+ weight: 8
5418
+ },
5419
+ // SSR Framework Detection
5420
+ {
5421
+ type: "ssr-framework",
5422
+ packageJson: {
5423
+ dependencies: ["next", "nuxt", "@nuxt/core", "remix", "@remix-run/node", "astro", "sveltekit"]
5424
+ },
5425
+ weight: 10
5426
+ },
5427
+ {
5428
+ type: "ssr-framework",
5429
+ files: ["next.config.js", "next.config.ts", "nuxt.config.ts", "remix.config.js", "astro.config.mjs"],
5430
+ weight: 10
5431
+ },
5432
+ // Serverless Detection
5433
+ {
5434
+ type: "serverless",
5435
+ files: ["serverless.yml", "serverless.yaml", "serverless.ts", "sam.yaml", "template.yaml"],
5436
+ weight: 10
5437
+ },
5438
+ {
5439
+ type: "serverless",
5440
+ packageJson: {
5441
+ dependencies: ["@aws-sdk/client-lambda", "aws-lambda", "@google-cloud/functions-framework"]
5442
+ },
5443
+ weight: 5
5444
+ },
5445
+ {
5446
+ type: "serverless",
5447
+ files: ["functions/", "lambda/", "netlify/functions/", "api/"],
5448
+ weight: 3
5449
+ },
5450
+ // Desktop Detection
5451
+ {
5452
+ type: "desktop",
5453
+ packageJson: {
5454
+ dependencies: ["electron", "@electron/remote", "tauri", "@tauri-apps/api"]
5455
+ },
5456
+ weight: 10
5457
+ },
5458
+ {
5459
+ type: "desktop",
5460
+ files: ["electron/main.ts", "src-tauri/", "electron.js", "main.electron.ts"],
5461
+ weight: 8
5462
+ },
5463
+ // Mobile Detection
5464
+ {
5465
+ type: "mobile",
5466
+ packageJson: {
5467
+ dependencies: ["react-native", "expo", "@react-native-community/cli"]
5468
+ },
5469
+ weight: 10
5470
+ },
5471
+ {
5472
+ type: "mobile",
5473
+ files: ["app.json", "metro.config.js", "ios/", "android/"],
5474
+ weight: 5
5475
+ },
5476
+ // Monorepo Detection
5477
+ {
5478
+ type: "monorepo",
5479
+ packageJson: {
5480
+ hasField: ["workspaces"]
5481
+ },
5482
+ weight: 10
5483
+ },
5484
+ {
5485
+ type: "monorepo",
5486
+ files: ["lerna.json", "pnpm-workspace.yaml", "turbo.json", "nx.json"],
5487
+ weight: 10
5488
+ },
5489
+ {
5490
+ type: "monorepo",
5491
+ files: ["packages/", "apps/"],
5492
+ weight: 5
5493
+ },
5494
+ // Script Detection (low weight, fallback)
5495
+ {
5496
+ type: "script",
5497
+ files: ["script.ts", "script.js", "run.ts", "run.js"],
5498
+ weight: 2
5499
+ }
5500
+ ];
5501
+ var SCORING_ADJUSTMENTS = [
5502
+ // Blocking I/O is fine in CLI and scripts
5503
+ {
5504
+ categoryId: "blocking-io",
5505
+ skip: ["cli", "script", "desktop"],
5506
+ lowerWeight: ["serverless"]
5507
+ },
5508
+ // SQL injection not relevant for pure frontends
5509
+ {
5510
+ categoryId: "sql-injection",
5511
+ skip: ["frontend-spa", "mobile"],
5512
+ higherWeight: ["web-server", "api"]
5513
+ },
5514
+ // XSS is critical for frontends, less so for pure APIs
5515
+ {
5516
+ categoryId: "xss",
5517
+ higherWeight: ["frontend-spa", "ssr-framework"],
5518
+ lowerWeight: ["api", "cli"]
5519
+ },
5520
+ // SSRF is critical for servers
5521
+ {
5522
+ categoryId: "ssrf",
5523
+ skip: ["frontend-spa", "cli", "script"],
5524
+ higherWeight: ["web-server", "api", "serverless"]
5525
+ },
5526
+ // Connection pool exhaustion not relevant for serverless
5527
+ {
5528
+ categoryId: "connection-pool-exhaustion",
5529
+ skip: ["serverless", "frontend-spa", "cli"],
5530
+ higherWeight: ["web-server", "api"]
5531
+ },
5532
+ // Memory leaks are critical for long-running servers
5533
+ {
5534
+ categoryId: "memory-leak",
5535
+ skip: ["serverless", "script"],
5536
+ higherWeight: ["web-server", "desktop"]
5537
+ },
5538
+ // Rate limiting not needed for CLI
5539
+ {
5540
+ categoryId: "rate-limiting",
5541
+ skip: ["cli", "script", "library"],
5542
+ higherWeight: ["web-server", "api"]
5543
+ },
5544
+ // CSRF not relevant for CLI or pure APIs
5545
+ {
5546
+ categoryId: "csrf",
5547
+ skip: ["cli", "script", "library", "api"],
5548
+ higherWeight: ["web-server", "ssr-framework"]
5549
+ },
5550
+ // Deserialization critical for APIs, less so for frontends
5551
+ {
5552
+ categoryId: "deserialization",
5553
+ skip: ["frontend-spa"],
5554
+ higherWeight: ["api", "web-server"]
5555
+ },
5556
+ // Command injection critical for servers, OK in CLI
5557
+ {
5558
+ categoryId: "command-injection",
5559
+ lowerWeight: ["cli", "script"],
5560
+ higherWeight: ["web-server", "api", "serverless"]
5561
+ }
5562
+ ];
5563
+ async function detectProjectType(projectPath) {
5564
+ const scores = /* @__PURE__ */ new Map();
5565
+ const evidence = [];
5566
+ const frameworks = [];
5567
+ const packageJsonPath = resolve(projectPath, "package.json");
5568
+ let packageJson = null;
5569
+ if (existsSync(packageJsonPath)) {
5570
+ try {
5571
+ const content = await readFile(packageJsonPath, "utf-8");
5572
+ packageJson = JSON.parse(content);
5573
+ } catch {
5574
+ }
5575
+ }
5576
+ for (const pattern of DETECTION_PATTERNS) {
5577
+ let matched = false;
5578
+ if (pattern.packageJson && packageJson) {
5579
+ if (pattern.packageJson.hasField) {
5580
+ for (const field of pattern.packageJson.hasField) {
5581
+ if (field in packageJson) {
5582
+ matched = true;
5583
+ evidence.push(`package.json has "${field}" field`);
5584
+ }
5585
+ }
5586
+ }
5587
+ if (pattern.packageJson.dependencies) {
5588
+ const deps = packageJson["dependencies"];
5589
+ if (deps) {
5590
+ for (const dep of pattern.packageJson.dependencies) {
5591
+ if (dep in deps) {
5592
+ matched = true;
5593
+ evidence.push(`Uses ${dep}`);
5594
+ frameworks.push(dep);
5595
+ }
5596
+ }
5597
+ }
5598
+ }
5599
+ if (pattern.packageJson.devDependencies) {
5600
+ const devDeps = packageJson["devDependencies"];
5601
+ if (devDeps) {
5602
+ for (const dep of pattern.packageJson.devDependencies) {
5603
+ if (dep in devDeps) {
5604
+ matched = true;
5605
+ evidence.push(`Uses ${dep} (dev)`);
5606
+ }
5607
+ }
5608
+ }
5609
+ }
5610
+ }
5611
+ if (pattern.files) {
5612
+ for (const file of pattern.files) {
5613
+ const filePath = resolve(projectPath, file);
5614
+ if (existsSync(filePath)) {
5615
+ matched = true;
5616
+ evidence.push(`Has ${file}`);
5617
+ }
5618
+ }
5619
+ }
5620
+ if (matched) {
5621
+ const current = scores.get(pattern.type) ?? 0;
5622
+ scores.set(pattern.type, current + pattern.weight);
5623
+ }
5624
+ }
5625
+ let bestType = "unknown";
5626
+ let bestScore = 0;
5627
+ for (const [type, score] of scores.entries()) {
5628
+ if (score > bestScore) {
5629
+ bestScore = score;
5630
+ bestType = type;
5631
+ }
5632
+ }
5633
+ let confidence = "low";
5634
+ if (bestScore >= 10) {
5635
+ confidence = "high";
5636
+ } else if (bestScore >= 5) {
5637
+ confidence = "medium";
5638
+ }
5639
+ const secondaryTypes = [];
5640
+ if (bestType === "monorepo") {
5641
+ for (const [type, score] of scores.entries()) {
5642
+ if (type !== "monorepo" && score >= 3) {
5643
+ secondaryTypes.push(type);
5644
+ }
5645
+ }
5646
+ }
5647
+ const languages = [];
5648
+ if (existsSync(resolve(projectPath, "tsconfig.json"))) languages.push("typescript");
5649
+ if (existsSync(resolve(projectPath, "package.json"))) languages.push("javascript");
5650
+ if (existsSync(resolve(projectPath, "requirements.txt"))) languages.push("python");
5651
+ if (existsSync(resolve(projectPath, "go.mod"))) languages.push("go");
5652
+ if (existsSync(resolve(projectPath, "Cargo.toml"))) languages.push("rust");
5653
+ if (existsSync(resolve(projectPath, "pom.xml"))) languages.push("java");
5654
+ const result = {
5655
+ type: bestType,
5656
+ confidence,
5657
+ evidence: [...new Set(evidence)],
5658
+ frameworks: [...new Set(frameworks)],
5659
+ languages
5660
+ };
5661
+ if (secondaryTypes.length > 0) {
5662
+ result.secondaryTypes = secondaryTypes;
5663
+ }
5664
+ return result;
5665
+ }
5666
+ function getCategoryWeight(categoryId, projectType) {
5667
+ const adjustment = SCORING_ADJUSTMENTS.find((a) => a.categoryId === categoryId);
5668
+ if (!adjustment) return 1;
5669
+ if (adjustment.skip?.includes(projectType)) return 0;
5670
+ if (adjustment.higherWeight?.includes(projectType)) return 1.5;
5671
+ if (adjustment.lowerWeight?.includes(projectType)) return 0.5;
5672
+ return 1;
5673
+ }
5674
+ function getProjectTypeDescription(type) {
5675
+ const descriptions = {
5676
+ "cli": "Command-line tool",
5677
+ "web-server": "Web server (Express, Fastify, etc.)",
5678
+ "api": "REST/GraphQL API",
5679
+ "library": "Library/package for consumption",
5680
+ "frontend-spa": "Frontend single-page application",
5681
+ "ssr-framework": "Server-side rendering framework",
5682
+ "serverless": "Serverless function",
5683
+ "desktop": "Desktop application",
5684
+ "mobile": "Mobile application",
5685
+ "script": "Script/automation",
5686
+ "monorepo": "Monorepo workspace",
5687
+ "unknown": "Unknown project type"
5688
+ };
5689
+ return descriptions[type];
5690
+ }
5356
5691
  init_errors();
5357
5692
  init_result();
5358
- init_types();
5693
+ var SEVERITY_WEIGHTS = {
5694
+ critical: 4,
5695
+ high: 3,
5696
+ medium: 2,
5697
+ low: 1
5698
+ };
5699
+ var CONFIDENCE_WEIGHTS = {
5700
+ high: 1,
5701
+ medium: 0.7,
5702
+ low: 0.4
5703
+ };
5704
+ var PRIORITY_WEIGHTS = {
5705
+ P0: 3,
5706
+ P1: 2,
5707
+ P2: 1
5708
+ };
5709
+ var DEFAULT_TEST_PATTERNS = {
5710
+ python: ["test_*.py", "*_test.py", "tests/**/*.py", "test/**/*.py"],
5711
+ typescript: ["*.test.ts", "*.spec.ts", "__tests__/**/*.ts", "tests/**/*.ts"],
5712
+ javascript: ["*.test.js", "*.spec.js", "__tests__/**/*.js", "tests/**/*.js"],
5713
+ go: ["*_test.go"],
5714
+ java: ["*Test.java", "*Tests.java", "src/test/**/*.java"],
5715
+ rust: ["tests/**/*.rs"]
5716
+ };
5717
+
5718
+ // src/core/scanner/scanner.ts
5359
5719
  var DEFAULT_OPTIONS = {
5360
5720
  excludeDirs: [
5361
5721
  // Package managers
@@ -5438,6 +5798,8 @@ var Scanner = class {
5438
5798
  } catch {
5439
5799
  return err(new AnalysisError(`Directory not found: ${targetDirectory}`));
5440
5800
  }
5801
+ const projectType = await detectProjectType(targetDirectory);
5802
+ this.log.info(`Detected project type: ${projectType.type} (${projectType.confidence} confidence)`);
5441
5803
  const categoriesResult = this.getCategoriesToScan(opts);
5442
5804
  if (!categoriesResult.success) {
5443
5805
  return categoriesResult;
@@ -5493,19 +5855,27 @@ var Scanner = class {
5493
5855
  }
5494
5856
  fileStats.testFiles = testFiles.size;
5495
5857
  fileStats.sourceFiles = fileStats.totalFiles - testFiles.size;
5496
- const gaps = this.detectionsToGaps(allDetections, categories, testFiles, opts);
5858
+ const allGaps = this.detectionsToGaps(allDetections, categories, testFiles, opts);
5859
+ const gaps = allGaps.filter((gap) => {
5860
+ const weight = getCategoryWeight(gap.categoryId, projectType.type);
5861
+ return weight > 0;
5862
+ });
5863
+ if (allGaps.length !== gaps.length) {
5864
+ this.log.info(`Filtered ${allGaps.length - gaps.length} gaps as irrelevant for ${projectType.type} project type`);
5865
+ }
5497
5866
  const filesWithGaps = new Set(gaps.map((g) => g.filePath));
5498
5867
  fileStats.filesWithGaps = filesWithGaps.size;
5499
5868
  const gapsByCategory = this.groupGapsByCategory(gaps);
5500
5869
  const gapsByFile = this.groupGapsByFile(gaps);
5501
5870
  const coverage = this.calculateCoverage(categories, gapsByCategory);
5502
- const score = this.calculateScore(gaps, coverage, categories);
5871
+ const score = this.calculateScore(gaps, coverage, categories, projectType.type);
5503
5872
  const summary = this.buildSummary(gaps, score, coverage, fileStats, categories);
5504
5873
  const completedAt = /* @__PURE__ */ new Date();
5505
5874
  const durationMs = completedAt.getTime() - startedAt.getTime();
5506
5875
  this.log.info(`Scan complete: ${gaps.length} gaps found in ${durationMs}ms`);
5507
5876
  return ok({
5508
5877
  targetDirectory,
5878
+ projectType,
5509
5879
  startedAt,
5510
5880
  completedAt,
5511
5881
  durationMs,
@@ -5531,8 +5901,13 @@ var Scanner = class {
5531
5901
  }
5532
5902
  /**
5533
5903
  * Calculate Pinata Score from gaps and coverage
5904
+ *
5905
+ * @param gaps - Detected gaps
5906
+ * @param coverage - Coverage metrics
5907
+ * @param categories - Categories that were scanned
5908
+ * @param projectType - Detected project type for context-aware weighting
5534
5909
  */
5535
- calculateScore(gaps, coverage, categories) {
5910
+ calculateScore(gaps, coverage, categories, projectType = "unknown") {
5536
5911
  let baseScore = 100;
5537
5912
  const penalties = [];
5538
5913
  const bonuses = [];
@@ -5544,14 +5919,19 @@ var Scanner = class {
5544
5919
  const severityWeight = SEVERITY_WEIGHTS[gap.severity];
5545
5920
  const confidenceWeight = CONFIDENCE_WEIGHTS[gap.confidence];
5546
5921
  const priorityWeight = PRIORITY_WEIGHTS[gap.priority];
5922
+ const projectTypeWeight = getCategoryWeight(gap.categoryId, projectType);
5547
5923
  const basePenalty = 2;
5548
- const penalty = basePenalty * severityWeight * confidenceWeight * Math.sqrt(priorityWeight);
5924
+ const penalty = basePenalty * severityWeight * confidenceWeight * Math.sqrt(priorityWeight) * projectTypeWeight;
5925
+ if (projectTypeWeight === 0) {
5926
+ continue;
5927
+ }
5549
5928
  baseScore -= penalty;
5550
5929
  const currentDomainScore = domainScores.get(gap.domain) ?? 100;
5551
5930
  domainScores.set(gap.domain, Math.max(0, currentDomainScore - penalty * 2));
5552
5931
  if (penalty >= 5) {
5932
+ const weightNote = projectTypeWeight !== 1 ? ` [${projectType} weight: ${projectTypeWeight}x]` : "";
5553
5933
  penalties.push({
5554
- reason: `${gap.severity} ${gap.domain} gap: ${gap.categoryName}`,
5934
+ reason: `${gap.severity} ${gap.domain} gap: ${gap.categoryName}${weightNote}`,
5555
5935
  points: Math.round(penalty),
5556
5936
  categoryId: gap.categoryId
5557
5937
  });
@@ -5965,9 +6345,6 @@ function createScanner(categoryStore) {
5965
6345
  return new Scanner(categoryStore);
5966
6346
  }
5967
6347
 
5968
- // src/core/scanner/index.ts
5969
- init_types();
5970
-
5971
6348
  // src/core/index.ts
5972
6349
  var VERSION = "0.4.0";
5973
6350
 
@@ -6701,34 +7078,34 @@ function createRenderer(options) {
6701
7078
  return new TemplateRenderer(options);
6702
7079
  }
6703
7080
  var SEVERITY_COLORS = {
6704
- critical: chalk5.red.bold,
6705
- high: chalk5.red,
6706
- medium: chalk5.yellow,
6707
- low: chalk5.gray
7081
+ critical: chalk7.red.bold,
7082
+ high: chalk7.red,
7083
+ medium: chalk7.yellow,
7084
+ low: chalk7.gray
6708
7085
  };
6709
7086
  var PRIORITY_COLORS = {
6710
- P0: chalk5.red.bold,
6711
- P1: chalk5.yellow,
6712
- P2: chalk5.gray
7087
+ P0: chalk7.red.bold,
7088
+ P1: chalk7.yellow,
7089
+ P2: chalk7.gray
6713
7090
  };
6714
7091
  var DOMAIN_COLORS = {
6715
- security: chalk5.red,
6716
- data: chalk5.blue,
6717
- concurrency: chalk5.magenta,
6718
- input: chalk5.cyan,
6719
- resource: chalk5.yellow,
6720
- reliability: chalk5.green,
6721
- performance: chalk5.yellowBright,
6722
- platform: chalk5.gray,
6723
- business: chalk5.white,
6724
- compliance: chalk5.blueBright
7092
+ security: chalk7.red,
7093
+ data: chalk7.blue,
7094
+ concurrency: chalk7.magenta,
7095
+ input: chalk7.cyan,
7096
+ resource: chalk7.yellow,
7097
+ reliability: chalk7.green,
7098
+ performance: chalk7.yellowBright,
7099
+ platform: chalk7.gray,
7100
+ business: chalk7.white,
7101
+ compliance: chalk7.blueBright
6725
7102
  };
6726
7103
  function formatTerminal(categories) {
6727
7104
  if (categories.length === 0) {
6728
- return chalk5.yellow("No categories found matching the filters.");
7105
+ return chalk7.yellow("No categories found matching the filters.");
6729
7106
  }
6730
7107
  const lines = [];
6731
- lines.push(chalk5.bold.underline(`Found ${categories.length} categories:
7108
+ lines.push(chalk7.bold.underline(`Found ${categories.length} categories:
6732
7109
  `));
6733
7110
  const byDomain = /* @__PURE__ */ new Map();
6734
7111
  for (const cat of categories) {
@@ -6739,26 +7116,26 @@ function formatTerminal(categories) {
6739
7116
  byDomain.get(domain).push(cat);
6740
7117
  }
6741
7118
  for (const [domain, domainCategories] of byDomain) {
6742
- const domainColor = DOMAIN_COLORS[domain] ?? chalk5.white;
7119
+ const domainColor = DOMAIN_COLORS[domain] ?? chalk7.white;
6743
7120
  lines.push(domainColor.bold(`
6744
7121
  ${domain.toUpperCase()} (${domainCategories.length})`));
6745
- lines.push(chalk5.gray("\u2500".repeat(40)));
7122
+ lines.push(chalk7.gray("\u2500".repeat(40)));
6746
7123
  for (const cat of domainCategories) {
6747
7124
  const priorityColor = PRIORITY_COLORS[cat.priority];
6748
7125
  const severityColor = SEVERITY_COLORS[cat.severity];
6749
7126
  const priority = priorityColor(`[${cat.priority}]`);
6750
7127
  const severity = severityColor(`${cat.severity}`);
6751
- const level = chalk5.cyan(`${cat.level}`);
6752
- const name = chalk5.white.bold(cat.name);
6753
- const id = chalk5.gray(`(${cat.id})`);
7128
+ const level = chalk7.cyan(`${cat.level}`);
7129
+ const name = chalk7.white.bold(cat.name);
7130
+ const id = chalk7.gray(`(${cat.id})`);
6754
7131
  lines.push(` ${priority} ${name} ${id}`);
6755
7132
  lines.push(` ${severity} | ${level}`);
6756
7133
  const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
6757
- lines.push(chalk5.gray(` ${desc}`));
7134
+ lines.push(chalk7.gray(` ${desc}`));
6758
7135
  lines.push("");
6759
7136
  }
6760
7137
  }
6761
- lines.push(chalk5.gray("\u2500".repeat(40)));
7138
+ lines.push(chalk7.gray("\u2500".repeat(40)));
6762
7139
  lines.push(formatStats(categories));
6763
7140
  return lines.join("\n");
6764
7141
  }
@@ -6780,7 +7157,7 @@ function formatStats(categories) {
6780
7157
  if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
6781
7158
  if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
6782
7159
  if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
6783
- parts.push(chalk5.gray("|"));
7160
+ parts.push(chalk7.gray("|"));
6784
7161
  if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
6785
7162
  if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
6786
7163
  if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
@@ -6837,13 +7214,9 @@ function isValidOutputFormat(format) {
6837
7214
  return ["terminal", "json", "markdown"].includes(format);
6838
7215
  }
6839
7216
  function formatError(error) {
6840
- return chalk5.red(`Error: ${error.message}`);
7217
+ return chalk7.red(`Error: ${error.message}`);
6841
7218
  }
6842
7219
 
6843
- // src/cli/generate-formatters.ts
6844
- init_errors();
6845
- init_result();
6846
-
6847
7220
  // src/ai/service.ts
6848
7221
  var DEFAULT_CONFIG = {
6849
7222
  provider: "anthropic",
@@ -6890,11 +7263,11 @@ var AIService = class {
6890
7263
  */
6891
7264
  getApiKeyFromConfig(provider) {
6892
7265
  try {
6893
- const { existsSync: existsSync4, readFileSync: readFileSync3 } = __require("fs");
7266
+ const { existsSync: existsSync6, readFileSync: readFileSync3 } = __require("fs");
6894
7267
  const { homedir: homedir3 } = __require("os");
6895
7268
  const { join: join5 } = __require("path");
6896
7269
  const configPath = join5(homedir3(), ".pinata", "config.json");
6897
- if (!existsSync4(configPath)) {
7270
+ if (!existsSync6(configPath)) {
6898
7271
  return "";
6899
7272
  }
6900
7273
  const content = readFileSync3(configPath, "utf-8");
@@ -7145,91 +7518,202 @@ function createAIService(config2) {
7145
7518
  return new AIService(config2);
7146
7519
  }
7147
7520
 
7148
- // src/ai/template-filler.ts
7149
- var SYSTEM_PROMPT = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
7150
- Given a code snippet and a list of template variables, suggest appropriate values for each variable.
7151
-
7152
- For each variable, analyze:
7153
- 1. The code snippet to extract relevant information (class names, function names, etc.)
7154
- 2. The variable description to understand what's needed
7155
- 3. The variable type to ensure correct formatting
7521
+ // src/ai/explainer.ts
7522
+ var SYSTEM_PROMPT = `You are a security expert explaining code vulnerabilities to developers.
7523
+ Your explanations should be:
7524
+ - Clear and actionable
7525
+ - Focused on the specific code pattern
7526
+ - Include concrete remediation steps
7527
+ - Reference relevant security standards (OWASP, CWE) when applicable
7156
7528
 
7157
7529
  Always respond with valid JSON matching this structure:
7158
7530
  {
7159
- "suggestions": [
7160
- {
7161
- "name": "variableName",
7162
- "value": "suggested value",
7163
- "reasoning": "why this value was chosen",
7164
- "confidence": 0.0-1.0
7165
- }
7166
- ]
7167
- }
7168
-
7169
- For arrays, use: "value": ["item1", "item2"]
7170
- For booleans, use: "value": true or "value": false
7171
- For numbers, use: "value": 42`;
7172
- async function suggestVariables(request, config2) {
7531
+ "summary": "1-2 sentence summary",
7532
+ "explanation": "Detailed explanation of the vulnerability",
7533
+ "risk": "What an attacker could do if this is exploited",
7534
+ "remediation": "Step-by-step instructions to fix",
7535
+ "safeExample": "Code example showing the safe pattern",
7536
+ "references": ["optional array of CVE/CWE/OWASP references"]
7537
+ }`;
7538
+ async function explainGap(gap, category, config2) {
7173
7539
  const ai = createAIService(config2);
7174
7540
  if (!ai.isConfigured()) {
7175
7541
  return {
7176
- success: true,
7177
- data: extractVariablesFromCode(request),
7542
+ success: false,
7543
+ error: "AI service not configured",
7178
7544
  durationMs: 0
7179
7545
  };
7180
7546
  }
7181
- const prompt = buildVariablePrompt(request);
7182
- const startTime = Date.now();
7547
+ const prompt = buildExplainPrompt(gap);
7183
7548
  const response = await ai.completeJSON({
7184
7549
  systemPrompt: SYSTEM_PROMPT,
7185
7550
  messages: [{ role: "user", content: prompt }],
7186
7551
  maxTokens: 1024,
7187
- temperature: 0.2
7552
+ temperature: 0.3
7188
7553
  });
7189
- if (!response.success || !response.data) {
7190
- return {
7191
- success: true,
7192
- data: extractVariablesFromCode(request),
7193
- durationMs: Date.now() - startTime
7194
- };
7195
- }
7196
- const suggestions = /* @__PURE__ */ new Map();
7197
- const unfilled = [];
7198
- const values = { ...request.existingValues };
7199
- const suggestionsList = response.data.suggestions ?? [];
7200
- for (const suggestion of suggestionsList) {
7201
- suggestions.set(suggestion.name, suggestion);
7202
- if (!(suggestion.name in values)) {
7203
- values[suggestion.name] = suggestion.value;
7204
- }
7205
- }
7206
- for (const variable of request.variables) {
7207
- if (!suggestions.has(variable.name) && !(variable.name in values)) {
7208
- if (variable.defaultValue !== void 0) {
7209
- values[variable.name] = variable.defaultValue;
7210
- } else {
7211
- unfilled.push(variable.name);
7212
- }
7554
+ return response;
7555
+ }
7556
+ async function explainGaps(gaps, categories, config2) {
7557
+ const results = /* @__PURE__ */ new Map();
7558
+ const BATCH_SIZE = 5;
7559
+ for (let i = 0; i < gaps.length; i += BATCH_SIZE) {
7560
+ const batch = gaps.slice(i, i + BATCH_SIZE);
7561
+ const promises = batch.map(async (gap) => {
7562
+ const category = categories?.get(gap.categoryId);
7563
+ const result = await explainGap(gap, category, config2);
7564
+ return { key: `${gap.filePath}:${gap.lineStart}:${gap.categoryId}`, result };
7565
+ });
7566
+ const batchResults = await Promise.all(promises);
7567
+ for (const { key, result } of batchResults) {
7568
+ results.set(key, result);
7213
7569
  }
7214
7570
  }
7215
- const result = {
7216
- success: true,
7217
- data: { suggestions, unfilled, values },
7218
- durationMs: response.durationMs
7219
- };
7220
- if (response.usage) {
7221
- result.usage = response.usage;
7222
- }
7223
- return result;
7571
+ return results;
7224
7572
  }
7225
- function buildVariablePrompt(request) {
7573
+ function buildExplainPrompt(gap, category) {
7226
7574
  const parts = [];
7227
- parts.push("Analyze this code and suggest values for the template variables:\n");
7228
- parts.push("**Code:**");
7229
- parts.push("```");
7230
- parts.push(request.codeSnippet);
7231
- parts.push("```\n");
7232
- parts.push(`**File:** ${request.filePath}
7575
+ parts.push(`Explain this security finding:
7576
+ `);
7577
+ parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
7578
+ parts.push(`**Severity:** ${gap.severity}`);
7579
+ parts.push(`**Confidence:** ${gap.confidence}`);
7580
+ parts.push(`**File:** ${gap.filePath}`);
7581
+ parts.push(`**Line:** ${gap.lineStart}`);
7582
+ if (gap.codeSnippet) {
7583
+ parts.push(`
7584
+ **Code:**
7585
+ \`\`\`
7586
+ ${gap.codeSnippet}
7587
+ \`\`\``);
7588
+ }
7589
+ parts.push(`
7590
+ **Pattern:** ${gap.patternId}`);
7591
+ parts.push(`**Detection Type:** ${gap.patternType}`);
7592
+ parts.push(`
7593
+ Provide a clear, actionable explanation for a developer.`);
7594
+ return parts.join("\n");
7595
+ }
7596
+ function generateFallbackExplanation(gap) {
7597
+ const summaries = {
7598
+ "sql-injection": "SQL query constructed with user input may allow injection attacks.",
7599
+ "xss": "User input rendered without escaping may allow script injection.",
7600
+ "command-injection": "Shell command constructed with user input may allow command execution.",
7601
+ "path-traversal": "File path constructed with user input may allow directory traversal.",
7602
+ "hardcoded-secrets": "Sensitive credentials found in source code.",
7603
+ "deserialization": "Untrusted data deserialization may allow code execution.",
7604
+ "ssrf": "Server-side request with user-controlled URL may allow internal access.",
7605
+ "xxe": "XML parser may be vulnerable to external entity injection.",
7606
+ "csrf": "State-changing request lacks CSRF protection.",
7607
+ "ldap-injection": "LDAP query constructed with user input may allow injection."
7608
+ };
7609
+ const remediations = {
7610
+ "sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
7611
+ "xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
7612
+ "command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
7613
+ "path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
7614
+ "hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
7615
+ "deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
7616
+ "ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
7617
+ "xxe": "Disable external entity processing in XML parser configuration.",
7618
+ "csrf": "Implement CSRF tokens for all state-changing requests.",
7619
+ "ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
7620
+ };
7621
+ const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
7622
+ const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
7623
+ return {
7624
+ summary,
7625
+ explanation: `The pattern "${gap.patternId}" detected a potential ${gap.categoryName} vulnerability at line ${gap.lineStart}. This type of issue has ${gap.severity} severity and was detected with ${gap.confidence} confidence.`,
7626
+ risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
7627
+ remediation,
7628
+ references: []
7629
+ };
7630
+ }
7631
+
7632
+ // src/ai/template-filler.ts
7633
+ var SYSTEM_PROMPT2 = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
7634
+ Given a code snippet and a list of template variables, suggest appropriate values for each variable.
7635
+
7636
+ For each variable, analyze:
7637
+ 1. The code snippet to extract relevant information (class names, function names, etc.)
7638
+ 2. The variable description to understand what's needed
7639
+ 3. The variable type to ensure correct formatting
7640
+
7641
+ Always respond with valid JSON matching this structure:
7642
+ {
7643
+ "suggestions": [
7644
+ {
7645
+ "name": "variableName",
7646
+ "value": "suggested value",
7647
+ "reasoning": "why this value was chosen",
7648
+ "confidence": 0.0-1.0
7649
+ }
7650
+ ]
7651
+ }
7652
+
7653
+ For arrays, use: "value": ["item1", "item2"]
7654
+ For booleans, use: "value": true or "value": false
7655
+ For numbers, use: "value": 42`;
7656
+ async function suggestVariables(request, config2) {
7657
+ const ai = createAIService(config2);
7658
+ if (!ai.isConfigured()) {
7659
+ return {
7660
+ success: true,
7661
+ data: extractVariablesFromCode(request),
7662
+ durationMs: 0
7663
+ };
7664
+ }
7665
+ const prompt = buildVariablePrompt(request);
7666
+ const startTime = Date.now();
7667
+ const response = await ai.completeJSON({
7668
+ systemPrompt: SYSTEM_PROMPT2,
7669
+ messages: [{ role: "user", content: prompt }],
7670
+ maxTokens: 1024,
7671
+ temperature: 0.2
7672
+ });
7673
+ if (!response.success || !response.data) {
7674
+ return {
7675
+ success: true,
7676
+ data: extractVariablesFromCode(request),
7677
+ durationMs: Date.now() - startTime
7678
+ };
7679
+ }
7680
+ const suggestions = /* @__PURE__ */ new Map();
7681
+ const unfilled = [];
7682
+ const values = { ...request.existingValues };
7683
+ const suggestionsList = response.data.suggestions ?? [];
7684
+ for (const suggestion of suggestionsList) {
7685
+ suggestions.set(suggestion.name, suggestion);
7686
+ if (!(suggestion.name in values)) {
7687
+ values[suggestion.name] = suggestion.value;
7688
+ }
7689
+ }
7690
+ for (const variable of request.variables) {
7691
+ if (!suggestions.has(variable.name) && !(variable.name in values)) {
7692
+ if (variable.defaultValue !== void 0) {
7693
+ values[variable.name] = variable.defaultValue;
7694
+ } else {
7695
+ unfilled.push(variable.name);
7696
+ }
7697
+ }
7698
+ }
7699
+ const result = {
7700
+ success: true,
7701
+ data: { suggestions, unfilled, values },
7702
+ durationMs: response.durationMs
7703
+ };
7704
+ if (response.usage) {
7705
+ result.usage = response.usage;
7706
+ }
7707
+ return result;
7708
+ }
7709
+ function buildVariablePrompt(request) {
7710
+ const parts = [];
7711
+ parts.push("Analyze this code and suggest values for the template variables:\n");
7712
+ parts.push("**Code:**");
7713
+ parts.push("```");
7714
+ parts.push(request.codeSnippet);
7715
+ parts.push("```\n");
7716
+ parts.push(`**File:** ${request.filePath}
7233
7717
  `);
7234
7718
  if (request.gap) {
7235
7719
  parts.push(`**Category:** ${request.gap.categoryName}`);
@@ -7372,606 +7856,258 @@ function toPascalCase(str) {
7372
7856
  return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
7373
7857
  }
7374
7858
 
7375
- // src/cli/generate-formatters.ts
7376
- function formatGeneratedTerminal(tests, basePath) {
7377
- const lines = [];
7378
- if (tests.length === 0) {
7379
- lines.push(chalk5.yellow("No tests generated."));
7380
- return lines.join("\n");
7381
- }
7382
- lines.push(chalk5.bold.cyan(`
7383
- Generated ${tests.length} test(s):
7384
- `));
7385
- lines.push(chalk5.gray("\u2500".repeat(60)));
7386
- for (const test of tests) {
7387
- const relGapPath = relative(basePath, test.gap.filePath);
7388
- lines.push("");
7389
- lines.push(chalk5.bold.white(`Test for: ${test.gap.categoryName}`));
7390
- lines.push(chalk5.gray(` Gap location: ${relGapPath}:${test.gap.lineStart}`));
7391
- lines.push(chalk5.gray(` Template: ${test.template.id}`));
7392
- lines.push(chalk5.gray(` Output: ${test.suggestedPath}`));
7393
- lines.push("");
7394
- lines.push(chalk5.cyan(`// --- ${test.suggestedPath} ---`));
7395
- lines.push("");
7396
- if (test.result.imports.length > 0) {
7397
- for (const imp of test.result.imports) {
7398
- lines.push(chalk5.gray(imp));
7399
- }
7400
- lines.push("");
7859
+ // src/ai/pattern-suggester.ts
7860
+ var SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
7861
+ Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
7862
+
7863
+ Your patterns should:
7864
+ 1. Be specific enough to avoid false positives
7865
+ 2. Be general enough to catch variations
7866
+ 3. Use standard regex syntax (no lookbehind for compatibility)
7867
+ 4. Include examples of what matches and what doesn't
7868
+
7869
+ Always respond with valid JSON matching this structure:
7870
+ {
7871
+ "suggestions": [
7872
+ {
7873
+ "id": "pattern-id-kebab-case",
7874
+ "pattern": "regex pattern here",
7875
+ "description": "What this pattern detects",
7876
+ "confidence": "high|medium|low",
7877
+ "matchExample": "code that should match",
7878
+ "safeExample": "similar code that should NOT match",
7879
+ "reasoning": "Why this pattern works"
7401
7880
  }
7402
- lines.push(test.result.content);
7403
- lines.push("");
7404
- lines.push(chalk5.gray("\u2500".repeat(60)));
7881
+ ]
7882
+ }
7883
+
7884
+ Important:
7885
+ - Escape backslashes properly for JSON (use \\\\s not \\s)
7886
+ - Test your patterns mentally against the examples
7887
+ - Prefer simpler patterns that are less likely to cause ReDoS`;
7888
+ async function suggestPatterns(request, config2) {
7889
+ const ai = createAIService(config2);
7890
+ if (!ai.isConfigured()) {
7891
+ return {
7892
+ success: false,
7893
+ error: "AI service not configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.",
7894
+ durationMs: 0
7895
+ };
7405
7896
  }
7406
- lines.push("");
7407
- lines.push(chalk5.bold("Summary:"));
7408
- lines.push(` Tests generated: ${chalk5.green(tests.length.toString())}`);
7409
- const categories = new Set(tests.map((t) => t.category.id));
7410
- lines.push(` Categories covered: ${chalk5.cyan(categories.size.toString())}`);
7411
- if (tests.some((t) => t.result.unresolved.length > 0)) {
7412
- const unresolvedCount = tests.reduce((acc, t) => acc + t.result.unresolved.length, 0);
7413
- lines.push(chalk5.yellow(` Unresolved placeholders: ${unresolvedCount}`));
7414
- lines.push(chalk5.gray(" (Some placeholders need manual completion)"));
7897
+ const prompt = buildPatternPrompt(request);
7898
+ const response = await ai.completeJSON({
7899
+ systemPrompt: SYSTEM_PROMPT3,
7900
+ messages: [{ role: "user", content: prompt }],
7901
+ maxTokens: 2048,
7902
+ temperature: 0.3
7903
+ });
7904
+ if (!response.success || !response.data) {
7905
+ return {
7906
+ success: false,
7907
+ error: response.error ?? "Failed to generate patterns",
7908
+ durationMs: response.durationMs
7909
+ };
7415
7910
  }
7416
- lines.push("");
7417
- lines.push(chalk5.gray("This is a dry run. Use --write to save files."));
7418
- return lines.join("\n");
7419
- }
7420
- function formatGeneratedJson(tests) {
7421
- const output = tests.map((test) => ({
7422
- gap: {
7423
- categoryId: test.gap.categoryId,
7424
- categoryName: test.gap.categoryName,
7425
- filePath: test.gap.filePath,
7426
- lineStart: test.gap.lineStart,
7427
- severity: test.gap.severity,
7428
- confidence: test.gap.confidence
7429
- },
7430
- template: {
7431
- id: test.template.id,
7432
- framework: test.template.framework,
7433
- language: test.template.language
7434
- },
7435
- suggestedPath: test.suggestedPath,
7436
- content: test.result.content,
7437
- imports: test.result.imports,
7438
- fixtures: test.result.fixtures,
7439
- substituted: test.result.substituted,
7440
- unresolved: test.result.unresolved
7441
- }));
7442
- return JSON.stringify(output, null, 2);
7911
+ const validated = validatePatterns(
7912
+ response.data.suggestions ?? [],
7913
+ request.vulnerableCode,
7914
+ request.safeCode ?? []
7915
+ );
7916
+ const result = {
7917
+ success: true,
7918
+ data: validated,
7919
+ durationMs: response.durationMs
7920
+ };
7921
+ if (response.usage) {
7922
+ result.usage = response.usage;
7923
+ }
7924
+ return result;
7443
7925
  }
7444
- function suggestTestPath(sourceFile, template, basePath) {
7445
- const dir = dirname(sourceFile);
7446
- const name = basename(sourceFile);
7447
- const nameWithoutExt = name.replace(/\.[^.]+$/, "");
7448
- const extMap = {
7449
- python: ".py",
7450
- typescript: ".ts",
7451
- javascript: ".js",
7452
- go: "_test.go",
7453
- java: "Test.java",
7454
- rust: ".rs"
7455
- };
7456
- const ext = extMap[template.language] ?? ".test.ts";
7457
- let testFileName;
7458
- switch (template.language) {
7459
- case "python":
7460
- testFileName = `test_${nameWithoutExt}${ext}`;
7461
- break;
7462
- case "go":
7463
- testFileName = `${nameWithoutExt}${ext}`;
7464
- break;
7465
- case "java":
7466
- testFileName = `${nameWithoutExt}${ext}`;
7467
- break;
7468
- default:
7469
- testFileName = `${nameWithoutExt}.test${ext}`;
7926
+ function buildPatternPrompt(request) {
7927
+ const parts = [];
7928
+ parts.push(`Generate regex patterns to detect ${request.category} vulnerabilities in ${request.language} code.
7929
+ `);
7930
+ parts.push("**Vulnerable code samples (patterns SHOULD match these):**");
7931
+ for (let i = 0; i < request.vulnerableCode.length; i++) {
7932
+ parts.push(`
7933
+ Example ${i + 1}:`);
7934
+ parts.push("```");
7935
+ parts.push(request.vulnerableCode[i] ?? "");
7936
+ parts.push("```");
7470
7937
  }
7471
- const relativeSrc = relative(basePath, dir);
7472
- const testDir = relativeSrc.replace(/^src/, "tests");
7473
- return `${testDir}/${testFileName}`;
7474
- }
7475
- function extractVariablesFromGap(gap) {
7476
- const fileName = basename(gap.filePath);
7477
- const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
7478
- const funcMatch = gap.codeSnippet.match(/(?:def|function|async function|const|let|var)\s+(\w+)/);
7479
- const classMatch = gap.codeSnippet.match(/(?:class)\s+(\w+)/);
7480
- return {
7481
- // File info
7482
- filePath: gap.filePath,
7483
- fileName,
7484
- fileNameWithoutExt,
7485
- lineNumber: gap.lineStart,
7486
- // Category info
7487
- categoryId: gap.categoryId,
7488
- categoryName: gap.categoryName,
7489
- domain: gap.domain,
7490
- level: gap.level,
7491
- severity: gap.severity,
7492
- confidence: gap.confidence,
7493
- // Code context
7494
- codeSnippet: gap.codeSnippet,
7495
- functionName: funcMatch?.[1] ?? "targetFunction",
7496
- className: classMatch?.[1] ?? "TargetClass",
7497
- // Common template variables
7498
- testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
7499
- testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
7500
- // Pattern info
7501
- patternId: gap.patternId,
7502
- patternType: gap.patternType
7503
- };
7504
- }
7505
- async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
7506
- const baseVars = extractVariablesFromGap(gap);
7507
- const result = await suggestVariables(
7508
- {
7509
- codeSnippet: gap.codeSnippet,
7510
- filePath: gap.filePath,
7511
- variables: templateVariables,
7512
- gap,
7513
- existingValues: baseVars
7514
- },
7515
- aiConfig
7516
- );
7517
- if (result.success && result.data) {
7518
- return result.data.values;
7938
+ if (request.safeCode && request.safeCode.length > 0) {
7939
+ parts.push("\n**Safe code samples (patterns should NOT match these):**");
7940
+ for (let i = 0; i < request.safeCode.length; i++) {
7941
+ parts.push(`
7942
+ Safe ${i + 1}:`);
7943
+ parts.push("```");
7944
+ parts.push(request.safeCode[i] ?? "");
7945
+ parts.push("```");
7946
+ }
7519
7947
  }
7520
- return baseVars;
7521
- }
7522
- async function writeGeneratedTests(tests, basePath, outputDir) {
7523
- const summary = {
7524
- created: [],
7525
- updated: [],
7526
- failed: [],
7527
- totalTests: 0
7528
- };
7529
- const testsByFile = /* @__PURE__ */ new Map();
7530
- for (const test of tests) {
7531
- let outputPath;
7532
- if (outputDir) {
7533
- outputPath = resolve(basePath, outputDir, test.suggestedPath);
7534
- } else {
7535
- outputPath = resolve(basePath, test.suggestedPath);
7948
+ if (request.existingPatterns && request.existingPatterns.length > 0) {
7949
+ parts.push("\n**Existing patterns (avoid duplicating):**");
7950
+ for (const pattern of request.existingPatterns) {
7951
+ parts.push(`- ${pattern}`);
7536
7952
  }
7537
- const existing = testsByFile.get(outputPath) ?? [];
7538
- existing.push(test);
7539
- testsByFile.set(outputPath, existing);
7540
7953
  }
7541
- for (const [outputPath, fileTests] of testsByFile) {
7954
+ parts.push(`
7955
+ Generate up to ${request.maxSuggestions ?? 3} distinct patterns.`);
7956
+ parts.push("Focus on patterns that will have high precision (low false positives).");
7957
+ return parts.join("\n");
7958
+ }
7959
+ function validatePatterns(suggestions, vulnerableCode, safeCode) {
7960
+ const validated = [];
7961
+ const rejected = [];
7962
+ for (const suggestion of suggestions) {
7542
7963
  try {
7543
- const dir = dirname(outputPath);
7544
- await mkdir(dir, { recursive: true });
7545
- let existingContent = "";
7546
- let fileExists = false;
7547
- try {
7548
- existingContent = await readFile(outputPath, "utf-8");
7549
- fileExists = true;
7550
- } catch {
7551
- }
7552
- const contentParts = [];
7553
- const allImports = /* @__PURE__ */ new Set();
7554
- for (const test of fileTests) {
7555
- for (const imp of test.result.imports) {
7556
- allImports.add(imp);
7964
+ const regex = new RegExp(suggestion.pattern, "gm");
7965
+ let matchCount = 0;
7966
+ for (const code of vulnerableCode) {
7967
+ regex.lastIndex = 0;
7968
+ if (regex.test(code)) {
7969
+ matchCount++;
7557
7970
  }
7558
7971
  }
7559
- if (!fileExists && allImports.size > 0) {
7560
- contentParts.push(Array.from(allImports).join("\n"));
7561
- contentParts.push("");
7562
- }
7563
- for (const test of fileTests) {
7564
- contentParts.push(`// Test for ${test.gap.categoryName}`);
7565
- contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
7566
- contentParts.push(`// Generated by Pinata`);
7567
- contentParts.push("");
7568
- contentParts.push(test.result.content);
7569
- contentParts.push("");
7972
+ let falsePositives = 0;
7973
+ for (const code of safeCode) {
7974
+ regex.lastIndex = 0;
7975
+ if (regex.test(code)) {
7976
+ falsePositives++;
7977
+ }
7570
7978
  }
7571
- const newContent = contentParts.join("\n");
7572
- let finalContent;
7573
- let appended = false;
7574
- if (fileExists) {
7575
- finalContent = existingContent.trimEnd() + "\n\n" + newContent;
7576
- appended = true;
7577
- } else {
7578
- finalContent = newContent;
7979
+ if (hasRedosPotential(suggestion.pattern)) {
7980
+ rejected.push({
7981
+ pattern: suggestion.pattern,
7982
+ reason: "Pattern may be vulnerable to ReDoS"
7983
+ });
7984
+ continue;
7579
7985
  }
7580
- await writeFile(outputPath, finalContent, "utf-8");
7581
- for (const test of fileTests) {
7582
- const result = {
7583
- path: outputPath,
7584
- created: !fileExists,
7585
- appended,
7586
- categoryId: test.gap.categoryId,
7587
- gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
7588
- };
7589
- if (fileExists) {
7590
- summary.updated.push(result);
7591
- } else {
7592
- summary.created.push(result);
7986
+ if (matchCount > 0) {
7987
+ if (falsePositives > 0 && suggestion.confidence === "high") {
7988
+ suggestion.confidence = "medium";
7593
7989
  }
7594
- summary.totalTests++;
7990
+ if (matchCount < vulnerableCode.length / 2 && suggestion.confidence === "high") {
7991
+ suggestion.confidence = "medium";
7992
+ }
7993
+ validated.push(suggestion);
7994
+ } else {
7995
+ rejected.push({
7996
+ pattern: suggestion.pattern,
7997
+ reason: `Pattern did not match any vulnerable samples (0/${vulnerableCode.length})`
7998
+ });
7595
7999
  }
7596
8000
  } catch (error) {
7597
- summary.failed.push({
7598
- path: outputPath,
7599
- error: error instanceof Error ? error.message : String(error)
8001
+ rejected.push({
8002
+ pattern: suggestion.pattern,
8003
+ reason: `Invalid regex: ${error instanceof Error ? error.message : "Unknown error"}`
7600
8004
  });
7601
8005
  }
7602
8006
  }
7603
- return ok(summary);
8007
+ return { suggestions: validated, rejected };
7604
8008
  }
7605
- function formatWriteSummary(summary, basePath) {
7606
- const lines = [];
7607
- lines.push("");
7608
- lines.push(chalk5.bold.cyan("Write Summary:"));
7609
- lines.push(chalk5.gray("\u2500".repeat(60)));
7610
- if (summary.created.length > 0) {
7611
- const uniquePaths = new Set(summary.created.map((r) => r.path));
7612
- lines.push("");
7613
- lines.push(chalk5.green.bold(`Created ${uniquePaths.size} file(s):`));
7614
- for (const path2 of uniquePaths) {
7615
- const relPath = relative(basePath, path2);
7616
- const testsInFile = summary.created.filter((r) => r.path === path2).length;
7617
- lines.push(chalk5.green(` + ${relPath} (${testsInFile} test(s))`));
7618
- }
7619
- }
7620
- if (summary.updated.length > 0) {
7621
- const uniquePaths = new Set(summary.updated.map((r) => r.path));
7622
- lines.push("");
7623
- lines.push(chalk5.yellow.bold(`Updated ${uniquePaths.size} file(s):`));
7624
- for (const path2 of uniquePaths) {
7625
- const relPath = relative(basePath, path2);
7626
- const testsInFile = summary.updated.filter((r) => r.path === path2).length;
7627
- lines.push(chalk5.yellow(` ~ ${relPath} (${testsInFile} test(s) appended)`));
7628
- }
8009
+ function hasRedosPotential(pattern) {
8010
+ if (/\([^)]*[+*][^)]*\)[+*]/.test(pattern)) {
8011
+ return true;
7629
8012
  }
7630
- if (summary.failed.length > 0) {
7631
- lines.push("");
7632
- lines.push(chalk5.red.bold(`Failed to write ${summary.failed.length} file(s):`));
7633
- for (const fail of summary.failed) {
7634
- const relPath = relative(basePath, fail.path);
7635
- lines.push(chalk5.red(` \u2717 ${relPath}: ${fail.error}`));
8013
+ if (/\([^)]*\|[^)]*\)[+*]/.test(pattern)) {
8014
+ const alternationMatch = pattern.match(/\(([^)]+)\)/g);
8015
+ if (alternationMatch) {
8016
+ for (const alt of alternationMatch) {
8017
+ if (/\w+\|\w*\w/.test(alt) && /[+*]/.test(alt)) {
8018
+ return true;
8019
+ }
8020
+ }
7636
8021
  }
7637
8022
  }
7638
- lines.push("");
7639
- lines.push(chalk5.gray("\u2500".repeat(60)));
7640
- 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)`));
7641
- if (summary.failed.length > 0) {
7642
- lines.push(chalk5.red(`Failures: ${summary.failed.length}`));
7643
- }
7644
- return lines.join("\n");
8023
+ return false;
7645
8024
  }
7646
8025
 
7647
- // src/ai/explainer.ts
7648
- var SYSTEM_PROMPT2 = `You are a security expert explaining code vulnerabilities to developers.
7649
- Your explanations should be:
7650
- - Clear and actionable
7651
- - Focused on the specific code pattern
7652
- - Include concrete remediation steps
7653
- - Reference relevant security standards (OWASP, CWE) when applicable
7654
-
7655
- Always respond with valid JSON matching this structure:
7656
- {
7657
- "summary": "1-2 sentence summary",
7658
- "explanation": "Detailed explanation of the vulnerability",
7659
- "risk": "What an attacker could do if this is exploited",
7660
- "remediation": "Step-by-step instructions to fix",
7661
- "safeExample": "Code example showing the safe pattern",
7662
- "references": ["optional array of CVE/CWE/OWASP references"]
7663
- }`;
7664
- async function explainGap(gap, category, config2) {
7665
- const ai = createAIService(config2);
7666
- if (!ai.isConfigured()) {
7667
- return {
7668
- success: false,
7669
- error: "AI service not configured",
7670
- durationMs: 0
7671
- };
7672
- }
7673
- const prompt = buildExplainPrompt(gap);
7674
- const response = await ai.completeJSON({
7675
- systemPrompt: SYSTEM_PROMPT2,
7676
- messages: [{ role: "user", content: prompt }],
7677
- maxTokens: 1024,
7678
- temperature: 0.3
7679
- });
7680
- return response;
7681
- }
7682
- function buildExplainPrompt(gap, category) {
7683
- const parts = [];
7684
- parts.push(`Explain this security finding:
7685
- `);
7686
- parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
7687
- parts.push(`**Severity:** ${gap.severity}`);
7688
- parts.push(`**Confidence:** ${gap.confidence}`);
7689
- parts.push(`**File:** ${gap.filePath}`);
7690
- parts.push(`**Line:** ${gap.lineStart}`);
7691
- if (gap.codeSnippet) {
7692
- parts.push(`
7693
- **Code:**
7694
- \`\`\`
7695
- ${gap.codeSnippet}
7696
- \`\`\``);
7697
- }
7698
- parts.push(`
7699
- **Pattern:** ${gap.patternId}`);
7700
- parts.push(`**Detection Type:** ${gap.patternType}`);
7701
- parts.push(`
7702
- Provide a clear, actionable explanation for a developer.`);
7703
- return parts.join("\n");
7704
- }
7705
- function generateFallbackExplanation(gap) {
7706
- const summaries = {
7707
- "sql-injection": "SQL query constructed with user input may allow injection attacks.",
7708
- "xss": "User input rendered without escaping may allow script injection.",
7709
- "command-injection": "Shell command constructed with user input may allow command execution.",
7710
- "path-traversal": "File path constructed with user input may allow directory traversal.",
7711
- "hardcoded-secrets": "Sensitive credentials found in source code.",
7712
- "deserialization": "Untrusted data deserialization may allow code execution.",
7713
- "ssrf": "Server-side request with user-controlled URL may allow internal access.",
7714
- "xxe": "XML parser may be vulnerable to external entity injection.",
7715
- "csrf": "State-changing request lacks CSRF protection.",
7716
- "ldap-injection": "LDAP query constructed with user input may allow injection."
7717
- };
7718
- const remediations = {
7719
- "sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
7720
- "xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
7721
- "command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
7722
- "path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
7723
- "hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
7724
- "deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
7725
- "ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
7726
- "xxe": "Disable external entity processing in XML parser configuration.",
7727
- "csrf": "Implement CSRF tokens for all state-changing requests.",
7728
- "ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
7729
- };
7730
- const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
7731
- const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
7732
- return {
7733
- summary,
7734
- explanation: `The pattern "${gap.patternId}" detected a potential ${gap.categoryName} vulnerability at line ${gap.lineStart}. This type of issue has ${gap.severity} severity and was detected with ${gap.confidence} confidence.`,
7735
- risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
7736
- remediation,
7737
- references: []
7738
- };
7739
- }
7740
-
7741
- // src/ai/pattern-suggester.ts
7742
- var SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
7743
- Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
7744
-
7745
- Your patterns should:
7746
- 1. Be specific enough to avoid false positives
7747
- 2. Be general enough to catch variations
7748
- 3. Use standard regex syntax (no lookbehind for compatibility)
7749
- 4. Include examples of what matches and what doesn't
7750
-
7751
- Always respond with valid JSON matching this structure:
7752
- {
7753
- "suggestions": [
7754
- {
7755
- "id": "pattern-id-kebab-case",
7756
- "pattern": "regex pattern here",
7757
- "description": "What this pattern detects",
7758
- "confidence": "high|medium|low",
7759
- "matchExample": "code that should match",
7760
- "safeExample": "similar code that should NOT match",
7761
- "reasoning": "Why this pattern works"
7762
- }
7763
- ]
7764
- }
7765
-
7766
- Important:
7767
- - Escape backslashes properly for JSON (use \\\\s not \\s)
7768
- - Test your patterns mentally against the examples
7769
- - Prefer simpler patterns that are less likely to cause ReDoS`;
7770
- async function suggestPatterns(request, config2) {
7771
- const ai = createAIService(config2);
7772
- if (!ai.isConfigured()) {
7773
- return {
7774
- success: false,
7775
- error: "AI service not configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.",
7776
- durationMs: 0
7777
- };
7778
- }
7779
- const prompt = buildPatternPrompt(request);
7780
- const response = await ai.completeJSON({
7781
- systemPrompt: SYSTEM_PROMPT3,
7782
- messages: [{ role: "user", content: prompt }],
7783
- maxTokens: 2048,
7784
- temperature: 0.3
7785
- });
7786
- if (!response.success || !response.data) {
7787
- return {
7788
- success: false,
7789
- error: response.error ?? "Failed to generate patterns",
7790
- durationMs: response.durationMs
7791
- };
7792
- }
7793
- const validated = validatePatterns(
7794
- response.data.suggestions ?? [],
7795
- request.vulnerableCode,
7796
- request.safeCode ?? []
7797
- );
7798
- const result = {
7799
- success: true,
7800
- data: validated,
7801
- durationMs: response.durationMs
7802
- };
7803
- if (response.usage) {
7804
- result.usage = response.usage;
7805
- }
7806
- return result;
7807
- }
7808
- function buildPatternPrompt(request) {
7809
- const parts = [];
7810
- parts.push(`Generate regex patterns to detect ${request.category} vulnerabilities in ${request.language} code.
7811
- `);
7812
- parts.push("**Vulnerable code samples (patterns SHOULD match these):**");
7813
- for (let i = 0; i < request.vulnerableCode.length; i++) {
7814
- parts.push(`
7815
- Example ${i + 1}:`);
7816
- parts.push("```");
7817
- parts.push(request.vulnerableCode[i] ?? "");
7818
- parts.push("```");
7819
- }
7820
- if (request.safeCode && request.safeCode.length > 0) {
7821
- parts.push("\n**Safe code samples (patterns should NOT match these):**");
7822
- for (let i = 0; i < request.safeCode.length; i++) {
7823
- parts.push(`
7824
- Safe ${i + 1}:`);
7825
- parts.push("```");
7826
- parts.push(request.safeCode[i] ?? "");
7827
- parts.push("```");
7828
- }
7829
- }
7830
- if (request.existingPatterns && request.existingPatterns.length > 0) {
7831
- parts.push("\n**Existing patterns (avoid duplicating):**");
7832
- for (const pattern of request.existingPatterns) {
7833
- parts.push(`- ${pattern}`);
7834
- }
7835
- }
7836
- parts.push(`
7837
- Generate up to ${request.maxSuggestions ?? 3} distinct patterns.`);
7838
- parts.push("Focus on patterns that will have high precision (low false positives).");
7839
- return parts.join("\n");
7840
- }
7841
- function validatePatterns(suggestions, vulnerableCode, safeCode) {
7842
- const validated = [];
7843
- const rejected = [];
7844
- for (const suggestion of suggestions) {
7845
- try {
7846
- const regex = new RegExp(suggestion.pattern, "gm");
7847
- let matchCount = 0;
7848
- for (const code of vulnerableCode) {
7849
- regex.lastIndex = 0;
7850
- if (regex.test(code)) {
7851
- matchCount++;
7852
- }
7853
- }
7854
- let falsePositives = 0;
7855
- for (const code of safeCode) {
7856
- regex.lastIndex = 0;
7857
- if (regex.test(code)) {
7858
- falsePositives++;
7859
- }
7860
- }
7861
- if (hasRedosPotential(suggestion.pattern)) {
7862
- rejected.push({
7863
- pattern: suggestion.pattern,
7864
- reason: "Pattern may be vulnerable to ReDoS"
7865
- });
7866
- continue;
7867
- }
7868
- if (matchCount > 0) {
7869
- if (falsePositives > 0 && suggestion.confidence === "high") {
7870
- suggestion.confidence = "medium";
7871
- }
7872
- if (matchCount < vulnerableCode.length / 2 && suggestion.confidence === "high") {
7873
- suggestion.confidence = "medium";
7874
- }
7875
- validated.push(suggestion);
7876
- } else {
7877
- rejected.push({
7878
- pattern: suggestion.pattern,
7879
- reason: `Pattern did not match any vulnerable samples (0/${vulnerableCode.length})`
7880
- });
7881
- }
7882
- } catch (error) {
7883
- rejected.push({
7884
- pattern: suggestion.pattern,
7885
- reason: `Invalid regex: ${error instanceof Error ? error.message : "Unknown error"}`
7886
- });
7887
- }
7888
- }
7889
- return { suggestions: validated, rejected };
7890
- }
7891
- function hasRedosPotential(pattern) {
7892
- if (/\([^)]*[+*][^)]*\)[+*]/.test(pattern)) {
7893
- return true;
7894
- }
7895
- if (/\([^)]*\|[^)]*\)[+*]/.test(pattern)) {
7896
- const alternationMatch = pattern.match(/\(([^)]+)\)/g);
7897
- if (alternationMatch) {
7898
- for (const alt of alternationMatch) {
7899
- if (/\w+\|\w*\w/.test(alt) && /[+*]/.test(alt)) {
7900
- return true;
7901
- }
7902
- }
8026
+ // src/cli/index.ts
8027
+ init_results_cache();
8028
+ var __filename2 = fileURLToPath(import.meta.url);
8029
+ var __dirname2 = dirname(__filename2);
8030
+ function getDefinitionsPath() {
8031
+ const candidates = [
8032
+ resolve(__dirname2, "../../src/categories/definitions"),
8033
+ resolve(process.cwd(), "src/categories/definitions"),
8034
+ resolve(__dirname2, "../categories/definitions")
8035
+ ];
8036
+ for (const candidate of candidates) {
8037
+ if (existsSync(candidate)) {
8038
+ return candidate;
7903
8039
  }
7904
8040
  }
7905
- return false;
8041
+ return candidates[0];
7906
8042
  }
7907
-
7908
- // src/cli/index.ts
7909
8043
  init_results_cache();
7910
8044
  var SEVERITY_COLORS2 = {
7911
- critical: chalk5.red.bold,
7912
- high: chalk5.red,
7913
- medium: chalk5.yellow,
7914
- low: chalk5.gray
8045
+ critical: chalk7.red.bold,
8046
+ high: chalk7.red,
8047
+ medium: chalk7.yellow,
8048
+ low: chalk7.gray
7915
8049
  };
7916
8050
  var DOMAIN_COLORS2 = {
7917
- security: chalk5.red,
7918
- data: chalk5.blue,
7919
- concurrency: chalk5.magenta,
7920
- input: chalk5.cyan,
7921
- resource: chalk5.yellow,
7922
- reliability: chalk5.green,
7923
- performance: chalk5.yellowBright,
7924
- platform: chalk5.gray,
7925
- business: chalk5.white,
7926
- compliance: chalk5.blueBright
8051
+ security: chalk7.red,
8052
+ data: chalk7.blue,
8053
+ concurrency: chalk7.magenta,
8054
+ input: chalk7.cyan,
8055
+ resource: chalk7.yellow,
8056
+ reliability: chalk7.green,
8057
+ performance: chalk7.yellowBright,
8058
+ platform: chalk7.gray,
8059
+ business: chalk7.white,
8060
+ compliance: chalk7.blueBright
7927
8061
  };
7928
8062
  var GRADE_COLORS = {
7929
- A: chalk5.green.bold,
7930
- B: chalk5.green,
7931
- C: chalk5.yellow,
7932
- D: chalk5.red,
7933
- F: chalk5.red.bold
8063
+ A: chalk7.green.bold,
8064
+ B: chalk7.green,
8065
+ C: chalk7.yellow,
8066
+ D: chalk7.red,
8067
+ F: chalk7.red.bold
7934
8068
  };
7935
8069
  var BANNER = `
7936
- ${chalk5.cyan(" ____ _ _ ")}
7937
- ${chalk5.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
7938
- ${chalk5.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
7939
- ${chalk5.cyan("| __/| | | | | (_| | || (_| |")}
7940
- ${chalk5.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
8070
+ ${chalk7.cyan(" ____ _ _ ")}
8071
+ ${chalk7.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
8072
+ ${chalk7.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
8073
+ ${chalk7.cyan("| __/| | | | | (_| | || (_| |")}
8074
+ ${chalk7.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
7941
8075
  `;
7942
8076
  function formatScanTerminal(result, basePath) {
7943
8077
  const lines = [];
7944
8078
  lines.push(BANNER);
7945
- lines.push(chalk5.gray(`Analyzing: ${result.targetDirectory}`));
7946
- lines.push(chalk5.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
8079
+ lines.push(chalk7.gray(`Analyzing: ${result.targetDirectory}`));
8080
+ const projectTypeLabel = getProjectTypeDescription(result.projectType.type);
8081
+ lines.push(chalk7.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
8082
+ lines.push(chalk7.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
7947
8083
  lines.push("");
7948
8084
  lines.push(formatScoreBox(result.score));
7949
8085
  lines.push("");
7950
- lines.push(chalk5.bold("Domain Coverage:"));
8086
+ lines.push(chalk7.bold("Domain Coverage:"));
7951
8087
  lines.push(formatDomainCoverage(result.coverage));
7952
8088
  lines.push("");
7953
8089
  if (result.gaps.length > 0) {
7954
8090
  lines.push(formatGapsSummary(result.gaps, basePath));
7955
8091
  lines.push("");
7956
8092
  } else {
7957
- lines.push(chalk5.green.bold("No gaps detected! Your codebase has good test coverage."));
8093
+ lines.push(chalk7.green.bold("No gaps detected! Your codebase has good test coverage."));
7958
8094
  lines.push("");
7959
8095
  }
7960
8096
  if (result.gaps.length > 0) {
7961
- lines.push(chalk5.gray("Run `pinata generate --gaps` to create tests for these gaps."));
8097
+ lines.push(chalk7.gray("Run `pinata generate --gaps` to create tests for these gaps."));
7962
8098
  }
7963
- lines.push(chalk5.gray(`
8099
+ lines.push(chalk7.gray(`
7964
8100
  Scan completed in ${result.durationMs}ms`));
7965
8101
  return lines.join("\n");
7966
8102
  }
7967
8103
  function formatScoreBox(score) {
7968
- const gradeColor = GRADE_COLORS[score.grade] ?? chalk5.white;
8104
+ const gradeColor = GRADE_COLORS[score.grade] ?? chalk7.white;
7969
8105
  const scoreStr = `Pinata Score: ${score.overall}/100 ${gradeColor(`(${score.grade})`)}`;
7970
8106
  const boxWidth = 60;
7971
8107
  const padding = Math.floor((boxWidth - scoreStr.length) / 2);
7972
- const top = chalk5.cyan("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
7973
- const middle = chalk5.cyan("\u2551") + " ".repeat(padding) + scoreStr + " ".repeat(boxWidth - padding - scoreStr.length) + chalk5.cyan("\u2551");
7974
- const bottom = chalk5.cyan("\u255A" + "\u2550".repeat(boxWidth) + "\u255D");
8108
+ const top = chalk7.cyan("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
8109
+ const middle = chalk7.cyan("\u2551") + " ".repeat(padding) + scoreStr + " ".repeat(boxWidth - padding - scoreStr.length) + chalk7.cyan("\u2551");
8110
+ const bottom = chalk7.cyan("\u255A" + "\u2550".repeat(boxWidth) + "\u255D");
7975
8111
  return `${top}
7976
8112
  ${middle}
7977
8113
  ${bottom}`;
@@ -7986,14 +8122,14 @@ function formatDomainCoverage(coverage) {
7986
8122
  }
7987
8123
  const percent = domainCoverage.coveragePercent;
7988
8124
  const filledWidth = Math.round(percent / 100 * barWidth);
7989
- const bar = chalk5.green("\u2588".repeat(filledWidth)) + chalk5.gray("\u2591".repeat(barWidth - filledWidth));
7990
- const domainColor = DOMAIN_COLORS2[domain] ?? chalk5.white;
8125
+ const bar = chalk7.green("\u2588".repeat(filledWidth)) + chalk7.gray("\u2591".repeat(barWidth - filledWidth));
8126
+ const domainColor = DOMAIN_COLORS2[domain] ?? chalk7.white;
7991
8127
  const domainName = domain.padEnd(15);
7992
8128
  const stats = `${domainCoverage.categoriesCovered}/${domainCoverage.categoriesScanned} categories`;
7993
8129
  lines.push(` ${domainColor(domainName)} ${bar} ${percent.toString().padStart(3)}% (${stats})`);
7994
8130
  }
7995
8131
  if (lines.length === 0) {
7996
- lines.push(chalk5.gray(" No domain coverage data available."));
8132
+ lines.push(chalk7.gray(" No domain coverage data available."));
7997
8133
  }
7998
8134
  return lines.join("\n");
7999
8135
  }
@@ -8004,48 +8140,48 @@ function formatGapsSummary(gaps, basePath) {
8004
8140
  const medium = gaps.filter((g) => g.severity === "medium");
8005
8141
  const low = gaps.filter((g) => g.severity === "low");
8006
8142
  if (critical.length > 0) {
8007
- lines.push(chalk5.red.bold(`
8143
+ lines.push(chalk7.red.bold(`
8008
8144
  Critical Gaps (${critical.length}):`));
8009
8145
  for (const gap of critical.slice(0, 5)) {
8010
8146
  lines.push(formatGapLine(gap, basePath, "critical"));
8011
8147
  }
8012
8148
  if (critical.length > 5) {
8013
- lines.push(chalk5.gray(` ... and ${critical.length - 5} more critical gaps`));
8149
+ lines.push(chalk7.gray(` ... and ${critical.length - 5} more critical gaps`));
8014
8150
  }
8015
8151
  }
8016
8152
  if (high.length > 0) {
8017
- lines.push(chalk5.red(`
8153
+ lines.push(chalk7.red(`
8018
8154
  High Severity Gaps (${high.length}):`));
8019
8155
  for (const gap of high.slice(0, 5)) {
8020
8156
  lines.push(formatGapLine(gap, basePath, "high"));
8021
8157
  }
8022
8158
  if (high.length > 5) {
8023
- lines.push(chalk5.gray(` ... and ${high.length - 5} more high severity gaps`));
8159
+ lines.push(chalk7.gray(` ... and ${high.length - 5} more high severity gaps`));
8024
8160
  }
8025
8161
  }
8026
8162
  if (medium.length > 0) {
8027
- lines.push(chalk5.yellow(`
8163
+ lines.push(chalk7.yellow(`
8028
8164
  Medium Severity Gaps (${medium.length}):`));
8029
8165
  for (const gap of medium.slice(0, 3)) {
8030
8166
  lines.push(formatGapLine(gap, basePath, "medium"));
8031
8167
  }
8032
8168
  if (medium.length > 3) {
8033
- lines.push(chalk5.gray(` ... and ${medium.length - 3} more medium severity gaps`));
8169
+ lines.push(chalk7.gray(` ... and ${medium.length - 3} more medium severity gaps`));
8034
8170
  }
8035
8171
  }
8036
8172
  if (low.length > 0) {
8037
- lines.push(chalk5.gray(`
8173
+ lines.push(chalk7.gray(`
8038
8174
  Low Severity: ${low.length} gaps`));
8039
8175
  }
8040
8176
  return lines.join("\n");
8041
8177
  }
8042
8178
  function formatGapLine(gap, basePath, severity) {
8043
- const severityColor = SEVERITY_COLORS2[severity] ?? chalk5.white;
8179
+ const severityColor = SEVERITY_COLORS2[severity] ?? chalk7.white;
8044
8180
  const icon = severity === "critical" ? "\u26D4" : severity === "high" ? "\u{1F534}" : severity === "medium" ? "\u{1F7E1}" : "\u26AA";
8045
8181
  const relPath = relative(basePath, gap.filePath);
8046
8182
  const location = `${relPath}:${gap.lineStart}`;
8047
8183
  const confidence = gap.confidence.toUpperCase();
8048
- return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk5.cyan(location.padEnd(30))} ${chalk5.gray(confidence)} confidence`;
8184
+ return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk7.cyan(location.padEnd(30))} ${chalk7.gray(confidence)} confidence`;
8049
8185
  }
8050
8186
  function formatLanguages(result) {
8051
8187
  const languages = [];
@@ -8059,6 +8195,14 @@ function formatLanguages(result) {
8059
8195
  function formatScanJson(result) {
8060
8196
  const serializable = {
8061
8197
  targetDirectory: result.targetDirectory,
8198
+ projectType: {
8199
+ type: result.projectType.type,
8200
+ confidence: result.projectType.confidence,
8201
+ evidence: result.projectType.evidence,
8202
+ frameworks: result.projectType.frameworks,
8203
+ languages: result.projectType.languages,
8204
+ ...result.projectType.secondaryTypes && { secondaryTypes: result.projectType.secondaryTypes }
8205
+ },
8062
8206
  startedAt: result.startedAt.toISOString(),
8063
8207
  completedAt: result.completedAt.toISOString(),
8064
8208
  durationMs: result.durationMs,
@@ -8260,637 +8404,785 @@ function isValidScanOutputFormat(format) {
8260
8404
  return ["terminal", "json", "markdown", "sarif", "html", "junit-xml"].includes(format);
8261
8405
  }
8262
8406
 
8263
- // src/cli/index.ts
8264
- var __filename2 = fileURLToPath(import.meta.url);
8265
- var __dirname2 = dirname(__filename2);
8266
- function getDefinitionsPath() {
8267
- const candidates = [
8268
- // When running from dist/cli/index.js
8269
- resolve(__dirname2, "../../src/categories/definitions"),
8270
- // When running from project root via npx/npm
8271
- resolve(process.cwd(), "src/categories/definitions"),
8272
- // When bundled in dist (future)
8273
- resolve(__dirname2, "../categories/definitions")
8274
- ];
8275
- for (const candidate of candidates) {
8276
- if (existsSync(candidate)) {
8277
- return candidate;
8278
- }
8279
- }
8280
- return candidates[0];
8281
- }
8282
- var program = new Command();
8283
- program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
8284
- 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) => {
8285
- const isQuiet = Boolean(options["quiet"]);
8286
- const isVerbose = Boolean(options["verbose"]);
8287
- if (isQuiet) {
8288
- logger.configure({ level: "error" });
8289
- } else if (isVerbose) {
8290
- logger.configure({ level: "debug" });
8291
- }
8292
- const targetDirectory = resolve(targetPath ?? process.cwd());
8293
- if (!existsSync(targetDirectory)) {
8294
- console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
8295
- process.exit(1);
8296
- }
8297
- const outputFormat = String(options["output"] ?? "terminal");
8298
- if (!isValidScanOutputFormat(outputFormat)) {
8299
- console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
8300
- process.exit(1);
8301
- }
8302
- const validSeverities = ["critical", "high", "medium", "low"];
8303
- const minSeverity = String(options["severity"] ?? "low");
8304
- if (!validSeverities.includes(minSeverity)) {
8305
- console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
8306
- process.exit(1);
8307
- }
8308
- const validConfidences = ["high", "medium", "low"];
8309
- const minConfidence = String(options["confidence"] ?? "high");
8310
- if (!validConfidences.includes(minConfidence)) {
8311
- console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
8312
- process.exit(1);
8313
- }
8314
- const domainsStr = options["domains"];
8315
- let domains = [];
8316
- if (domainsStr) {
8317
- const domainList = domainsStr.split(",").map((d) => d.trim());
8318
- for (const domain of domainList) {
8319
- if (!RISK_DOMAINS.includes(domain)) {
8320
- console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
8321
- process.exit(1);
8322
- }
8407
+ // src/cli/commands/analyze.ts
8408
+ function registerAnalyzeCommand(program2) {
8409
+ program2.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("--output-file <path>", "Write output to file (useful for SARIF upload)").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("--execute", "Run dynamic tests in Docker sandbox to confirm vulnerabilities").option("--dry-run", "Preview generated tests without executing (use with --execute)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
8410
+ const isQuiet = Boolean(options["quiet"]);
8411
+ const isVerbose = Boolean(options["verbose"]);
8412
+ if (isQuiet) {
8413
+ logger.configure({ level: "error" });
8414
+ } else if (isVerbose) {
8415
+ logger.configure({ level: "debug" });
8323
8416
  }
8324
- domains = domainList;
8325
- }
8326
- const excludeStr = options["exclude"];
8327
- const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
8328
- const failOn = options["failOn"];
8329
- if (failOn && !["critical", "high", "medium"].includes(failOn)) {
8330
- console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
8331
- process.exit(1);
8332
- }
8333
- const showSpinner = outputFormat === "terminal" && !isQuiet;
8334
- const spinner = showSpinner ? ora("Loading categories...").start() : null;
8335
- try {
8336
- const store = createCategoryStore();
8337
- const definitionsPath = getDefinitionsPath();
8338
- logger.debug(`Loading categories from: ${definitionsPath}`);
8339
- const loadResult = await store.loadFromDirectory(definitionsPath);
8340
- if (!loadResult.success) {
8341
- spinner?.fail("Failed to load categories");
8342
- console.error(formatError(loadResult.error));
8417
+ const targetDirectory = resolve(targetPath ?? process.cwd());
8418
+ if (!existsSync(targetDirectory)) {
8419
+ console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
8343
8420
  process.exit(1);
8344
8421
  }
8345
- if (spinner) {
8346
- spinner.text = `Loaded ${loadResult.data} categories. Scanning...`;
8347
- }
8348
- logger.debug(`Loaded ${loadResult.data} categories`);
8349
- const scanner = createScanner(store);
8350
- const scanOptions = {
8351
- minSeverity,
8352
- minConfidence,
8353
- detectTestFiles: true
8354
- };
8355
- if (domains.length > 0) {
8356
- scanOptions.domains = domains;
8422
+ const outputFormat = String(options["output"] ?? "terminal");
8423
+ if (!isValidScanOutputFormat(outputFormat)) {
8424
+ console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
8425
+ process.exit(1);
8357
8426
  }
8358
- if (excludeDirs) {
8359
- scanOptions.excludeDirs = excludeDirs;
8427
+ const validSeverities = ["critical", "high", "medium", "low"];
8428
+ const minSeverity = String(options["severity"] ?? "low");
8429
+ if (!validSeverities.includes(minSeverity)) {
8430
+ console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
8431
+ process.exit(1);
8360
8432
  }
8361
- const scanResult = await scanner.scanDirectory(targetDirectory, scanOptions);
8362
- if (!scanResult.success) {
8363
- spinner?.fail("Scan failed");
8364
- console.error(formatError(scanResult.error));
8433
+ const validConfidences = ["high", "medium", "low"];
8434
+ const minConfidence = String(options["confidence"] ?? "high");
8435
+ if (!validConfidences.includes(minConfidence)) {
8436
+ console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
8365
8437
  process.exit(1);
8366
8438
  }
8367
- spinner?.stop();
8368
- const shouldVerify = Boolean(options["verify"]);
8369
- if (shouldVerify && scanResult.data.gaps.length > 0) {
8370
- const { hasApiKey: hasApiKey2, setConfigValue: setConfigValue2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8371
- const { createInterface } = await import('readline');
8372
- let provider = "anthropic";
8373
- if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
8374
- spinner?.stop();
8375
- console.log(chalk5.yellow("\nAI verification requires an API key."));
8376
- console.log(chalk5.gray("Get one at: https://console.anthropic.com/settings/keys"));
8377
- console.log(chalk5.gray("Or: https://platform.openai.com/api-keys\n"));
8378
- const rl = createInterface({ input: process.stdin, output: process.stdout });
8379
- const askQuestion = (question) => {
8380
- return new Promise((resolve6) => {
8381
- rl.question(question, (answer) => resolve6(answer.trim()));
8382
- });
8383
- };
8384
- const apiKey = await askQuestion(chalk5.cyan("Enter your Anthropic or OpenAI API key: "));
8385
- rl.close();
8386
- if (!apiKey) {
8387
- console.log(chalk5.red("No API key provided. Skipping AI verification."));
8388
- } else {
8389
- if (apiKey.startsWith("sk-ant-")) {
8390
- setConfigValue2("anthropicApiKey", apiKey);
8391
- provider = "anthropic";
8392
- console.log(chalk5.green("Anthropic API key saved to ~/.pinata/config.json\n"));
8393
- } else {
8394
- setConfigValue2("openaiApiKey", apiKey);
8395
- provider = "openai";
8396
- console.log(chalk5.green("OpenAI API key saved to ~/.pinata/config.json\n"));
8397
- }
8439
+ const domainsStr = options["domains"];
8440
+ let domains = [];
8441
+ if (domainsStr) {
8442
+ const domainList = domainsStr.split(",").map((d) => d.trim());
8443
+ for (const domain of domainList) {
8444
+ if (!RISK_DOMAINS.includes(domain)) {
8445
+ console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
8446
+ process.exit(1);
8398
8447
  }
8399
- } else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
8400
- provider = "openai";
8401
8448
  }
8402
- if (!hasApiKey2(provider)) {
8403
- } else {
8404
- const verifySpinner = showSpinner ? ora("Verifying gaps with AI...").start() : null;
8405
- try {
8406
- const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
8407
- const { readFile: readFile6 } = await import('fs/promises');
8408
- const apiKey = getApiKey2(provider);
8409
- const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
8410
- const { verified, dismissed, stats } = await verifier.verifyAll(
8411
- scanResult.data.gaps,
8412
- async (path2) => readFile6(path2, "utf-8")
8413
- );
8414
- scanResult.data.gaps = verified;
8415
- const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
8416
- let deduction = 0;
8417
- for (const gap of verified) {
8418
- deduction += severityWeights[gap.severity] ?? 1;
8449
+ domains = domainList;
8450
+ }
8451
+ const excludeStr = options["exclude"];
8452
+ const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
8453
+ const failOn = options["failOn"];
8454
+ if (failOn && !["critical", "high", "medium"].includes(failOn)) {
8455
+ console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
8456
+ process.exit(1);
8457
+ }
8458
+ const showSpinner = outputFormat === "terminal" && !isQuiet;
8459
+ const spinner = showSpinner ? ora3("Loading categories...").start() : null;
8460
+ try {
8461
+ const store = createCategoryStore();
8462
+ const definitionsPath = getDefinitionsPath();
8463
+ logger.debug(`Loading categories from: ${definitionsPath}`);
8464
+ const loadResult = await store.loadFromDirectory(definitionsPath);
8465
+ if (!loadResult.success) {
8466
+ spinner?.fail("Failed to load categories");
8467
+ console.error(formatError(loadResult.error));
8468
+ process.exit(1);
8469
+ }
8470
+ if (spinner) {
8471
+ spinner.text = `Loaded ${loadResult.data} categories. Scanning...`;
8472
+ }
8473
+ const scanner = createScanner(store);
8474
+ const scanOptions = {
8475
+ minSeverity,
8476
+ minConfidence,
8477
+ detectTestFiles: true
8478
+ };
8479
+ if (domains.length > 0) {
8480
+ scanOptions.domains = domains;
8481
+ }
8482
+ if (excludeDirs) {
8483
+ scanOptions.excludeDirs = excludeDirs;
8484
+ }
8485
+ const scanResult = await scanner.scanDirectory(targetDirectory, scanOptions);
8486
+ if (!scanResult.success) {
8487
+ spinner?.fail("Scan failed");
8488
+ console.error(formatError(scanResult.error));
8489
+ process.exit(1);
8490
+ }
8491
+ spinner?.stop();
8492
+ const shouldVerify = Boolean(options["verify"]);
8493
+ if (shouldVerify && scanResult.data.gaps.length > 0) {
8494
+ const { hasApiKey: hasApiKey2, setConfigValue: setConfigValue2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8495
+ const { createInterface } = await import('readline');
8496
+ let provider = "anthropic";
8497
+ if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
8498
+ spinner?.stop();
8499
+ console.log(chalk7.yellow("\nAI verification requires an API key."));
8500
+ console.log(chalk7.gray("Get one at: https://console.anthropic.com/settings/keys"));
8501
+ console.log(chalk7.gray("Or: https://platform.openai.com/api-keys\n"));
8502
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8503
+ const askQuestion = (question) => new Promise((resolve9) => rl.question(question, (answer) => resolve9(answer.trim())));
8504
+ const apiKey = await askQuestion(chalk7.cyan("Enter your Anthropic or OpenAI API key: "));
8505
+ rl.close();
8506
+ if (!apiKey) {
8507
+ console.log(chalk7.red("No API key provided. Skipping AI verification."));
8508
+ } else {
8509
+ if (apiKey.startsWith("sk-ant-")) {
8510
+ setConfigValue2("anthropicApiKey", apiKey);
8511
+ provider = "anthropic";
8512
+ console.log(chalk7.green("Anthropic API key saved to ~/.pinata/config.json\n"));
8513
+ } else {
8514
+ setConfigValue2("openaiApiKey", apiKey);
8515
+ provider = "openai";
8516
+ console.log(chalk7.green("OpenAI API key saved to ~/.pinata/config.json\n"));
8517
+ }
8419
8518
  }
8420
- const newOverall = Math.max(0, 100 - deduction);
8421
- const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
8422
- scanResult.data.score.overall = newOverall;
8423
- scanResult.data.score.grade = newGrade;
8424
- verifySpinner?.succeed(
8425
- `AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
8426
- );
8427
- if (isVerbose && dismissed.length > 0) {
8428
- console.log(chalk5.gray("\nDismissed as false positives:"));
8429
- for (const { gap, reason } of dismissed.slice(0, 5)) {
8430
- console.log(chalk5.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
8431
- console.log(chalk5.gray(` Reason: ${reason.slice(0, 100)}...`));
8519
+ } else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
8520
+ provider = "openai";
8521
+ }
8522
+ if (hasApiKey2(provider)) {
8523
+ const verifySpinner = showSpinner ? ora3("Verifying gaps with AI...").start() : null;
8524
+ try {
8525
+ const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
8526
+ const { readFile: readFile7 } = await import('fs/promises');
8527
+ const apiKey = getApiKey2(provider);
8528
+ const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
8529
+ const { verified, dismissed, stats } = await verifier.verifyAll(
8530
+ scanResult.data.gaps,
8531
+ async (path2) => readFile7(path2, "utf-8")
8532
+ );
8533
+ scanResult.data.gaps = verified;
8534
+ const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
8535
+ let deduction = 0;
8536
+ for (const gap of verified) {
8537
+ deduction += severityWeights[gap.severity] ?? 1;
8538
+ }
8539
+ const newOverall = Math.max(0, 100 - deduction);
8540
+ const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
8541
+ scanResult.data.score.overall = newOverall;
8542
+ scanResult.data.score.grade = newGrade;
8543
+ verifySpinner?.succeed(
8544
+ `AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
8545
+ );
8546
+ if (isVerbose && dismissed.length > 0) {
8547
+ console.log(chalk7.gray("\nDismissed as false positives:"));
8548
+ for (const { gap, reason } of dismissed.slice(0, 5)) {
8549
+ console.log(chalk7.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
8550
+ console.log(chalk7.gray(` Reason: ${reason.slice(0, 100)}...`));
8551
+ }
8552
+ if (dismissed.length > 5) {
8553
+ console.log(chalk7.gray(` ... and ${dismissed.length - 5} more`));
8554
+ }
8432
8555
  }
8433
- if (dismissed.length > 5) {
8434
- console.log(chalk5.gray(` ... and ${dismissed.length - 5} more`));
8556
+ } catch (error) {
8557
+ verifySpinner?.fail("AI verification failed (results unverified)");
8558
+ if (isVerbose) {
8559
+ console.error(chalk7.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
8435
8560
  }
8436
8561
  }
8437
- } catch (error) {
8438
- verifySpinner?.fail("AI verification failed (results unverified)");
8439
- if (isVerbose) {
8440
- console.error(chalk5.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
8441
- }
8442
8562
  }
8443
8563
  }
8444
- }
8445
- const shouldExecute = Boolean(options["execute"]);
8446
- const isDryRun = Boolean(options["dryRun"]);
8447
- if (shouldExecute && scanResult.data.gaps.length > 0) {
8448
- const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
8449
- const { readFile: readFile6 } = await import('fs/promises');
8450
- const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
8451
- if (testableGaps.length === 0) {
8452
- console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
8453
- console.log(chalk5.gray("Testable types: sql-injection, xss, command-injection, path-traversal"));
8454
- } else {
8455
- const runner = createRunner2(void 0, isDryRun);
8456
- const initResult = await runner.initialize();
8457
- if (!initResult.ready) {
8458
- console.log(chalk5.red(`
8459
- Dynamic execution unavailable: ${initResult.error}`));
8564
+ const shouldExecute = Boolean(options["execute"]);
8565
+ const isDryRun = Boolean(options["dryRun"]);
8566
+ if (shouldExecute && scanResult.data.gaps.length > 0) {
8567
+ const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
8568
+ const { readFile: readFile7 } = await import('fs/promises');
8569
+ const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
8570
+ if (testableGaps.length === 0) {
8571
+ console.log(chalk7.yellow("\nNo dynamically testable gaps found."));
8460
8572
  } else {
8461
- const fileContents = /* @__PURE__ */ new Map();
8462
- for (const gap of testableGaps) {
8463
- if (!fileContents.has(gap.filePath)) {
8464
- try {
8465
- fileContents.set(gap.filePath, await readFile6(gap.filePath, "utf-8"));
8466
- } catch {
8573
+ const runner = createRunner2(void 0, isDryRun);
8574
+ const initResult = await runner.initialize();
8575
+ if (!initResult.ready) {
8576
+ console.log(chalk7.red(`
8577
+ Dynamic execution unavailable: ${initResult.error}`));
8578
+ } else {
8579
+ const fileContents = /* @__PURE__ */ new Map();
8580
+ for (const gap of testableGaps) {
8581
+ if (!fileContents.has(gap.filePath)) {
8582
+ try {
8583
+ fileContents.set(gap.filePath, await readFile7(gap.filePath, "utf-8"));
8584
+ } catch {
8585
+ }
8467
8586
  }
8468
8587
  }
8469
- }
8470
- const executionSummary = await runner.executeAll(testableGaps, fileContents);
8471
- for (const result of executionSummary.results) {
8472
- const gap = scanResult.data.gaps.find(
8473
- (g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
8474
- );
8475
- if (gap && result.status === "confirmed") {
8476
- gap.confirmed = true;
8477
- gap.evidence = result.evidence;
8588
+ const executionSummary = await runner.executeAll(testableGaps, fileContents);
8589
+ for (const result of executionSummary.results) {
8590
+ const gap = scanResult.data.gaps.find(
8591
+ (g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
8592
+ );
8593
+ if (gap && result.status === "confirmed") {
8594
+ gap.confirmed = true;
8595
+ gap.evidence = result.evidence;
8596
+ }
8478
8597
  }
8479
- }
8480
- if (executionSummary.confirmed > 0) {
8481
- console.log(chalk5.red.bold(`
8598
+ if (executionSummary.confirmed > 0) {
8599
+ console.log(chalk7.red.bold(`
8482
8600
  \u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
8601
+ }
8483
8602
  }
8484
8603
  }
8485
8604
  }
8605
+ const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
8606
+ if (!cacheResult.success) {
8607
+ logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
8608
+ }
8609
+ const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
8610
+ const outputFile = options["outputFile"];
8611
+ if (outputFile) {
8612
+ writeFileSync(resolve(outputFile), output, "utf-8");
8613
+ logger.info(`Results written to: ${resolve(outputFile)}`);
8614
+ } else {
8615
+ console.log(output);
8616
+ }
8617
+ if (isVerbose && scanResult.data.warnings.length > 0) {
8618
+ console.error("\nWarnings:");
8619
+ for (const warning of scanResult.data.warnings) {
8620
+ console.error(` - ${warning}`);
8621
+ }
8622
+ }
8623
+ if (failOn) {
8624
+ const severityOrder = { critical: 3, high: 2, medium: 1 };
8625
+ const failLevel = severityOrder[failOn] ?? 0;
8626
+ const hasFailingGaps = scanResult.data.gaps.some((gap) => (severityOrder[gap.severity] ?? 0) >= failLevel);
8627
+ if (hasFailingGaps) {
8628
+ process.exit(1);
8629
+ }
8630
+ }
8631
+ process.exit(0);
8632
+ } catch (error) {
8633
+ spinner?.fail("Analysis failed");
8634
+ console.error(formatError(error instanceof Error ? error : new Error(String(error))));
8635
+ process.exit(1);
8486
8636
  }
8487
- const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
8488
- if (!cacheResult.success) {
8489
- logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
8637
+ });
8638
+ }
8639
+
8640
+ // src/cli/generate-formatters.ts
8641
+ init_errors();
8642
+ init_result();
8643
+ function formatGeneratedTerminal(tests, basePath) {
8644
+ const lines = [];
8645
+ if (tests.length === 0) {
8646
+ lines.push(chalk7.yellow("No tests generated."));
8647
+ return lines.join("\n");
8648
+ }
8649
+ lines.push(chalk7.bold.cyan(`
8650
+ Generated ${tests.length} test(s):
8651
+ `));
8652
+ lines.push(chalk7.gray("\u2500".repeat(60)));
8653
+ for (const test of tests) {
8654
+ const relGapPath = relative(basePath, test.gap.filePath);
8655
+ lines.push("");
8656
+ lines.push(chalk7.bold.white(`Test for: ${test.gap.categoryName}`));
8657
+ lines.push(chalk7.gray(` Gap location: ${relGapPath}:${test.gap.lineStart}`));
8658
+ lines.push(chalk7.gray(` Template: ${test.template.id}`));
8659
+ lines.push(chalk7.gray(` Output: ${test.suggestedPath}`));
8660
+ lines.push("");
8661
+ lines.push(chalk7.cyan(`// --- ${test.suggestedPath} ---`));
8662
+ lines.push("");
8663
+ if (test.result.imports.length > 0) {
8664
+ for (const imp of test.result.imports) {
8665
+ lines.push(chalk7.gray(imp));
8666
+ }
8667
+ lines.push("");
8490
8668
  }
8491
- const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
8492
- const outputFile = options["outputFile"];
8493
- if (outputFile) {
8494
- const outputPath = resolve(outputFile);
8495
- writeFileSync(outputPath, output, "utf-8");
8496
- logger.info(`Results written to: ${outputPath}`);
8669
+ lines.push(test.result.content);
8670
+ lines.push("");
8671
+ lines.push(chalk7.gray("\u2500".repeat(60)));
8672
+ }
8673
+ lines.push("");
8674
+ lines.push(chalk7.bold("Summary:"));
8675
+ lines.push(` Tests generated: ${chalk7.green(tests.length.toString())}`);
8676
+ const categories = new Set(tests.map((t) => t.category.id));
8677
+ lines.push(` Categories covered: ${chalk7.cyan(categories.size.toString())}`);
8678
+ if (tests.some((t) => t.result.unresolved.length > 0)) {
8679
+ const unresolvedCount = tests.reduce((acc, t) => acc + t.result.unresolved.length, 0);
8680
+ lines.push(chalk7.yellow(` Unresolved placeholders: ${unresolvedCount}`));
8681
+ lines.push(chalk7.gray(" (Some placeholders need manual completion)"));
8682
+ }
8683
+ lines.push("");
8684
+ lines.push(chalk7.gray("This is a dry run. Use --write to save files."));
8685
+ return lines.join("\n");
8686
+ }
8687
+ function formatGeneratedJson(tests) {
8688
+ const output = tests.map((test) => ({
8689
+ gap: {
8690
+ categoryId: test.gap.categoryId,
8691
+ categoryName: test.gap.categoryName,
8692
+ filePath: test.gap.filePath,
8693
+ lineStart: test.gap.lineStart,
8694
+ severity: test.gap.severity,
8695
+ confidence: test.gap.confidence
8696
+ },
8697
+ template: {
8698
+ id: test.template.id,
8699
+ framework: test.template.framework,
8700
+ language: test.template.language
8701
+ },
8702
+ suggestedPath: test.suggestedPath,
8703
+ content: test.result.content,
8704
+ imports: test.result.imports,
8705
+ fixtures: test.result.fixtures,
8706
+ substituted: test.result.substituted,
8707
+ unresolved: test.result.unresolved
8708
+ }));
8709
+ return JSON.stringify(output, null, 2);
8710
+ }
8711
+ function suggestTestPath(sourceFile, template, basePath) {
8712
+ const dir = dirname(sourceFile);
8713
+ const name = basename(sourceFile);
8714
+ const nameWithoutExt = name.replace(/\.[^.]+$/, "");
8715
+ const extMap = {
8716
+ python: ".py",
8717
+ typescript: ".ts",
8718
+ javascript: ".js",
8719
+ go: "_test.go",
8720
+ java: "Test.java",
8721
+ rust: ".rs"
8722
+ };
8723
+ const ext = extMap[template.language] ?? ".test.ts";
8724
+ let testFileName;
8725
+ switch (template.language) {
8726
+ case "python":
8727
+ testFileName = `test_${nameWithoutExt}${ext}`;
8728
+ break;
8729
+ case "go":
8730
+ testFileName = `${nameWithoutExt}${ext}`;
8731
+ break;
8732
+ case "java":
8733
+ testFileName = `${nameWithoutExt}${ext}`;
8734
+ break;
8735
+ default:
8736
+ testFileName = `${nameWithoutExt}.test${ext}`;
8737
+ }
8738
+ const relativeSrc = relative(basePath, dir);
8739
+ const testDir = relativeSrc.replace(/^src/, "tests");
8740
+ return `${testDir}/${testFileName}`;
8741
+ }
8742
+ function extractVariablesFromGap(gap) {
8743
+ const fileName = basename(gap.filePath);
8744
+ const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
8745
+ const funcMatch = gap.codeSnippet.match(/(?:def|function|async function|const|let|var)\s+(\w+)/);
8746
+ const classMatch = gap.codeSnippet.match(/(?:class)\s+(\w+)/);
8747
+ return {
8748
+ // File info
8749
+ filePath: gap.filePath,
8750
+ fileName,
8751
+ fileNameWithoutExt,
8752
+ lineNumber: gap.lineStart,
8753
+ // Category info
8754
+ categoryId: gap.categoryId,
8755
+ categoryName: gap.categoryName,
8756
+ domain: gap.domain,
8757
+ level: gap.level,
8758
+ severity: gap.severity,
8759
+ confidence: gap.confidence,
8760
+ // Code context
8761
+ codeSnippet: gap.codeSnippet,
8762
+ functionName: funcMatch?.[1] ?? "targetFunction",
8763
+ className: classMatch?.[1] ?? "TargetClass",
8764
+ // Common template variables
8765
+ testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
8766
+ testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
8767
+ // Pattern info
8768
+ patternId: gap.patternId,
8769
+ patternType: gap.patternType
8770
+ };
8771
+ }
8772
+ async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
8773
+ const baseVars = extractVariablesFromGap(gap);
8774
+ const result = await suggestVariables(
8775
+ {
8776
+ codeSnippet: gap.codeSnippet,
8777
+ filePath: gap.filePath,
8778
+ variables: templateVariables,
8779
+ gap,
8780
+ existingValues: baseVars
8781
+ },
8782
+ aiConfig
8783
+ );
8784
+ if (result.success && result.data) {
8785
+ return result.data.values;
8786
+ }
8787
+ return baseVars;
8788
+ }
8789
+ async function writeGeneratedTests(tests, basePath, outputDir) {
8790
+ const summary = {
8791
+ created: [],
8792
+ updated: [],
8793
+ failed: [],
8794
+ totalTests: 0
8795
+ };
8796
+ const testsByFile = /* @__PURE__ */ new Map();
8797
+ for (const test of tests) {
8798
+ let outputPath;
8799
+ if (outputDir) {
8800
+ outputPath = resolve(basePath, outputDir, test.suggestedPath);
8497
8801
  } else {
8498
- console.log(output);
8802
+ outputPath = resolve(basePath, test.suggestedPath);
8499
8803
  }
8500
- if (isVerbose && scanResult.data.warnings.length > 0) {
8501
- console.error("\nWarnings:");
8502
- for (const warning of scanResult.data.warnings) {
8503
- console.error(` - ${warning}`);
8804
+ const existing = testsByFile.get(outputPath) ?? [];
8805
+ existing.push(test);
8806
+ testsByFile.set(outputPath, existing);
8807
+ }
8808
+ for (const [outputPath, fileTests] of testsByFile) {
8809
+ try {
8810
+ const dir = dirname(outputPath);
8811
+ await mkdir(dir, { recursive: true });
8812
+ let existingContent = "";
8813
+ let fileExists = false;
8814
+ try {
8815
+ existingContent = await readFile(outputPath, "utf-8");
8816
+ fileExists = true;
8817
+ } catch {
8818
+ }
8819
+ const contentParts = [];
8820
+ const allImports = /* @__PURE__ */ new Set();
8821
+ for (const test of fileTests) {
8822
+ for (const imp of test.result.imports) {
8823
+ allImports.add(imp);
8824
+ }
8825
+ }
8826
+ if (!fileExists && allImports.size > 0) {
8827
+ contentParts.push(Array.from(allImports).join("\n"));
8828
+ contentParts.push("");
8829
+ }
8830
+ for (const test of fileTests) {
8831
+ contentParts.push(`// Test for ${test.gap.categoryName}`);
8832
+ contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
8833
+ contentParts.push(`// Generated by Pinata`);
8834
+ contentParts.push("");
8835
+ contentParts.push(test.result.content);
8836
+ contentParts.push("");
8837
+ }
8838
+ const newContent = contentParts.join("\n");
8839
+ let finalContent;
8840
+ let appended = false;
8841
+ if (fileExists) {
8842
+ finalContent = existingContent.trimEnd() + "\n\n" + newContent;
8843
+ appended = true;
8844
+ } else {
8845
+ finalContent = newContent;
8846
+ }
8847
+ await writeFile(outputPath, finalContent, "utf-8");
8848
+ for (const test of fileTests) {
8849
+ const result = {
8850
+ path: outputPath,
8851
+ created: !fileExists,
8852
+ appended,
8853
+ categoryId: test.gap.categoryId,
8854
+ gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
8855
+ };
8856
+ if (fileExists) {
8857
+ summary.updated.push(result);
8858
+ } else {
8859
+ summary.created.push(result);
8860
+ }
8861
+ summary.totalTests++;
8504
8862
  }
8505
- }
8506
- if (failOn) {
8507
- const severityOrder = {
8508
- critical: 3,
8509
- high: 2,
8510
- medium: 1
8511
- };
8512
- const failLevel = severityOrder[failOn] ?? 0;
8513
- const hasFailingGaps = scanResult.data.gaps.some((gap) => {
8514
- const gapLevel = severityOrder[gap.severity] ?? 0;
8515
- return gapLevel >= failLevel;
8863
+ } catch (error) {
8864
+ summary.failed.push({
8865
+ path: outputPath,
8866
+ error: error instanceof Error ? error.message : String(error)
8516
8867
  });
8517
- if (hasFailingGaps) {
8518
- const count = scanResult.data.gaps.filter((gap) => {
8519
- const gapLevel = severityOrder[gap.severity] ?? 0;
8520
- return gapLevel >= failLevel;
8521
- }).length;
8522
- logger.debug(`Exiting with code 1 due to ${count} gaps at ${failOn} level or above`);
8523
- process.exit(1);
8524
- }
8525
8868
  }
8526
- process.exit(0);
8527
- } catch (error) {
8528
- spinner?.fail("Analysis failed");
8529
- console.error(formatError(error instanceof Error ? error : new Error(String(error))));
8530
- process.exit(1);
8531
- }
8532
- });
8533
- 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) => {
8534
- const isQuiet = Boolean(options["quiet"]);
8535
- const isVerbose = Boolean(options["verbose"]);
8536
- const dryRun = !options["write"];
8537
- const useAI = Boolean(options["ai"]);
8538
- const aiProvider = String(options["aiProvider"] ?? "anthropic");
8539
- const outputFormat = String(options["output"] ?? "terminal");
8540
- if (isQuiet) {
8541
- logger.configure({ level: "error" });
8542
- } else if (isVerbose) {
8543
- logger.configure({ level: "debug" });
8544
8869
  }
8545
- if (!["terminal", "json"].includes(outputFormat)) {
8546
- console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
8547
- process.exit(1);
8870
+ return ok(summary);
8871
+ }
8872
+ function formatWriteSummary(summary, basePath) {
8873
+ const lines = [];
8874
+ lines.push("");
8875
+ lines.push(chalk7.bold.cyan("Write Summary:"));
8876
+ lines.push(chalk7.gray("\u2500".repeat(60)));
8877
+ if (summary.created.length > 0) {
8878
+ const uniquePaths = new Set(summary.created.map((r) => r.path));
8879
+ lines.push("");
8880
+ lines.push(chalk7.green.bold(`Created ${uniquePaths.size} file(s):`));
8881
+ for (const path2 of uniquePaths) {
8882
+ const relPath = relative(basePath, path2);
8883
+ const testsInFile = summary.created.filter((r) => r.path === path2).length;
8884
+ lines.push(chalk7.green(` + ${relPath} (${testsInFile} test(s))`));
8885
+ }
8548
8886
  }
8549
- const hasGaps = Boolean(options["gaps"]);
8550
- const categoryId = options["category"];
8551
- const domainFilter = options["domain"];
8552
- if (!hasGaps && !categoryId && !domainFilter) {
8553
- console.error(formatError(new Error(
8554
- "Specify what to generate: --gaps (all gaps), --category <id>, or --domain <domain>"
8555
- )));
8556
- process.exit(1);
8887
+ if (summary.updated.length > 0) {
8888
+ const uniquePaths = new Set(summary.updated.map((r) => r.path));
8889
+ lines.push("");
8890
+ lines.push(chalk7.yellow.bold(`Updated ${uniquePaths.size} file(s):`));
8891
+ for (const path2 of uniquePaths) {
8892
+ const relPath = relative(basePath, path2);
8893
+ const testsInFile = summary.updated.filter((r) => r.path === path2).length;
8894
+ lines.push(chalk7.yellow(` ~ ${relPath} (${testsInFile} test(s) appended)`));
8895
+ }
8557
8896
  }
8558
- if (domainFilter && !RISK_DOMAINS.includes(domainFilter)) {
8559
- console.error(formatError(new Error(
8560
- `Invalid domain: ${domainFilter}. Valid domains: ${RISK_DOMAINS.join(", ")}`
8561
- )));
8562
- process.exit(1);
8897
+ if (summary.failed.length > 0) {
8898
+ lines.push("");
8899
+ lines.push(chalk7.red.bold(`Failed to write ${summary.failed.length} file(s):`));
8900
+ for (const fail of summary.failed) {
8901
+ const relPath = relative(basePath, fail.path);
8902
+ lines.push(chalk7.red(` \u2717 ${relPath}: ${fail.error}`));
8903
+ }
8563
8904
  }
8564
- const validSeverities = ["critical", "high", "medium", "low"];
8565
- const minSeverity = String(options["severity"] ?? "medium");
8566
- if (!validSeverities.includes(minSeverity)) {
8567
- console.error(formatError(new Error(
8568
- `Invalid severity: ${minSeverity}. Use: critical, high, medium, low`
8569
- )));
8570
- process.exit(1);
8905
+ lines.push("");
8906
+ lines.push(chalk7.gray("\u2500".repeat(60)));
8907
+ lines.push(chalk7.bold(`Total: ${summary.totalTests} test(s) written to ${(/* @__PURE__ */ new Set([...summary.created.map((r) => r.path), ...summary.updated.map((r) => r.path)])).size} file(s)`));
8908
+ if (summary.failed.length > 0) {
8909
+ lines.push(chalk7.red(`Failures: ${summary.failed.length}`));
8571
8910
  }
8572
- const severityOrder = {
8573
- critical: 4,
8574
- high: 3,
8575
- medium: 2,
8576
- low: 1
8577
- };
8578
- const showSpinner = outputFormat === "terminal" && !isQuiet;
8579
- const spinner = showSpinner ? ora("Loading cached scan results...").start() : null;
8580
- try {
8581
- const projectRoot = process.cwd();
8582
- const cacheResult = await loadScanResults(projectRoot);
8583
- if (!cacheResult.success) {
8584
- spinner?.fail("No cached results");
8585
- console.error(formatError(cacheResult.error));
8586
- console.error(chalk5.yellow("\nRun `pinata analyze` first to scan for gaps."));
8587
- process.exit(1);
8588
- }
8589
- const cached = cacheResult.data;
8590
- let gaps = cached.gaps;
8591
- if (spinner) {
8592
- spinner.text = `Loaded ${gaps.length} gaps from cache. Filtering...`;
8593
- }
8594
- if (categoryId) {
8595
- gaps = gaps.filter((g) => g.categoryId === categoryId);
8596
- }
8597
- if (domainFilter) {
8598
- gaps = gaps.filter((g) => g.domain === domainFilter);
8599
- }
8600
- gaps = gaps.filter((g) => {
8601
- const gapLevel = severityOrder[g.severity] ?? 0;
8602
- const minLevel = severityOrder[minSeverity] ?? 0;
8603
- return gapLevel >= minLevel;
8604
- });
8605
- if (gaps.length === 0) {
8606
- spinner?.succeed("No gaps match the filters");
8607
- console.log(chalk5.yellow("\nNo gaps found matching the specified filters."));
8608
- process.exit(0);
8911
+ return lines.join("\n");
8912
+ }
8913
+
8914
+ // src/cli/commands/generate.ts
8915
+ init_results_cache();
8916
+ function registerGenerateCommand(program2) {
8917
+ program2.command("generate").description("Generate tests for identified gaps").option("--gaps", "Generate tests for all identified gaps").option("-c, --category <id>", "Generate tests for specific category").option("-d, --domain <domain>", "Generate tests for all categories in domain").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "medium").option("--output-dir <dir>", "Directory for generated test files").option("--write", "Write files to disk (default is dry-run)").option("--ai", "Use AI for smarter template variable filling").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json", "terminal").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
8918
+ const isQuiet = Boolean(options["quiet"]);
8919
+ const isVerbose = Boolean(options["verbose"]);
8920
+ const dryRun = !options["write"];
8921
+ const useAI = Boolean(options["ai"]);
8922
+ const aiProvider = String(options["aiProvider"] ?? "anthropic");
8923
+ const outputFormat = String(options["output"] ?? "terminal");
8924
+ if (isQuiet) {
8925
+ logger.configure({ level: "error" });
8926
+ } else if (isVerbose) {
8927
+ logger.configure({ level: "debug" });
8609
8928
  }
8610
- if (spinner) {
8611
- spinner.text = `Found ${gaps.length} gaps. Loading categories...`;
8929
+ if (!["terminal", "json"].includes(outputFormat)) {
8930
+ console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
8931
+ process.exit(1);
8612
8932
  }
8613
- const store = createCategoryStore();
8614
- const definitionsPath = getDefinitionsPath();
8615
- const loadResult = await store.loadFromDirectory(definitionsPath);
8616
- if (!loadResult.success) {
8617
- spinner?.fail("Failed to load categories");
8618
- console.error(formatError(loadResult.error));
8933
+ const hasGaps = Boolean(options["gaps"]);
8934
+ const categoryId = options["category"];
8935
+ const domainFilter = options["domain"];
8936
+ if (!hasGaps && !categoryId && !domainFilter) {
8937
+ console.error(formatError(new Error("Specify what to generate: --gaps (all gaps), --category <id>, or --domain <domain>")));
8619
8938
  process.exit(1);
8620
8939
  }
8621
- if (spinner) {
8622
- spinner.text = `Generating tests for ${gaps.length} gaps...`;
8940
+ if (domainFilter && !RISK_DOMAINS.includes(domainFilter)) {
8941
+ console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
8942
+ process.exit(1);
8623
8943
  }
8624
- const renderer = createRenderer({ strict: false, allowUnresolved: true });
8625
- const generatedTests = [];
8626
- const errors = [];
8627
- const gapsByCategory = /* @__PURE__ */ new Map();
8628
- for (const gap of gaps) {
8629
- const existing = gapsByCategory.get(gap.categoryId) ?? [];
8630
- existing.push(gap);
8631
- gapsByCategory.set(gap.categoryId, existing);
8944
+ const validSeverities = ["critical", "high", "medium", "low"];
8945
+ const minSeverity = String(options["severity"] ?? "medium");
8946
+ if (!validSeverities.includes(minSeverity)) {
8947
+ console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
8948
+ process.exit(1);
8632
8949
  }
8633
- for (const [catId, categoryGaps] of gapsByCategory) {
8634
- const categoryResult = store.get(catId);
8635
- if (!categoryResult.success) {
8636
- errors.push(`Category not found: ${catId}`);
8637
- continue;
8950
+ const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
8951
+ const showSpinner = outputFormat === "terminal" && !isQuiet;
8952
+ const spinner = showSpinner ? ora3("Loading cached scan results...").start() : null;
8953
+ try {
8954
+ const projectRoot = process.cwd();
8955
+ const cacheResult = await loadScanResults(projectRoot);
8956
+ if (!cacheResult.success) {
8957
+ spinner?.fail("No cached results");
8958
+ console.error(formatError(cacheResult.error));
8959
+ console.error(chalk7.yellow("\nRun `pinata analyze` first to scan for gaps."));
8960
+ process.exit(1);
8638
8961
  }
8639
- const category = categoryResult.data;
8640
- for (const gap of categoryGaps) {
8641
- const gapExt = gap.filePath.split(".").pop() ?? "";
8642
- const langMap = {
8643
- py: "python",
8644
- ts: "typescript",
8645
- tsx: "typescript",
8646
- js: "javascript",
8647
- jsx: "javascript",
8648
- go: "go",
8649
- java: "java",
8650
- rs: "rust"
8651
- };
8652
- const gapLang = langMap[gapExt];
8653
- let template = category.testTemplates.find((t) => t.language === gapLang);
8654
- if (!template) {
8655
- template = category.testTemplates[0];
8656
- }
8657
- if (!template) {
8658
- errors.push(`No templates available for ${catId}`);
8962
+ const cached = cacheResult.data;
8963
+ let gaps = cached.gaps;
8964
+ if (spinner) {
8965
+ spinner.text = `Loaded ${gaps.length} gaps from cache. Filtering...`;
8966
+ }
8967
+ if (categoryId) {
8968
+ gaps = gaps.filter((g) => g.categoryId === categoryId);
8969
+ }
8970
+ if (domainFilter) {
8971
+ gaps = gaps.filter((g) => g.domain === domainFilter);
8972
+ }
8973
+ gaps = gaps.filter((g) => (severityOrder[g.severity] ?? 0) >= (severityOrder[minSeverity] ?? 0));
8974
+ if (gaps.length === 0) {
8975
+ spinner?.succeed("No gaps match the filters");
8976
+ console.log(chalk7.yellow("\nNo gaps found matching the specified filters."));
8977
+ process.exit(0);
8978
+ }
8979
+ if (spinner) {
8980
+ spinner.text = `Found ${gaps.length} gaps. Loading categories...`;
8981
+ }
8982
+ const store = createCategoryStore();
8983
+ const definitionsPath = getDefinitionsPath();
8984
+ const loadResult = await store.loadFromDirectory(definitionsPath);
8985
+ if (!loadResult.success) {
8986
+ spinner?.fail("Failed to load categories");
8987
+ console.error(formatError(loadResult.error));
8988
+ process.exit(1);
8989
+ }
8990
+ if (spinner) {
8991
+ spinner.text = `Generating tests for ${gaps.length} gaps...`;
8992
+ }
8993
+ const renderer = createRenderer({ strict: false, allowUnresolved: true });
8994
+ const generatedTests = [];
8995
+ const errors = [];
8996
+ const gapsByCategory = /* @__PURE__ */ new Map();
8997
+ for (const gap of gaps) {
8998
+ const existing = gapsByCategory.get(gap.categoryId) ?? [];
8999
+ existing.push(gap);
9000
+ gapsByCategory.set(gap.categoryId, existing);
9001
+ }
9002
+ for (const [catId, categoryGaps] of gapsByCategory) {
9003
+ const categoryResult = store.get(catId);
9004
+ if (!categoryResult.success) {
9005
+ errors.push(`Category not found: ${catId}`);
8659
9006
  continue;
8660
9007
  }
8661
- let variables;
8662
- if (useAI) {
8663
- variables = await extractVariablesWithAI(gap, template.variables, {
8664
- provider: aiProvider
8665
- });
8666
- } else {
8667
- variables = extractVariablesFromGap(gap);
8668
- }
8669
- const renderResult = renderer.renderTemplate(template, variables);
8670
- if (!renderResult.success) {
8671
- errors.push(`Failed to render ${catId}: ${renderResult.error.message}`);
8672
- continue;
9008
+ const category = categoryResult.data;
9009
+ for (const gap of categoryGaps) {
9010
+ const gapExt = gap.filePath.split(".").pop() ?? "";
9011
+ const langMap = { py: "python", ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", go: "go", java: "java", rs: "rust" };
9012
+ const gapLang = langMap[gapExt];
9013
+ let template = category.testTemplates.find((t) => t.language === gapLang);
9014
+ if (!template) {
9015
+ template = category.testTemplates[0];
9016
+ }
9017
+ if (!template) {
9018
+ errors.push(`No templates available for ${catId}`);
9019
+ continue;
9020
+ }
9021
+ let variables;
9022
+ if (useAI) {
9023
+ variables = await extractVariablesWithAI(gap, template.variables, { provider: aiProvider });
9024
+ } else {
9025
+ variables = extractVariablesFromGap(gap);
9026
+ }
9027
+ const renderResult = renderer.renderTemplate(template, variables);
9028
+ if (!renderResult.success) {
9029
+ errors.push(`Failed to render ${catId}: ${renderResult.error.message}`);
9030
+ continue;
9031
+ }
9032
+ const suggestedPath = suggestTestPath(gap.filePath, template, cached.targetDirectory);
9033
+ generatedTests.push({ gap, category, template, result: renderResult.data, suggestedPath });
8673
9034
  }
8674
- const suggestedPath = suggestTestPath(gap.filePath, template, cached.targetDirectory);
8675
- generatedTests.push({
8676
- gap,
8677
- category,
8678
- template,
8679
- result: renderResult.data,
8680
- suggestedPath
8681
- });
8682
9035
  }
8683
- }
8684
- spinner?.stop();
8685
- if (outputFormat === "json") {
8686
- console.log(formatGeneratedJson(generatedTests));
8687
- } else {
8688
- console.log(formatGeneratedTerminal(generatedTests, cached.targetDirectory));
8689
- }
8690
- if (isVerbose && errors.length > 0) {
8691
- console.error(chalk5.yellow("\nWarnings:"));
8692
- for (const error of errors) {
8693
- console.error(chalk5.gray(` - ${error}`));
9036
+ spinner?.stop();
9037
+ if (outputFormat === "json") {
9038
+ console.log(formatGeneratedJson(generatedTests));
9039
+ } else {
9040
+ console.log(formatGeneratedTerminal(generatedTests, cached.targetDirectory));
8694
9041
  }
8695
- }
8696
- if (!dryRun) {
8697
- const outputDirOption = options["outputDir"];
8698
- const writeResult = await writeGeneratedTests(
8699
- generatedTests,
8700
- cached.targetDirectory,
8701
- outputDirOption
8702
- );
8703
- if (!writeResult.success) {
8704
- console.error(formatError(writeResult.error));
8705
- process.exit(1);
9042
+ if (isVerbose && errors.length > 0) {
9043
+ console.error(chalk7.yellow("\nWarnings:"));
9044
+ for (const error of errors) {
9045
+ console.error(chalk7.gray(` - ${error}`));
9046
+ }
8706
9047
  }
8707
- console.log(formatWriteSummary(writeResult.data, cached.targetDirectory));
8708
- if (writeResult.data.failed.length > 0) {
8709
- process.exit(1);
9048
+ if (!dryRun) {
9049
+ const outputDirOption = options["outputDir"];
9050
+ const writeResult = await writeGeneratedTests(generatedTests, cached.targetDirectory, outputDirOption);
9051
+ if (!writeResult.success) {
9052
+ console.error(formatError(writeResult.error));
9053
+ process.exit(1);
9054
+ }
9055
+ console.log(formatWriteSummary(writeResult.data, cached.targetDirectory));
9056
+ if (writeResult.data.failed.length > 0) {
9057
+ process.exit(1);
9058
+ }
8710
9059
  }
9060
+ process.exit(0);
9061
+ } catch (error) {
9062
+ spinner?.fail("Generation failed");
9063
+ console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9064
+ process.exit(1);
8711
9065
  }
8712
- process.exit(0);
8713
- } catch (error) {
8714
- spinner?.fail("Generation failed");
8715
- console.error(formatError(error instanceof Error ? error : new Error(String(error))));
8716
- process.exit(1);
8717
- }
8718
- });
9066
+ });
9067
+ }
9068
+
9069
+ // src/cli/index.ts
9070
+ var program = new Command();
9071
+ program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
9072
+ registerAnalyzeCommand(program);
9073
+ registerGenerateCommand(program);
8719
9074
  program.command("explain").description("Get natural language explanations for detected gaps").option("-n, --top <count>", "Explain top N gaps by priority", "5").option("-c, --category <id>", "Explain gaps for specific category").option("-d, --domain <domain>", "Explain gaps for specific domain").option("--ai", "Use AI for detailed explanations (requires API key)").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json, markdown", "terminal").option("-v, --verbose", "Show more details").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
8720
9075
  const isQuiet = Boolean(options["quiet"]);
8721
9076
  const isVerbose = Boolean(options["verbose"]);
9077
+ const topN = parseInt(String(options["top"] ?? "5"), 10);
9078
+ const categoryFilter = options["category"];
9079
+ const domainFilter = options["domain"];
8722
9080
  const useAI = Boolean(options["ai"]);
8723
9081
  const aiProvider = String(options["aiProvider"] ?? "anthropic");
8724
9082
  const outputFormat = String(options["output"] ?? "terminal");
8725
- const topN = parseInt(String(options["top"] ?? "5"), 10);
8726
9083
  if (isQuiet) {
8727
9084
  logger.configure({ level: "error" });
8728
9085
  } else if (isVerbose) {
8729
9086
  logger.configure({ level: "debug" });
8730
9087
  }
8731
9088
  if (!["terminal", "json", "markdown"].includes(outputFormat)) {
8732
- console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown`)));
9089
+ console.error(formatError(new Error(`Invalid format: ${outputFormat}. Use: terminal, json, markdown`)));
8733
9090
  process.exit(1);
8734
9091
  }
8735
- const showSpinner = outputFormat === "terminal" && !isQuiet;
8736
- const spinner = showSpinner ? ora("Loading cached scan results...").start() : null;
8737
- try {
8738
- const projectRoot = process.cwd();
8739
- const cacheResult = await loadScanResults(projectRoot);
8740
- if (!cacheResult.success) {
8741
- spinner?.fail("No cached results");
8742
- console.error(formatError(cacheResult.error));
8743
- console.error(chalk5.yellow("\nRun `pinata analyze` first to scan for gaps."));
9092
+ const projectRoot = process.cwd();
9093
+ const cacheResult = await loadScanResults(projectRoot);
9094
+ if (!cacheResult.success) {
9095
+ console.error(formatError(cacheResult.error));
9096
+ console.error(chalk7.yellow("\nRun `pinata analyze` first to scan for gaps."));
9097
+ process.exit(1);
9098
+ }
9099
+ const cached = cacheResult.data;
9100
+ let gaps = cached.gaps;
9101
+ if (categoryFilter) {
9102
+ gaps = gaps.filter((g) => g.categoryId === categoryFilter);
9103
+ }
9104
+ if (domainFilter) {
9105
+ if (!RISK_DOMAINS.includes(domainFilter)) {
9106
+ console.error(formatError(new Error(`Invalid domain: ${domainFilter}`)));
8744
9107
  process.exit(1);
8745
9108
  }
8746
- const cached = cacheResult.data;
8747
- let gaps = cached.gaps;
8748
- const categoryFilter = options["category"];
8749
- const domainFilter = options["domain"];
8750
- if (categoryFilter) {
8751
- gaps = gaps.filter((g) => g.categoryId === categoryFilter);
8752
- }
8753
- if (domainFilter) {
8754
- if (!RISK_DOMAINS.includes(domainFilter)) {
8755
- spinner?.fail("Invalid domain");
8756
- console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid: ${RISK_DOMAINS.join(", ")}`)));
8757
- process.exit(1);
9109
+ gaps = gaps.filter((g) => g.domain === domainFilter);
9110
+ }
9111
+ gaps = gaps.slice(0, topN);
9112
+ if (gaps.length === 0) {
9113
+ console.log(chalk7.yellow("No gaps to explain."));
9114
+ process.exit(0);
9115
+ }
9116
+ let explanations;
9117
+ if (useAI) {
9118
+ const spinner = ora3("Generating AI explanations...").start();
9119
+ try {
9120
+ const aiConfig = { provider: aiProvider };
9121
+ const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9122
+ if (hasApiKey2(aiProvider)) {
9123
+ const key = getApiKey2(aiProvider);
9124
+ if (key) {
9125
+ aiConfig.apiKey = key;
9126
+ }
8758
9127
  }
8759
- gaps = gaps.filter((g) => g.domain === domainFilter);
8760
- }
8761
- if (gaps.length === 0) {
8762
- spinner?.succeed("No gaps to explain");
8763
- console.log(chalk5.yellow("\nNo gaps found matching the filters."));
8764
- process.exit(0);
8765
- }
8766
- gaps = gaps.sort((a, b) => b.priorityScore - a.priorityScore).slice(0, topN);
8767
- if (spinner) {
8768
- spinner.text = `Explaining ${gaps.length} gap(s)...`;
8769
- }
8770
- const explanations = [];
8771
- if (useAI) {
8772
- const ai = createAIService({ provider: aiProvider });
8773
- if (!ai.isConfigured()) {
8774
- spinner?.warn("AI not configured, using fallback explanations");
8775
- console.error(chalk5.yellow(`
9128
+ const resultMap = await explainGaps(gaps, void 0, aiConfig);
9129
+ explanations = gaps.map((g) => {
9130
+ const key = `${g.categoryId}:${g.filePath}:${g.lineStart}`;
9131
+ const result = resultMap.get(key);
9132
+ if (result?.success && result.data) {
9133
+ return result.data;
9134
+ }
9135
+ return generateFallbackExplanation(g);
9136
+ });
9137
+ spinner.succeed(`Generated ${explanations.length} explanations`);
9138
+ } catch (error) {
9139
+ spinner.fail("AI explanation failed");
9140
+ console.error(chalk7.yellow(`
8776
9141
  Set ${aiProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} for AI explanations.
8777
9142
  `));
8778
- for (const gap of gaps) {
8779
- explanations.push({
8780
- gap,
8781
- explanation: generateFallbackExplanation(gap)
8782
- });
8783
- }
8784
- } else {
8785
- for (const gap of gaps) {
8786
- const result = await explainGap(gap, void 0, { provider: aiProvider });
8787
- if (result.success && result.data) {
8788
- explanations.push({ gap, explanation: result.data });
8789
- } else {
8790
- explanations.push({
8791
- gap,
8792
- explanation: generateFallbackExplanation(gap)
8793
- });
8794
- }
8795
- }
8796
- }
8797
- } else {
8798
- for (const gap of gaps) {
8799
- explanations.push({
8800
- gap,
8801
- explanation: generateFallbackExplanation(gap)
8802
- });
8803
- }
9143
+ explanations = gaps.map((g) => generateFallbackExplanation(g));
8804
9144
  }
8805
- spinner?.stop();
8806
- if (outputFormat === "json") {
8807
- console.log(JSON.stringify(explanations.map((e) => ({
8808
- gap: {
8809
- categoryId: e.gap.categoryId,
8810
- categoryName: e.gap.categoryName,
8811
- filePath: e.gap.filePath,
8812
- lineStart: e.gap.lineStart,
8813
- severity: e.gap.severity,
8814
- confidence: e.gap.confidence,
8815
- codeSnippet: e.gap.codeSnippet
8816
- },
8817
- explanation: e.explanation
8818
- })), null, 2));
8819
- } else if (outputFormat === "markdown") {
8820
- console.log(`# Gap Explanations
8821
- `);
8822
- console.log(`Generated ${explanations.length} explanation(s).
8823
- `);
8824
- for (const { gap, explanation } of explanations) {
8825
- console.log(`## ${gap.categoryName}
8826
- `);
8827
- console.log(`**File:** \`${gap.filePath}:${gap.lineStart}\`
8828
- `);
8829
- console.log(`**Severity:** ${gap.severity} | **Confidence:** ${gap.confidence}
8830
- `);
8831
- console.log(`### Summary
8832
- ${explanation.summary}
8833
- `);
8834
- console.log(`### Explanation
8835
- ${explanation.explanation}
8836
- `);
8837
- console.log(`### Risk
8838
- ${explanation.risk}
9145
+ } else {
9146
+ explanations = gaps.map((g) => generateFallbackExplanation(g));
9147
+ }
9148
+ if (outputFormat === "json") {
9149
+ const output = gaps.map((g, i) => ({ gap: { categoryId: g.categoryId, severity: g.severity, filePath: g.filePath, lineStart: g.lineStart }, ...explanations[i] }));
9150
+ console.log(JSON.stringify(output, null, 2));
9151
+ } else if (outputFormat === "markdown") {
9152
+ console.log("# Gap Explanations\n");
9153
+ for (let i = 0; i < explanations.length; i++) {
9154
+ const exp = explanations[i];
9155
+ const gap = gaps[i];
9156
+ console.log(`## ${exp.summary}
8839
9157
  `);
8840
- console.log(`### How to Fix
8841
- ${explanation.remediation}
9158
+ console.log(`**Severity**: ${gap.severity} | **Category**: ${gap.categoryId}
8842
9159
  `);
8843
- if (explanation.safeExample) {
8844
- console.log(`### Safe Example
8845
- \`\`\`
8846
- ${explanation.safeExample}
8847
- \`\`\`
9160
+ console.log(exp.explanation);
9161
+ if (exp.remediation) {
9162
+ console.log(`
9163
+ **Remediation**: ${exp.remediation}
8848
9164
  `);
8849
- }
8850
- console.log("---\n");
8851
9165
  }
8852
- } else {
9166
+ console.log("---\n");
9167
+ }
9168
+ } else {
9169
+ console.log();
9170
+ for (let i = 0; i < explanations.length; i++) {
9171
+ const exp = explanations[i];
9172
+ const gap = gaps[i];
9173
+ const severityColor = gap.severity === "critical" ? chalk7.red : gap.severity === "high" ? chalk7.yellow : chalk7.blue;
9174
+ console.log(`${severityColor.bold(`[${gap.severity.toUpperCase()}]`)} ${chalk7.bold(exp.summary)}`);
9175
+ console.log(chalk7.gray(` Category: ${gap.categoryId} | ${gap.filePath}:${gap.lineStart}`));
8853
9176
  console.log();
8854
- console.log(chalk5.bold.cyan("Gap Explanations"));
8855
- console.log(chalk5.gray("\u2500".repeat(60)));
8856
- for (const { gap, explanation } of explanations) {
8857
- console.log();
8858
- console.log(chalk5.bold.white(gap.categoryName));
8859
- console.log(chalk5.gray(` ${gap.filePath}:${gap.lineStart}`));
8860
- const severityColor = gap.severity === "critical" ? chalk5.red : gap.severity === "high" ? chalk5.yellow : chalk5.blue;
8861
- console.log(` ${severityColor(gap.severity)} | ${gap.confidence} confidence`);
8862
- console.log();
8863
- console.log(chalk5.cyan(" Summary:"));
8864
- console.log(` ${explanation.summary}`);
8865
- if (isVerbose) {
8866
- console.log();
8867
- console.log(chalk5.cyan(" Explanation:"));
8868
- for (const line of explanation.explanation.split("\n")) {
8869
- console.log(` ${line}`);
8870
- }
8871
- }
8872
- console.log();
8873
- console.log(chalk5.red(" Risk:"));
8874
- console.log(` ${explanation.risk}`);
8875
- console.log();
8876
- console.log(chalk5.green(" How to Fix:"));
8877
- for (const line of explanation.remediation.split("\n")) {
8878
- console.log(` ${line}`);
8879
- }
8880
- if (explanation.safeExample) {
8881
- console.log();
8882
- console.log(chalk5.cyan(" Safe Example:"));
8883
- console.log(chalk5.gray(` ${explanation.safeExample}`));
8884
- }
9177
+ for (const line of exp.explanation.split("\n")) {
9178
+ console.log(` ${line}`);
9179
+ }
9180
+ if (exp.remediation) {
8885
9181
  console.log();
8886
- console.log(chalk5.gray("\u2500".repeat(60)));
9182
+ console.log(chalk7.green(` Fix: ${exp.remediation}`));
8887
9183
  }
9184
+ console.log();
8888
9185
  }
8889
- process.exit(0);
8890
- } catch (error) {
8891
- spinner?.fail("Explanation failed");
8892
- console.error(formatError(error instanceof Error ? error : new Error(String(error))));
8893
- process.exit(1);
8894
9186
  }
8895
9187
  });
8896
9188
  program.command("suggest-patterns").description("Use AI to suggest new detection patterns based on code samples").requiredOption("-c, --category <id>", "Category to suggest patterns for").requiredOption("-l, --language <lang>", "Language of the code samples").option("-f, --file <path>", "File containing vulnerable code samples (one per line)").option("--code <snippet>", "Vulnerable code snippet (can be specified multiple times)", (v, a) => [...a, v], []).option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, yaml, json", "terminal").action(async (options) => {
@@ -8898,96 +9190,73 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
8898
9190
  const language = String(options["language"]);
8899
9191
  const aiProvider = String(options["aiProvider"] ?? "anthropic");
8900
9192
  const outputFormat = String(options["output"] ?? "terminal");
8901
- const codeSnippets = options["code"];
8902
9193
  const filePath = options["file"];
8903
- let vulnerableCode = [...codeSnippets];
9194
+ const codeSnippets = options["code"] ?? [];
9195
+ const samples = [...codeSnippets];
8904
9196
  if (filePath) {
9197
+ const { readFile: readFile7 } = await import('fs/promises');
8905
9198
  try {
8906
- const { readFile: readFile6 } = await import('fs/promises');
8907
- const content = await readFile6(filePath, "utf-8");
8908
- vulnerableCode = [...vulnerableCode, ...content.split("\n---\n").filter(Boolean)];
9199
+ const content = await readFile7(resolve(filePath), "utf-8");
9200
+ samples.push(...content.split("\n---\n").filter((s) => s.trim()));
8909
9201
  } catch (error) {
8910
9202
  console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
8911
9203
  process.exit(1);
8912
9204
  }
8913
9205
  }
8914
- if (vulnerableCode.length === 0) {
9206
+ if (samples.length === 0) {
8915
9207
  console.error(formatError(new Error("Provide code samples via --code or --file")));
8916
9208
  process.exit(1);
8917
9209
  }
8918
- const spinner = ora("Generating pattern suggestions...").start();
9210
+ const spinner = ora3("Generating pattern suggestions with AI...").start();
8919
9211
  try {
9212
+ const aiConfig = { provider: aiProvider };
9213
+ const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9214
+ if (hasApiKey2(aiProvider)) {
9215
+ const key = getApiKey2(aiProvider);
9216
+ if (key) {
9217
+ aiConfig.apiKey = key;
9218
+ }
9219
+ }
8920
9220
  const result = await suggestPatterns(
8921
- {
8922
- category: categoryId,
8923
- language,
8924
- vulnerableCode,
8925
- maxSuggestions: 5
8926
- },
8927
- { provider: aiProvider }
9221
+ { category: categoryId, vulnerableCode: samples, language },
9222
+ aiConfig
8928
9223
  );
8929
- spinner.stop();
8930
- if (!result.success) {
8931
- console.error(formatError(new Error(result.error ?? "Failed to generate patterns")));
9224
+ if (!result.success || !result.data) {
9225
+ spinner.fail("Pattern suggestion failed");
9226
+ console.error(chalk7.red(result.error ?? "Unknown error"));
8932
9227
  process.exit(1);
8933
9228
  }
8934
- const { suggestions, rejected } = result.data ?? { suggestions: [], rejected: [] };
9229
+ const suggestions = result.data.suggestions;
9230
+ spinner.succeed(`Generated ${suggestions.length} pattern suggestions`);
8935
9231
  if (outputFormat === "json") {
8936
- console.log(JSON.stringify({ suggestions, rejected }, null, 2));
9232
+ console.log(JSON.stringify(suggestions, null, 2));
8937
9233
  } else if (outputFormat === "yaml") {
8938
- console.log(`# Suggested patterns for ${categoryId}
8939
- `);
8940
- console.log(`detectionPatterns:`);
8941
- for (const suggestion of suggestions) {
8942
- const escapedPattern = suggestion.pattern.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
8943
- console.log(` - id: ${suggestion.id}`);
8944
- console.log(` type: regex`);
8945
- console.log(` language: ${language}`);
8946
- console.log(` pattern: "${escapedPattern}"`);
8947
- console.log(` confidence: ${suggestion.confidence}`);
8948
- console.log(` description: ${suggestion.description}`);
9234
+ for (const s of suggestions) {
9235
+ console.log("---");
9236
+ console.log(`id: ${s.id}`);
9237
+ console.log(`pattern: "${s.pattern}"`);
9238
+ console.log(`confidence: ${s.confidence}`);
9239
+ console.log(`description: "${s.description}"`);
9240
+ console.log(`matchExample: "${s.matchExample}"`);
9241
+ console.log(`safeExample: "${s.safeExample}"`);
8949
9242
  console.log();
8950
9243
  }
8951
9244
  } else {
8952
9245
  console.log();
8953
- console.log(chalk5.bold.cyan("Pattern Suggestions"));
8954
- console.log(chalk5.gray("\u2500".repeat(60)));
8955
- if (suggestions.length === 0) {
8956
- console.log(chalk5.yellow("\nNo valid patterns could be generated."));
8957
- } else {
8958
- for (const suggestion of suggestions) {
8959
- console.log();
8960
- console.log(chalk5.bold.white(suggestion.id));
8961
- console.log(chalk5.gray(` ${suggestion.description}`));
8962
- console.log();
8963
- console.log(chalk5.cyan(" Pattern:"));
8964
- console.log(` ${suggestion.pattern}`);
8965
- console.log();
8966
- console.log(chalk5.cyan(" Confidence:") + ` ${suggestion.confidence}`);
8967
- console.log();
8968
- console.log(chalk5.green(" Would match:"));
8969
- console.log(chalk5.gray(` ${suggestion.matchExample}`));
8970
- console.log();
8971
- console.log(chalk5.red(" Should NOT match:"));
8972
- console.log(chalk5.gray(` ${suggestion.safeExample}`));
8973
- console.log();
8974
- console.log(chalk5.cyan(" Reasoning:"));
8975
- console.log(` ${suggestion.reasoning}`);
8976
- console.log();
8977
- console.log(chalk5.gray("\u2500".repeat(60)));
8978
- }
8979
- }
8980
- if (rejected.length > 0) {
9246
+ console.log(chalk7.bold(`Suggested Patterns for ${categoryId}`));
9247
+ console.log();
9248
+ for (const s of suggestions) {
9249
+ const confColor = s.confidence === "high" ? chalk7.green : s.confidence === "medium" ? chalk7.yellow : chalk7.red;
9250
+ console.log(` ${chalk7.cyan(s.id)} [${confColor(s.confidence)}]`);
9251
+ console.log(` Pattern: ${chalk7.gray(s.pattern)}`);
9252
+ console.log(` ${s.description}`);
9253
+ console.log(` Match: ${chalk7.gray(s.matchExample.slice(0, 80))}`);
9254
+ console.log(` Safe: ${chalk7.gray(s.safeExample.slice(0, 80))}`);
8981
9255
  console.log();
8982
- console.log(chalk5.yellow.bold(`Rejected ${rejected.length} pattern(s):`));
8983
- for (const r of rejected) {
8984
- console.log(chalk5.gray(` - ${r.pattern.slice(0, 40)}... : ${r.reason}`));
8985
- }
8986
9256
  }
8987
9257
  }
8988
- process.exit(0);
8989
9258
  } catch (error) {
8990
- spinner.fail("Pattern generation failed");
9259
+ spinner.fail("Pattern suggestion failed");
8991
9260
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
8992
9261
  process.exit(1);
8993
9262
  }
@@ -9042,9 +9311,7 @@ program.command("search <query>").description("Search category taxonomy by name,
9042
9311
  }
9043
9312
  const langFilter = options["language"];
9044
9313
  if (langFilter) {
9045
- results = results.filter(
9046
- (cat) => cat.applicableLanguages.includes(langFilter)
9047
- );
9314
+ results = results.filter((cat) => cat.applicableLanguages.includes(langFilter));
9048
9315
  }
9049
9316
  if (outputFormat === "json") {
9050
9317
  console.log(JSON.stringify(results, null, 2));
@@ -9066,19 +9333,18 @@ ${cat.description}
9066
9333
  }
9067
9334
  } else {
9068
9335
  console.log();
9069
- console.log(chalk5.bold(`Search Results for "${query}"`));
9070
- console.log(chalk5.gray(`Found ${results.length} matching categories.`));
9336
+ console.log(chalk7.bold(`Search Results for "${query}"`));
9337
+ console.log(chalk7.gray(`Found ${results.length} matching categories.`));
9071
9338
  console.log();
9072
9339
  if (results.length === 0) {
9073
- console.log(chalk5.yellow("No categories match your search."));
9074
- console.log(chalk5.gray("Try a different query or broaden your filters."));
9340
+ console.log(chalk7.yellow("No categories match your search."));
9075
9341
  } else {
9076
9342
  for (const cat of results) {
9077
- const domainColor = cat.domain === "security" ? chalk5.red : chalk5.blue;
9078
- console.log(` ${chalk5.cyan(cat.id)} - ${chalk5.bold(cat.name)}`);
9343
+ const domainColor = cat.domain === "security" ? chalk7.red : chalk7.blue;
9344
+ console.log(` ${chalk7.cyan(cat.id)} - ${chalk7.bold(cat.name)}`);
9079
9345
  console.log(` ${domainColor(cat.domain)} | ${cat.level} | ${cat.priority}`);
9080
9346
  if (options["verbose"]) {
9081
- console.log(` ${chalk5.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
9347
+ console.log(` ${chalk7.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
9082
9348
  }
9083
9349
  console.log();
9084
9350
  }
@@ -9112,21 +9378,17 @@ program.command("list").description("List all categories").option("-d, --domain
9112
9378
  process.exit(1);
9113
9379
  }
9114
9380
  const priorityFilter = options["priority"];
9115
- const validPriorities = ["P0", "P1", "P2"];
9116
- if (priorityFilter !== void 0 && !validPriorities.includes(priorityFilter)) {
9381
+ if (priorityFilter !== void 0 && !["P0", "P1", "P2"].includes(priorityFilter)) {
9117
9382
  console.error(formatError(new Error(`Invalid priority: ${priorityFilter}. Use: P0, P1, P2`)));
9118
9383
  process.exit(1);
9119
9384
  }
9120
- logger.debug("Loading categories...");
9121
9385
  const store = createCategoryStore();
9122
9386
  const definitionsPath = getDefinitionsPath();
9123
- logger.debug(`Loading from: ${definitionsPath}`);
9124
9387
  const loadResult = await store.loadFromDirectory(definitionsPath);
9125
9388
  if (!loadResult.success) {
9126
9389
  console.error(formatError(loadResult.error));
9127
9390
  process.exit(1);
9128
9391
  }
9129
- logger.debug(`Loaded ${loadResult.data} categories`);
9130
9392
  const filter = {};
9131
9393
  if (domainFilter) {
9132
9394
  filter.domain = domainFilter;
@@ -9150,54 +9412,42 @@ program.command("init").description("Initialize Pinata configuration in project"
9150
9412
  const configPath = resolve(process.cwd(), ".pinata.yml");
9151
9413
  const cacheDir = resolve(process.cwd(), ".pinata");
9152
9414
  if (existsSync(configPath) && !options["force"]) {
9153
- console.log(chalk5.yellow("Configuration file already exists at .pinata.yml"));
9154
- console.log(chalk5.gray("Use --force to overwrite."));
9415
+ console.log(chalk7.yellow("Configuration file already exists at .pinata.yml"));
9416
+ console.log(chalk7.gray("Use --force to overwrite."));
9155
9417
  process.exit(0);
9156
9418
  }
9157
9419
  const defaultConfig = `# Pinata Configuration
9158
9420
  # https://github.com/pinata/pinata
9159
9421
 
9160
- # Paths to analyze
9161
9422
  include:
9162
9423
  - "src/**/*.ts"
9163
9424
  - "src/**/*.tsx"
9164
9425
  - "src/**/*.py"
9165
9426
  - "src/**/*.js"
9166
9427
 
9167
- # Paths to exclude from analysis
9168
9428
  exclude:
9169
9429
  - "node_modules/**"
9170
9430
  - "dist/**"
9171
9431
  - "build/**"
9172
9432
  - "**/*.test.ts"
9173
9433
  - "**/*.spec.ts"
9174
- - "**/test/**"
9175
- - "**/tests/**"
9176
- - "**/__tests__/**"
9177
9434
 
9178
- # Risk domains to analyze
9179
- # Options: security, data, concurrency, input, resource, reliability, performance, platform, business, compliance
9180
9435
  domains:
9181
9436
  - security
9182
9437
  - data
9183
9438
  - concurrency
9184
9439
  - input
9185
9440
 
9186
- # Minimum severity to report
9187
- # Options: critical, high, medium, low
9188
9441
  minSeverity: medium
9189
9442
 
9190
- # Output configuration
9191
9443
  output:
9192
- format: terminal # terminal, json, markdown, sarif, html
9444
+ format: terminal
9193
9445
  color: true
9194
9446
 
9195
- # Test generation settings
9196
9447
  generate:
9197
9448
  outputDir: tests/generated
9198
- framework: auto # auto, pytest, jest, vitest, mocha
9449
+ framework: auto
9199
9450
 
9200
- # Fail CI if gaps exceed thresholds
9201
9451
  thresholds:
9202
9452
  critical: 0
9203
9453
  high: 5
@@ -9206,25 +9456,23 @@ thresholds:
9206
9456
  const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
9207
9457
  try {
9208
9458
  await writeFileAsync(configPath, defaultConfig, "utf8");
9209
- console.log(chalk5.green("Created .pinata.yml"));
9459
+ console.log(chalk7.green("Created .pinata.yml"));
9210
9460
  await mkdir4(cacheDir, { recursive: true });
9211
- console.log(chalk5.green("Created .pinata/ directory"));
9461
+ console.log(chalk7.green("Created .pinata/ directory"));
9212
9462
  const gitignorePath = resolve(process.cwd(), ".gitignore");
9213
9463
  if (existsSync(gitignorePath)) {
9214
- const { readFile: readFile6, appendFile } = await import('fs/promises');
9215
- const gitignore = await readFile6(gitignorePath, "utf8");
9464
+ const { readFile: readFile7, appendFile } = await import('fs/promises');
9465
+ const gitignore = await readFile7(gitignorePath, "utf8");
9216
9466
  if (!gitignore.includes(".pinata/")) {
9217
9467
  await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
9218
- console.log(chalk5.green("Added .pinata/ to .gitignore"));
9468
+ console.log(chalk7.green("Added .pinata/ to .gitignore"));
9219
9469
  }
9220
9470
  }
9221
9471
  console.log();
9222
- console.log(chalk5.bold("Pinata initialized successfully!"));
9223
- console.log();
9224
- console.log("Next steps:");
9225
- console.log(chalk5.gray(" 1. Review and customize .pinata.yml"));
9226
- console.log(chalk5.gray(" 2. Run: pinata analyze"));
9227
- console.log(chalk5.gray(" 3. Generate tests: pinata generate"));
9472
+ console.log(chalk7.bold("Pinata initialized successfully!"));
9473
+ console.log(chalk7.gray(" 1. Review and customize .pinata.yml"));
9474
+ console.log(chalk7.gray(" 2. Run: pinata analyze"));
9475
+ console.log(chalk7.gray(" 3. Generate tests: pinata generate"));
9228
9476
  } catch (error) {
9229
9477
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9230
9478
  process.exit(1);
@@ -9237,18 +9485,15 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9237
9485
  const checkAge = Boolean(options["checkAge"]);
9238
9486
  const strictMode = Boolean(options["strict"]);
9239
9487
  const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
9240
- console.log(chalk5.bold("\nPinata Dependency Audit\n"));
9488
+ console.log(chalk7.bold("\nPinata Dependency Audit\n"));
9241
9489
  if (!existsSync(packagePath)) {
9242
- console.error(chalk5.red(`Error: ${packagePath} not found`));
9490
+ console.error(chalk7.red(`Error: ${packagePath} not found`));
9243
9491
  process.exit(1);
9244
9492
  }
9245
9493
  const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
9246
- const allDeps = {
9247
- ...packageJson.dependencies,
9248
- ...packageJson.devDependencies
9249
- };
9494
+ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
9250
9495
  const packages = Object.keys(allDeps);
9251
- console.log(chalk5.gray(`Found ${packages.length} dependencies
9496
+ console.log(chalk7.gray(`Found ${packages.length} dependencies
9252
9497
  `));
9253
9498
  const issues = [];
9254
9499
  const KNOWN_MALWARE = /* @__PURE__ */ new Set([
@@ -9271,56 +9516,31 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9271
9516
  ]);
9272
9517
  for (const pkg of packages) {
9273
9518
  if (KNOWN_MALWARE.has(pkg)) {
9274
- issues.push({
9275
- pkg,
9276
- severity: "critical",
9277
- message: "Known malicious/compromised package (Shai-Hulud/typosquat)"
9278
- });
9519
+ issues.push({ pkg, severity: "critical", message: "Known malicious/compromised package (Shai-Hulud/typosquat)" });
9279
9520
  }
9280
9521
  }
9281
9522
  for (const [pkg, version] of Object.entries(allDeps)) {
9282
9523
  if (version?.startsWith("^")) {
9283
- issues.push({
9284
- pkg,
9285
- severity: "warning",
9286
- message: `Unpinned version (${version}) - allows minor updates`
9287
- });
9524
+ issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows minor updates` });
9288
9525
  } else if (version?.startsWith("~")) {
9289
- issues.push({
9290
- pkg,
9291
- severity: "warning",
9292
- message: `Unpinned version (${version}) - allows patch updates`
9293
- });
9526
+ issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows patch updates` });
9294
9527
  } else if (version === "*" || version === "latest") {
9295
- issues.push({
9296
- pkg,
9297
- severity: "critical",
9298
- message: `Extremely dangerous version (${version}) - allows any version`
9299
- });
9528
+ issues.push({ pkg, severity: "critical", message: `Extremely dangerous version (${version}) - allows any version` });
9300
9529
  }
9301
9530
  }
9302
9531
  if (checkRegistry || doAllChecks) {
9303
- const spinner = ora("Checking npm registry...").start();
9532
+ const spinner = ora3("Checking npm registry...").start();
9304
9533
  for (const pkg of packages.slice(0, 50)) {
9305
9534
  try {
9306
9535
  const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`);
9307
9536
  if (response.status === 404) {
9308
- issues.push({
9309
- pkg,
9310
- severity: "critical",
9311
- message: "Package NOT FOUND in npm registry (slopsquatting risk)"
9312
- });
9537
+ issues.push({ pkg, severity: "critical", message: "Package NOT FOUND in npm registry (slopsquatting risk)" });
9313
9538
  } else if (response.ok) {
9314
9539
  const data = await response.json();
9315
9540
  if ((checkAge || doAllChecks) && data.time?.created) {
9316
- const created = new Date(data.time.created);
9317
- const ageInDays = (Date.now() - created.getTime()) / (1e3 * 60 * 60 * 24);
9541
+ const ageInDays = (Date.now() - new Date(data.time.created).getTime()) / (1e3 * 60 * 60 * 24);
9318
9542
  if (ageInDays < 30) {
9319
- issues.push({
9320
- pkg,
9321
- severity: "warning",
9322
- message: `Very new package (${Math.floor(ageInDays)} days old)`
9323
- });
9543
+ issues.push({ pkg, severity: "warning", message: `Very new package (${Math.floor(ageInDays)} days old)` });
9324
9544
  }
9325
9545
  }
9326
9546
  }
@@ -9332,24 +9552,24 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
9332
9552
  const criticals = issues.filter((i) => i.severity === "critical");
9333
9553
  const warnings = issues.filter((i) => i.severity === "warning");
9334
9554
  if (criticals.length > 0) {
9335
- console.log(chalk5.red.bold(`
9555
+ console.log(chalk7.red.bold(`
9336
9556
  Critical Issues (${criticals.length}):`));
9337
9557
  for (const issue of criticals) {
9338
- console.log(chalk5.red(` \u2717 ${issue.pkg}: ${issue.message}`));
9558
+ console.log(chalk7.red(` \u2717 ${issue.pkg}: ${issue.message}`));
9339
9559
  }
9340
9560
  }
9341
9561
  if (warnings.length > 0) {
9342
- console.log(chalk5.yellow.bold(`
9562
+ console.log(chalk7.yellow.bold(`
9343
9563
  Warnings (${warnings.length}):`));
9344
9564
  for (const issue of warnings.slice(0, 20)) {
9345
- console.log(chalk5.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
9565
+ console.log(chalk7.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
9346
9566
  }
9347
9567
  if (warnings.length > 20) {
9348
- console.log(chalk5.gray(` ... and ${warnings.length - 20} more`));
9568
+ console.log(chalk7.gray(` ... and ${warnings.length - 20} more`));
9349
9569
  }
9350
9570
  }
9351
9571
  if (issues.length === 0) {
9352
- console.log(chalk5.green("\u2713 No dependency issues found"));
9572
+ console.log(chalk7.green("\u2713 No dependency issues found"));
9353
9573
  }
9354
9574
  console.log();
9355
9575
  if (criticals.length > 0 || strictMode && warnings.length > 0) {
@@ -9362,7 +9582,7 @@ program.command("feedback").description("View pattern performance feedback (Laye
9362
9582
  const shouldReset = Boolean(options["reset"]);
9363
9583
  if (shouldReset) {
9364
9584
  await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
9365
- console.log(chalk5.green("Feedback data reset."));
9585
+ console.log(chalk7.green("Feedback data reset."));
9366
9586
  return;
9367
9587
  }
9368
9588
  const state = await loadFeedback2();
@@ -9374,20 +9594,20 @@ program.command("feedback").description("View pattern performance feedback (Laye
9374
9594
  console.log(generateReport2(state));
9375
9595
  return;
9376
9596
  }
9377
- console.log(chalk5.bold("\nPinata Feedback Report\n"));
9597
+ console.log(chalk7.bold("\nPinata Feedback Report\n"));
9378
9598
  console.log(`Total scans: ${state.totalScans}`);
9379
9599
  console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
9380
9600
  if (state.totalScans === 0) {
9381
- console.log(chalk5.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
9601
+ console.log(chalk7.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
9382
9602
  return;
9383
9603
  }
9384
9604
  const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
9385
9605
  if (patterns.length > 0) {
9386
- console.log(chalk5.bold("\nPattern Performance:"));
9606
+ console.log(chalk7.bold("\nPattern Performance:"));
9387
9607
  for (const p of patterns.slice(0, 15)) {
9388
9608
  const total = p.confirmedCount + p.unconfirmedCount;
9389
9609
  const precisionPct = (p.precision * 100).toFixed(0);
9390
- const color = p.precision >= 0.7 ? chalk5.green : p.precision >= 0.4 ? chalk5.yellow : chalk5.red;
9610
+ const color = p.precision >= 0.7 ? chalk7.green : p.precision >= 0.4 ? chalk7.yellow : chalk7.red;
9391
9611
  console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
9392
9612
  }
9393
9613
  }
@@ -9409,76 +9629,74 @@ Examples:
9409
9629
  case "anthropic-api-key": {
9410
9630
  const validation = validateApiKey2("anthropic", value);
9411
9631
  if (!validation.valid) {
9412
- console.log(chalk5.red(`Invalid API key: ${validation.error}`));
9632
+ console.log(chalk7.red(`Invalid API key: ${validation.error}`));
9413
9633
  process.exit(1);
9414
9634
  }
9415
9635
  setConfigValue2("anthropicApiKey", value);
9416
- console.log(chalk5.green(`Anthropic API key set: ${maskApiKey2(value)}`));
9636
+ console.log(chalk7.green(`Anthropic API key set: ${maskApiKey2(value)}`));
9417
9637
  break;
9418
9638
  }
9419
9639
  case "openai-api-key": {
9420
9640
  const validation = validateApiKey2("openai", value);
9421
9641
  if (!validation.valid) {
9422
- console.log(chalk5.red(`Invalid API key: ${validation.error}`));
9642
+ console.log(chalk7.red(`Invalid API key: ${validation.error}`));
9423
9643
  process.exit(1);
9424
9644
  }
9425
9645
  setConfigValue2("openaiApiKey", value);
9426
- console.log(chalk5.green(`OpenAI API key set: ${maskApiKey2(value)}`));
9646
+ console.log(chalk7.green(`OpenAI API key set: ${maskApiKey2(value)}`));
9427
9647
  break;
9428
9648
  }
9429
9649
  case "default-provider": {
9430
9650
  if (value !== "anthropic" && value !== "openai") {
9431
- console.log(chalk5.red("Provider must be 'anthropic' or 'openai'"));
9651
+ console.log(chalk7.red("Provider must be 'anthropic' or 'openai'"));
9432
9652
  process.exit(1);
9433
9653
  }
9434
9654
  setConfigValue2("defaultProvider", value);
9435
- console.log(chalk5.green(`Default provider set to: ${value}`));
9655
+ console.log(chalk7.green(`Default provider set to: ${value}`));
9436
9656
  break;
9437
9657
  }
9438
9658
  default:
9439
- console.log(chalk5.red(`Unknown config key: ${key}`));
9440
- console.log(chalk5.gray("Run 'pinata config set --help' for available keys"));
9659
+ console.log(chalk7.red(`Unknown config key: ${key}`));
9660
+ console.log(chalk7.gray("Run 'pinata config set --help' for available keys"));
9441
9661
  process.exit(1);
9442
9662
  }
9443
- console.log(chalk5.gray(`Config stored at: ${getConfigPath2()}`));
9663
+ console.log(chalk7.gray(`Config stored at: ${getConfigPath2()}`));
9444
9664
  });
9445
9665
  config.command("get <key>").description("Get a configuration value").action(async (key) => {
9446
9666
  const { loadConfig: loadConfig2, maskApiKey: maskApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9447
9667
  const cfg = loadConfig2();
9448
9668
  switch (key) {
9449
9669
  case "anthropic-api-key":
9450
- console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk5.gray("(not set)"));
9670
+ console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk7.gray("(not set)"));
9451
9671
  break;
9452
9672
  case "openai-api-key":
9453
- console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk5.gray("(not set)"));
9673
+ console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk7.gray("(not set)"));
9454
9674
  break;
9455
9675
  case "default-provider":
9456
- console.log(cfg.defaultProvider ?? chalk5.gray("anthropic (default)"));
9676
+ console.log(cfg.defaultProvider ?? chalk7.gray("anthropic (default)"));
9457
9677
  break;
9458
9678
  default:
9459
- console.log(chalk5.red(`Unknown config key: ${key}`));
9679
+ console.log(chalk7.red(`Unknown config key: ${key}`));
9460
9680
  process.exit(1);
9461
9681
  }
9462
9682
  });
9463
9683
  config.command("list").description("List all configuration values").action(async () => {
9464
9684
  const { loadConfig: loadConfig2, maskApiKey: maskApiKey2, getConfigPath: getConfigPath2, hasApiKey: hasApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9465
9685
  const cfg = loadConfig2();
9466
- console.log(chalk5.bold("Pinata Configuration"));
9467
- console.log(chalk5.gray(`Config file: ${getConfigPath2()}`));
9686
+ console.log(chalk7.bold("Pinata Configuration"));
9687
+ console.log(chalk7.gray(`Config file: ${getConfigPath2()}`));
9468
9688
  console.log();
9469
9689
  console.log("AI Providers:");
9470
- const anthropicStatus = hasApiKey2("anthropic") ? chalk5.green("configured") : chalk5.gray("not set");
9471
- const openaiStatus = hasApiKey2("openai") ? chalk5.green("configured") : chalk5.gray("not set");
9472
- console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ? chalk5.gray(`(${maskApiKey2(cfg.anthropicApiKey)})`) : ""}`);
9473
- console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ? chalk5.gray(`(${maskApiKey2(cfg.openaiApiKey)})`) : ""}`);
9690
+ const anthropicStatus = hasApiKey2("anthropic") ? chalk7.green("configured") : chalk7.gray("not set");
9691
+ const openaiStatus = hasApiKey2("openai") ? chalk7.green("configured") : chalk7.gray("not set");
9692
+ console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ? chalk7.gray(`(${maskApiKey2(cfg.anthropicApiKey)})`) : ""}`);
9693
+ console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ? chalk7.gray(`(${maskApiKey2(cfg.openaiApiKey)})`) : ""}`);
9474
9694
  console.log(` Default provider: ${cfg.defaultProvider ?? "anthropic"}`);
9475
9695
  console.log();
9476
9696
  if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
9477
- console.log(chalk5.yellow("No AI provider configured."));
9478
- console.log(chalk5.gray("To use AI features (explain, suggest-patterns, --ai flag):"));
9479
- console.log(chalk5.gray(" pinata config set anthropic-api-key sk-ant-xxx"));
9480
- console.log(chalk5.gray(" # or"));
9481
- console.log(chalk5.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
9697
+ console.log(chalk7.yellow("No AI provider configured."));
9698
+ console.log(chalk7.gray(" pinata config set anthropic-api-key sk-ant-xxx"));
9699
+ console.log(chalk7.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
9482
9700
  }
9483
9701
  });
9484
9702
  config.command("unset <key>").description("Remove a configuration value").action(async (key) => {
@@ -9486,18 +9704,18 @@ config.command("unset <key>").description("Remove a configuration value").action
9486
9704
  switch (key) {
9487
9705
  case "anthropic-api-key":
9488
9706
  deleteConfigValue2("anthropicApiKey");
9489
- console.log(chalk5.green("Anthropic API key removed"));
9707
+ console.log(chalk7.green("Anthropic API key removed"));
9490
9708
  break;
9491
9709
  case "openai-api-key":
9492
9710
  deleteConfigValue2("openaiApiKey");
9493
- console.log(chalk5.green("OpenAI API key removed"));
9711
+ console.log(chalk7.green("OpenAI API key removed"));
9494
9712
  break;
9495
9713
  case "default-provider":
9496
9714
  deleteConfigValue2("defaultProvider");
9497
- console.log(chalk5.green("Default provider reset to: anthropic"));
9715
+ console.log(chalk7.green("Default provider reset to: anthropic"));
9498
9716
  break;
9499
9717
  default:
9500
- console.log(chalk5.red(`Unknown config key: ${key}`));
9718
+ console.log(chalk7.red(`Unknown config key: ${key}`));
9501
9719
  process.exit(1);
9502
9720
  }
9503
9721
  });
@@ -9505,18 +9723,13 @@ var auth = program.command("auth").description("Manage API key authentication");
9505
9723
  auth.command("login").description("Set API key for Pinata Cloud").option("-k, --key <key>", "API key (or set PINATA_API_KEY env var)").action(async (options) => {
9506
9724
  const apiKey = options["key"] ?? process.env["PINATA_API_KEY"];
9507
9725
  if (!apiKey) {
9508
- console.log(chalk5.yellow("No API key provided."));
9509
- console.log();
9510
- console.log("Provide an API key using one of:");
9511
- console.log(chalk5.gray(" pinata auth login --key <your-api-key>"));
9512
- console.log(chalk5.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
9513
- console.log();
9514
- console.log("Get your API key at: https://app.pinata.dev/settings/api");
9726
+ console.log(chalk7.yellow("No API key provided."));
9727
+ console.log(chalk7.gray(" pinata auth login --key <your-api-key>"));
9728
+ console.log(chalk7.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
9515
9729
  process.exit(1);
9516
9730
  }
9517
9731
  if (apiKey.length < 20 || !apiKey.startsWith("pk_")) {
9518
- console.log(chalk5.red("Invalid API key format."));
9519
- console.log(chalk5.gray("Keys should start with 'pk_' and be at least 20 characters."));
9732
+ console.log(chalk7.red("Invalid API key format. Keys should start with 'pk_'."));
9520
9733
  process.exit(1);
9521
9734
  }
9522
9735
  const configDir = resolve(process.cwd(), ".pinata");
@@ -9525,19 +9738,13 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
9525
9738
  try {
9526
9739
  await mkdir4(configDir, { recursive: true });
9527
9740
  const maskedKey = `****${apiKey.slice(-8)}`;
9528
- const authData = {
9529
- configured: true,
9530
- keyId: maskedKey,
9531
- configuredAt: (/* @__PURE__ */ new Date()).toISOString()
9532
- };
9533
- await writeFileAsync(authPath, JSON.stringify(authData, null, 2), "utf8");
9741
+ await writeFileAsync(authPath, JSON.stringify({ configured: true, keyId: maskedKey, configuredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf8");
9534
9742
  const envPath = resolve(configDir, ".env");
9535
9743
  await writeFileAsync(envPath, `PINATA_API_KEY=${apiKey}
9536
9744
  `, { mode: 384 });
9537
- console.log(chalk5.green("API key configured successfully!"));
9538
- console.log(chalk5.gray(`Key ID: ${maskedKey}`));
9539
- console.log();
9540
- console.log(chalk5.yellow("Important: Add .pinata/.env to your .gitignore"));
9745
+ console.log(chalk7.green("API key configured successfully!"));
9746
+ console.log(chalk7.gray(`Key ID: ${maskedKey}`));
9747
+ console.log(chalk7.yellow("Important: Add .pinata/.env to your .gitignore"));
9541
9748
  } catch (error) {
9542
9749
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9543
9750
  process.exit(1);
@@ -9558,11 +9765,7 @@ auth.command("logout").description("Remove stored API key").action(async () => {
9558
9765
  await rm2(envPath);
9559
9766
  removed = true;
9560
9767
  }
9561
- if (removed) {
9562
- console.log(chalk5.green("API key removed successfully."));
9563
- } else {
9564
- console.log(chalk5.yellow("No stored API key found."));
9565
- }
9768
+ console.log(removed ? chalk7.green("API key removed successfully.") : chalk7.yellow("No stored API key found."));
9566
9769
  } catch (error) {
9567
9770
  console.error(formatError(error instanceof Error ? error : new Error(String(error))));
9568
9771
  process.exit(1);
@@ -9571,19 +9774,19 @@ auth.command("logout").description("Remove stored API key").action(async () => {
9571
9774
  auth.command("status").description("Check authentication status").action(async () => {
9572
9775
  const authPath = resolve(process.cwd(), ".pinata", "auth.json");
9573
9776
  if (!existsSync(authPath)) {
9574
- console.log(chalk5.yellow("Not authenticated."));
9575
- console.log(chalk5.gray("Run: pinata auth login --key <your-api-key>"));
9777
+ console.log(chalk7.yellow("Not authenticated."));
9778
+ console.log(chalk7.gray("Run: pinata auth login --key <your-api-key>"));
9576
9779
  process.exit(0);
9577
9780
  }
9578
9781
  try {
9579
- const { readFile: readFile6 } = await import('fs/promises');
9580
- const authData = JSON.parse(await readFile6(authPath, "utf8"));
9581
- console.log(chalk5.green("Authenticated"));
9582
- console.log(chalk5.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
9583
- console.log(chalk5.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
9584
- } catch (error) {
9585
- console.log(chalk5.yellow("Authentication status unknown."));
9586
- console.log(chalk5.gray("Run: pinata auth login to reconfigure."));
9782
+ const { readFile: readFile7 } = await import('fs/promises');
9783
+ const authData = JSON.parse(await readFile7(authPath, "utf8"));
9784
+ console.log(chalk7.green("Authenticated"));
9785
+ console.log(chalk7.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
9786
+ console.log(chalk7.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
9787
+ } catch {
9788
+ console.log(chalk7.yellow("Authentication status unknown."));
9789
+ console.log(chalk7.gray("Run: pinata auth login to reconfigure."));
9587
9790
  }
9588
9791
  });
9589
9792
  program.parse();