depwire-cli 0.9.25 → 0.9.27

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.
@@ -14,8 +14,9 @@ import {
14
14
  loadMetadata,
15
15
  parseProject,
16
16
  parseTypeScriptFile,
17
+ scanSecurity,
17
18
  searchSymbols
18
- } from "./chunk-QHVWDUSX.js";
19
+ } from "./chunk-DA5LWNJ4.js";
19
20
 
20
21
  // src/viz/data.ts
21
22
  import { basename } from "path";
@@ -170,14 +171,14 @@ async function findAvailablePort(startPort, maxAttempts = 10) {
170
171
  const net = await import("net");
171
172
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
172
173
  const testPort = startPort + attempt;
173
- const isAvailable = await new Promise((resolve2) => {
174
+ const isAvailable = await new Promise((resolve4) => {
174
175
  const server = net.createServer();
175
176
  server.once("error", () => {
176
- resolve2(false);
177
+ resolve4(false);
177
178
  });
178
179
  server.once("listening", () => {
179
180
  server.close();
180
- resolve2(true);
181
+ resolve4(true);
181
182
  });
182
183
  server.listen(testPort, "127.0.0.1");
183
184
  });
@@ -376,7 +377,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
376
377
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
377
378
 
378
379
  // src/mcp/tools.ts
379
- import { dirname as dirname2, join as join5 } from "path";
380
+ import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
380
381
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
381
382
 
382
383
  // src/mcp/connect.ts
@@ -625,8 +626,8 @@ async function getCurrentBranch(dir) {
625
626
  }
626
627
  }
627
628
  async function checkoutCommit(dir, hash) {
628
- if (!/^[a-zA-Z0-9_.\-\/]+$/.test(hash)) {
629
- throw new Error(`Invalid git ref: ${hash}`);
629
+ if (!/^[a-f0-9]+$/.test(hash)) {
630
+ throw new Error(`Invalid commit hash: ${hash}`);
630
631
  }
631
632
  try {
632
633
  execSync(`git checkout -q ${hash}`, { cwd: dir, stdio: "ignore" });
@@ -635,11 +636,12 @@ async function checkoutCommit(dir, hash) {
635
636
  }
636
637
  }
637
638
  async function restoreOriginal(dir, originalBranch) {
638
- if (!/^[a-zA-Z0-9_.\-\/]+$/.test(originalBranch)) {
639
- throw new Error(`Invalid git ref: ${originalBranch}`);
639
+ if (!/^[a-zA-Z0-9/_.\-]+$/.test(originalBranch)) {
640
+ throw new Error(`Invalid branch name: ${originalBranch}`);
640
641
  }
641
642
  try {
642
643
  execSync(`git checkout -q ${originalBranch}`, {
644
+ // depwire-security-reviewed: branch validated above
643
645
  cwd: dir,
644
646
  stdio: "ignore"
645
647
  });
@@ -787,19 +789,22 @@ function getWeekNumber(date) {
787
789
 
788
790
  // src/temporal/snapshots.ts
789
791
  import { writeFileSync, readFileSync, mkdirSync, existsSync as existsSync2, readdirSync } from "fs";
790
- import { join as join4 } from "path";
792
+ import { resolve as resolve2 } from "path";
791
793
  function saveSnapshot(snapshot, outputDir) {
792
794
  if (!existsSync2(outputDir)) {
793
795
  mkdirSync(outputDir, { recursive: true });
794
796
  }
795
797
  const filename = `${snapshot.commitHash.substring(0, 8)}.json`;
796
- const filepath = join4(outputDir, filename);
798
+ const filepath = resolve2(outputDir, filename);
799
+ if (!filepath.startsWith(resolve2(outputDir))) {
800
+ throw new Error(`Path traversal attempt blocked: ${filepath}`);
801
+ }
797
802
  writeFileSync(filepath, JSON.stringify(snapshot, null, 2), "utf-8");
798
803
  }
799
804
  function loadSnapshot(commitHash, outputDir) {
800
805
  const shortHash = commitHash.substring(0, 8);
801
- const filepath = join4(outputDir, `${shortHash}.json`);
802
- if (!existsSync2(filepath)) {
806
+ const filepath = resolve2(outputDir, `${shortHash}.json`);
807
+ if (!filepath.startsWith(resolve2(outputDir)) || !existsSync2(filepath)) {
803
808
  return null;
804
809
  }
805
810
  try {
@@ -1127,6 +1132,50 @@ Always run this before any refactor that touches file structure.`,
1127
1132
  },
1128
1133
  required: ["operation", "target"]
1129
1134
  }
1135
+ },
1136
+ {
1137
+ name: "security_scan",
1138
+ description: `Scan the codebase for security vulnerabilities using deterministic checks + graph-aware severity scoring. No API key required.
1139
+
1140
+ Checks: dependency CVEs, shell injection, hardcoded secrets, path traversal, auth bypass, input validation, information disclosure, cryptography weaknesses, frontend XSS, architecture-level risks.
1141
+
1142
+ Graph-aware severity: vulnerabilities reachable from MCP tools or HTTP routes are automatically elevated. A medium shell injection reachable from connect_repo becomes Critical.
1143
+
1144
+ Returns ranked findings (Critical \u2192 Low) with attack scenarios and suggested fixes. Use --target for single-file scan.`,
1145
+ inputSchema: {
1146
+ type: "object",
1147
+ properties: {
1148
+ target: {
1149
+ type: "string",
1150
+ description: "Relative file path to scan. Omit to scan entire repo."
1151
+ },
1152
+ classes: {
1153
+ type: "array",
1154
+ items: {
1155
+ type: "string",
1156
+ enum: [
1157
+ "dependency-cve",
1158
+ "shell-injection",
1159
+ "code-injection",
1160
+ "secrets",
1161
+ "path-traversal",
1162
+ "auth",
1163
+ "input-validation",
1164
+ "information-disclosure",
1165
+ "architecture",
1166
+ "cryptography",
1167
+ "supply-chain",
1168
+ "frontend-xss"
1169
+ ]
1170
+ },
1171
+ description: "Vulnerability classes to check. Omit for all."
1172
+ },
1173
+ graphAware: {
1174
+ type: "boolean",
1175
+ description: "Enable graph-aware severity elevation (recommended). Default: true."
1176
+ }
1177
+ }
1178
+ }
1130
1179
  }
1131
1180
  ];
1132
1181
  }
@@ -1209,6 +1258,19 @@ async function handleToolCall(name, args, state) {
1209
1258
  } else {
1210
1259
  result = handleSimulateChange(args, state);
1211
1260
  }
1261
+ } else if (name === "security_scan") {
1262
+ if (!isProjectLoaded(state)) {
1263
+ result = {
1264
+ error: "No project loaded",
1265
+ message: "Use connect_repo to connect to a codebase first"
1266
+ };
1267
+ } else {
1268
+ result = await scanSecurity(state.projectRoot, state.graph, {
1269
+ target: args.target,
1270
+ classes: args.classes,
1271
+ graphAware: args.graphAware !== false
1272
+ });
1273
+ }
1212
1274
  } else {
1213
1275
  if (!isProjectLoaded(state)) {
1214
1276
  result = {
@@ -1679,6 +1741,10 @@ Available document types:
1679
1741
  continue;
1680
1742
  }
1681
1743
  const filePath = join5(docsDir, metadata.documents[doc].file);
1744
+ if (!resolve3(filePath).startsWith(resolve3(docsDir))) {
1745
+ missing.push(doc);
1746
+ continue;
1747
+ }
1682
1748
  if (!existsSync3(filePath)) {
1683
1749
  missing.push(doc);
1684
1750
  continue;
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  stashChanges,
18
18
  updateFileInGraph,
19
19
  watchProject
20
- } from "./chunk-ORGAO3HT.js";
20
+ } from "./chunk-RGD3YJYQ.js";
21
21
  import {
22
22
  SimulationEngine,
23
23
  analyzeDeadCode,
@@ -29,14 +29,15 @@ import {
29
29
  getHealthTrend,
30
30
  getImpact,
31
31
  parseProject,
32
+ scanSecurity,
32
33
  searchSymbols
33
- } from "./chunk-QHVWDUSX.js";
34
+ } from "./chunk-DA5LWNJ4.js";
34
35
 
35
36
  // src/index.ts
36
37
  import { Command } from "commander";
37
- import { resolve as resolve2, dirname as dirname3, join as join4 } from "path";
38
- import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
39
- import { fileURLToPath as fileURLToPath3 } from "url";
38
+ import { resolve as resolve4, dirname as dirname4, join as join5 } from "path";
39
+ import { writeFileSync, readFileSync as readFileSync3, existsSync } from "fs";
40
+ import { fileURLToPath as fileURLToPath4 } from "url";
40
41
 
41
42
  // src/graph/serializer.ts
42
43
  import { DirectedGraph } from "graphology";
@@ -230,7 +231,7 @@ import { join as join2 } from "path";
230
231
  import express from "express";
231
232
  import { readFileSync } from "fs";
232
233
  import { fileURLToPath } from "url";
233
- import { dirname, join } from "path";
234
+ import { dirname, resolve } from "path";
234
235
  import open from "open";
235
236
 
236
237
  // src/viz/temporal-data.ts
@@ -305,10 +306,10 @@ async function findAvailablePort(startPort) {
305
306
  const net = await import("net");
306
307
  for (let attempt = 0; attempt < 10; attempt++) {
307
308
  const testPort = startPort + attempt;
308
- const isAvailable = await new Promise((resolve3) => {
309
- const server = net.createServer().once("error", () => resolve3(false)).once("listening", () => {
309
+ const isAvailable = await new Promise((resolve5) => {
310
+ const server = net.createServer().once("error", () => resolve5(false)).once("listening", () => {
310
311
  server.close();
311
- resolve3(true);
312
+ resolve5(true);
312
313
  }).listen(testPort, "127.0.0.1");
313
314
  });
314
315
  if (isAvailable) {
@@ -324,19 +325,22 @@ async function startTemporalServer(snapshots, projectRoot, preferredPort = 3334)
324
325
  app.get("/api/data", (_req, res) => {
325
326
  res.json(vizData);
326
327
  });
327
- const publicDir = join(__dirname, "viz", "public");
328
+ const publicDir = resolve(__dirname, "viz", "public");
328
329
  app.get("/", (_req, res) => {
329
- const htmlPath = join(publicDir, "temporal.html");
330
+ const htmlPath = resolve(publicDir, "temporal.html");
331
+ if (!htmlPath.startsWith(publicDir)) return res.status(403).send("Forbidden");
330
332
  const html = readFileSync(htmlPath, "utf-8");
331
333
  res.send(html);
332
334
  });
333
335
  app.get("/temporal.js", (_req, res) => {
334
- const jsPath = join(publicDir, "temporal.js");
336
+ const jsPath = resolve(publicDir, "temporal.js");
337
+ if (!jsPath.startsWith(publicDir)) return res.status(403).send("Forbidden");
335
338
  const js = readFileSync(jsPath, "utf-8");
336
339
  res.type("application/javascript").send(js);
337
340
  });
338
341
  app.get("/temporal.css", (_req, res) => {
339
- const cssPath = join(publicDir, "temporal.css");
342
+ const cssPath = resolve(publicDir, "temporal.css");
343
+ if (!cssPath.startsWith(publicDir)) return res.status(403).send("Forbidden");
340
344
  const css = readFileSync(cssPath, "utf-8");
341
345
  res.type("text/css").send(css);
342
346
  });
@@ -349,13 +353,13 @@ async function startTemporalServer(snapshots, projectRoot, preferredPort = 3334)
349
353
  console.log(" (Could not open browser automatically)");
350
354
  });
351
355
  });
352
- await new Promise((resolve3, reject) => {
356
+ await new Promise((resolve5, reject) => {
353
357
  server.on("error", reject);
354
358
  process.on("SIGINT", () => {
355
359
  console.log("\n\nShutting down temporal server...");
356
360
  server.close(() => {
357
361
  console.log("Server stopped");
358
- resolve3();
362
+ resolve5();
359
363
  process.exit(0);
360
364
  });
361
365
  });
@@ -500,7 +504,7 @@ async function trackCommand(command, version = "unknown") {
500
504
  }
501
505
 
502
506
  // src/commands/whatif.ts
503
- import { resolve } from "path";
507
+ import { resolve as resolve2 } from "path";
504
508
  import chalk from "chalk";
505
509
 
506
510
  // src/viz/whatif-server.ts
@@ -689,14 +693,14 @@ async function findAvailablePort2(startPort, maxAttempts = 10) {
689
693
  const net = await import("net");
690
694
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
691
695
  const testPort = startPort + attempt;
692
- const isAvailable = await new Promise((resolve3) => {
696
+ const isAvailable = await new Promise((resolve5) => {
693
697
  const server = net.createServer();
694
698
  server.once("error", () => {
695
- resolve3(false);
699
+ resolve5(false);
696
700
  });
697
701
  server.once("listening", () => {
698
702
  server.close();
699
- resolve3(true);
703
+ resolve5(true);
700
704
  });
701
705
  server.listen(testPort, "127.0.0.1");
702
706
  });
@@ -751,7 +755,7 @@ Opening What If UI at ${url}`);
751
755
  // src/commands/whatif.ts
752
756
  async function whatif(dir, options) {
753
757
  if (!options.simulate) {
754
- const projectRoot2 = dir === "." ? findProjectRoot() : resolve(dir);
758
+ const projectRoot2 = dir === "." ? findProjectRoot() : resolve2(dir);
755
759
  console.error(`Parsing project: ${projectRoot2}`);
756
760
  const parsedFiles2 = await parseProject(projectRoot2);
757
761
  const graph2 = buildGraph(parsedFiles2);
@@ -777,7 +781,7 @@ async function whatif(dir, options) {
777
781
  process.exit(1);
778
782
  }
779
783
  const action = buildAction(options);
780
- const projectRoot = dir === "." ? findProjectRoot() : resolve(dir);
784
+ const projectRoot = dir === "." ? findProjectRoot() : resolve2(dir);
781
785
  console.error(`Parsing project: ${projectRoot}`);
782
786
  const parsedFiles = await parseProject(projectRoot);
783
787
  const graph = buildGraph(parsedFiles);
@@ -887,18 +891,198 @@ function formatAction(action) {
887
891
  }
888
892
  }
889
893
 
890
- // src/index.ts
894
+ // src/commands/security.ts
895
+ import { resolve as resolve3, dirname as dirname3, join as join4 } from "path";
896
+ import { readFileSync as readFileSync2 } from "fs";
897
+ import { fileURLToPath as fileURLToPath3 } from "url";
898
+
899
+ // src/security/reporter.ts
900
+ import chalk2 from "chalk";
901
+ var SEVERITY_COLORS = {
902
+ critical: chalk2.red.bold,
903
+ high: chalk2.red,
904
+ medium: chalk2.yellow,
905
+ low: chalk2.blue,
906
+ info: chalk2.dim
907
+ };
908
+ var SEVERITY_LABELS = {
909
+ critical: "CRITICAL",
910
+ high: "HIGH",
911
+ medium: "MEDIUM",
912
+ low: "LOW",
913
+ info: "INFO"
914
+ };
915
+ function formatTable(result, elapsedMs) {
916
+ const lines = [];
917
+ const sep = "\u2500".repeat(62);
918
+ lines.push("");
919
+ lines.push(chalk2.bold("Depwire Security Scan"));
920
+ lines.push("");
921
+ const summaryParts = [
922
+ result.summary.critical > 0 ? chalk2.red.bold(`${result.summary.critical} Critical`) : null,
923
+ result.summary.high > 0 ? chalk2.red(`${result.summary.high} High`) : null,
924
+ result.summary.medium > 0 ? chalk2.yellow(`${result.summary.medium} Medium`) : null,
925
+ result.summary.low > 0 ? chalk2.blue(`${result.summary.low} Low`) : null,
926
+ result.summary.info > 0 ? chalk2.dim(`${result.summary.info} Info`) : null
927
+ ].filter(Boolean);
928
+ if (summaryParts.length > 0) {
929
+ lines.push(`\u250C${sep}\u2510`);
930
+ lines.push(`\u2502 ${summaryParts.join(" \u2502 ")} \u2502`);
931
+ lines.push(`\u2514${sep}\u2518`);
932
+ } else {
933
+ lines.push(chalk2.green.bold(" No security findings detected."));
934
+ }
935
+ lines.push("");
936
+ const severityOrder = ["critical", "high", "medium", "low", "info"];
937
+ for (const severity of severityOrder) {
938
+ const group = result.findings.filter((f) => f.severity === severity);
939
+ if (group.length === 0) continue;
940
+ const colorFn = SEVERITY_COLORS[severity];
941
+ lines.push(colorFn(SEVERITY_LABELS[severity]));
942
+ for (const finding of group) {
943
+ lines.push(` ${colorFn(`[${finding.id}]`)} ${finding.title}`);
944
+ lines.push(` File: ${finding.file}${finding.line ? `:${finding.line}` : ""}`);
945
+ lines.push(` ${chalk2.dim(finding.description)}`);
946
+ lines.push(` ${chalk2.dim("Fix:")} ${finding.suggestedFix}`);
947
+ if (finding.graphReachability?.elevatedBy) {
948
+ lines.push(` ${chalk2.magenta("\u2191 Elevated:")} ${finding.graphReachability.elevatedBy}`);
949
+ }
950
+ lines.push("");
951
+ }
952
+ }
953
+ const elapsed = (elapsedMs / 1e3).toFixed(1);
954
+ lines.push(chalk2.dim(`Scanned ${result.filesScanned} files in ${elapsed}s`));
955
+ lines.push(chalk2.dim("Run with --format json for machine output"));
956
+ lines.push(chalk2.dim("Run with --format sarif for GitHub Security integration"));
957
+ lines.push("");
958
+ return lines.join("\n");
959
+ }
960
+ function formatJSON(result) {
961
+ return JSON.stringify(result, null, 2);
962
+ }
963
+ function formatSARIF(result, version) {
964
+ const rules = result.findings.map((f) => ({
965
+ id: f.id,
966
+ shortDescription: { text: f.title },
967
+ fullDescription: { text: f.description },
968
+ help: { text: f.suggestedFix },
969
+ properties: {
970
+ severity: f.severity,
971
+ vulnerabilityClass: f.vulnerabilityClass
972
+ }
973
+ }));
974
+ const uniqueRules = Array.from(
975
+ new Map(rules.map((r) => [r.id, r])).values()
976
+ );
977
+ const results = result.findings.map((f) => {
978
+ let level;
979
+ if (f.severity === "critical" || f.severity === "high") level = "error";
980
+ else if (f.severity === "medium") level = "warning";
981
+ else level = "note";
982
+ const sarifResult = {
983
+ ruleId: f.id,
984
+ level,
985
+ message: { text: `${f.title}: ${f.description}` },
986
+ locations: [
987
+ {
988
+ physicalLocation: {
989
+ artifactLocation: { uri: f.file },
990
+ region: f.line ? { startLine: f.line } : void 0
991
+ }
992
+ }
993
+ ]
994
+ };
995
+ return sarifResult;
996
+ });
997
+ const sarif = {
998
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
999
+ version: "2.1.0",
1000
+ runs: [
1001
+ {
1002
+ tool: {
1003
+ driver: {
1004
+ name: "depwire",
1005
+ version,
1006
+ rules: uniqueRules
1007
+ }
1008
+ },
1009
+ results
1010
+ }
1011
+ ]
1012
+ };
1013
+ return JSON.stringify(sarif, null, 2);
1014
+ }
1015
+
1016
+ // src/commands/security.ts
891
1017
  var __filename3 = fileURLToPath3(import.meta.url);
892
1018
  var __dirname3 = dirname3(__filename3);
893
- var packageJsonPath = join4(__dirname3, "../package.json");
894
- var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
1019
+ function getVersion() {
1020
+ try {
1021
+ let dir = __dirname3;
1022
+ for (let i = 0; i < 5; i++) {
1023
+ const pkgPath = join4(dir, "package.json");
1024
+ try {
1025
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1026
+ if (pkg.name === "depwire-cli") return pkg.version;
1027
+ } catch {
1028
+ }
1029
+ dir = dirname3(dir);
1030
+ }
1031
+ } catch {
1032
+ }
1033
+ return "0.0.0";
1034
+ }
1035
+ var SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
1036
+ async function securityCommand(dir, options) {
1037
+ const projectRoot = dir === "." ? findProjectRoot() : resolve3(dir);
1038
+ console.error(`Scanning: ${projectRoot}`);
1039
+ const startTime = Date.now();
1040
+ const parsedFiles = await parseProject(projectRoot);
1041
+ console.error(`Parsed ${parsedFiles.length} files`);
1042
+ const graph = buildGraph(parsedFiles);
1043
+ console.error(`Built graph: ${graph.order} symbols, ${graph.size} edges`);
1044
+ const result = await scanSecurity(projectRoot, graph, {
1045
+ target: options.target,
1046
+ classes: options.class,
1047
+ format: options.format || "table",
1048
+ graphAware: true
1049
+ });
1050
+ const elapsedMs = Date.now() - startTime;
1051
+ const format = options.format || "table";
1052
+ if (format === "json") {
1053
+ console.log(formatJSON(result));
1054
+ } else if (format === "sarif") {
1055
+ console.log(formatSARIF(result, getVersion()));
1056
+ } else {
1057
+ console.log(formatTable(result, elapsedMs));
1058
+ }
1059
+ if (options.failOn) {
1060
+ const threshold = options.failOn;
1061
+ const thresholdIdx = SEVERITY_ORDER.indexOf(threshold);
1062
+ if (thresholdIdx >= 0) {
1063
+ const hasFindings = result.findings.some(
1064
+ (f) => SEVERITY_ORDER.indexOf(f.severity) <= thresholdIdx
1065
+ );
1066
+ if (hasFindings) {
1067
+ console.error(`Findings at or above ${threshold} severity detected \u2014 exiting with code 1`);
1068
+ process.exit(1);
1069
+ }
1070
+ }
1071
+ }
1072
+ }
1073
+
1074
+ // src/index.ts
1075
+ var __filename4 = fileURLToPath4(import.meta.url);
1076
+ var __dirname4 = dirname4(__filename4);
1077
+ var packageJsonPath = join5(__dirname4, "../package.json");
1078
+ var packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
895
1079
  var program = new Command();
896
1080
  program.name("depwire").description("Code cross-reference graph builder for TypeScript projects").version(packageJson.version);
897
1081
  program.command("parse").description("Parse a TypeScript project and build dependency graph").argument("[directory]", "Project directory to parse (defaults to current directory or auto-detected project root)").option("-o, --output <path>", "Output JSON file path", "depwire-output.json").option("--pretty", "Pretty-print JSON output").option("--stats", "Print summary statistics").option("--exclude <patterns...>", 'Glob patterns to exclude (e.g., "**/*.test.*" "dist/**")').option("--verbose", "Show detailed parsing progress").action(async (directory, options) => {
898
1082
  trackCommand("parse", packageJson.version);
899
1083
  const startTime = Date.now();
900
1084
  try {
901
- const projectRoot = directory ? resolve2(directory) : findProjectRoot();
1085
+ const projectRoot = directory ? resolve4(directory) : findProjectRoot();
902
1086
  console.log(`Parsing project: ${projectRoot}`);
903
1087
  const parsedFiles = await parseProject(projectRoot, {
904
1088
  exclude: options.exclude,
@@ -937,12 +1121,12 @@ Orphan Files (no cross-references): ${summary.orphanFiles.length}`);
937
1121
  program.command("query").description("Query impact analysis for a symbol").argument("<directory>", "Project directory").argument("<symbol-name>", "Symbol name to query").action(async (directory, symbolName) => {
938
1122
  trackCommand("query", packageJson.version);
939
1123
  try {
940
- const projectRoot = resolve2(directory);
941
- const cacheFile = "depwire-output.json";
1124
+ const projectRoot = resolve4(directory);
1125
+ const cacheFile = resolve4("depwire-output.json");
942
1126
  let graph;
943
1127
  if (existsSync(cacheFile)) {
944
1128
  console.log("Loading from cache...");
945
- const json = JSON.parse(readFileSync2(cacheFile, "utf-8"));
1129
+ const json = JSON.parse(readFileSync3(cacheFile, "utf-8"));
946
1130
  graph = importFromJSON(json);
947
1131
  } else {
948
1132
  console.log("Parsing project...");
@@ -986,7 +1170,7 @@ Total Transitive Dependents: ${impact.transitiveDependents.length}`);
986
1170
  program.command("viz").description("Launch interactive arc diagram visualization").argument("[directory]", "Project directory to visualize (defaults to current directory or auto-detected project root)").option("-p, --port <number>", "Server port", "3333").option("--no-open", "Don't auto-open browser").option("--exclude <patterns...>", 'Glob patterns to exclude (e.g., "**/*.test.*" "dist/**")').option("--verbose", "Show detailed parsing progress").action(async (directory, options) => {
987
1171
  trackCommand("viz", packageJson.version);
988
1172
  try {
989
- const projectRoot = directory ? resolve2(directory) : findProjectRoot();
1173
+ const projectRoot = directory ? resolve4(directory) : findProjectRoot();
990
1174
  console.log(`Parsing project: ${projectRoot}`);
991
1175
  const parsedFiles = await parseProject(projectRoot, {
992
1176
  exclude: options.exclude,
@@ -1009,7 +1193,7 @@ program.command("viz").description("Launch interactive arc diagram visualization
1009
1193
  program.command("temporal").description("Visualize how the dependency graph evolved over git history").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--commits <number>", "Number of commits to sample", "20").option("--strategy <type>", "Sampling strategy: even, weekly, monthly", "even").option("-p, --port <number>", "Server port", "3334").option("--output <path>", "Save snapshots to custom path (default: .depwire/temporal/)").option("--verbose", "Show progress for each commit being parsed").option("--stats", "Show summary statistics at end").action(async (directory, options) => {
1010
1194
  trackCommand("temporal", packageJson.version);
1011
1195
  try {
1012
- const projectRoot = directory ? resolve2(directory) : findProjectRoot();
1196
+ const projectRoot = directory ? resolve4(directory) : findProjectRoot();
1013
1197
  await runTemporalAnalysis(projectRoot, {
1014
1198
  commits: parseInt(options.commits, 10),
1015
1199
  strategy: options.strategy,
@@ -1029,11 +1213,11 @@ program.command("mcp").description("Start MCP server for AI coding tools").argum
1029
1213
  const state = createEmptyState();
1030
1214
  let projectRootToConnect = null;
1031
1215
  if (directory) {
1032
- projectRootToConnect = resolve2(directory);
1216
+ projectRootToConnect = resolve4(directory);
1033
1217
  } else {
1034
1218
  const detectedRoot = findProjectRoot();
1035
1219
  const cwd = process.cwd();
1036
- if (detectedRoot !== cwd || existsSync(join4(cwd, "package.json")) || existsSync(join4(cwd, "tsconfig.json")) || existsSync(join4(cwd, "go.mod")) || existsSync(join4(cwd, "pyproject.toml")) || existsSync(join4(cwd, "setup.py")) || existsSync(join4(cwd, ".git"))) {
1220
+ if (detectedRoot !== cwd || existsSync(join5(cwd, "package.json")) || existsSync(join5(cwd, "tsconfig.json")) || existsSync(join5(cwd, "go.mod")) || existsSync(join5(cwd, "pyproject.toml")) || existsSync(join5(cwd, "setup.py")) || existsSync(join5(cwd, ".git"))) {
1037
1221
  projectRootToConnect = detectedRoot;
1038
1222
  }
1039
1223
  }
@@ -1090,8 +1274,8 @@ program.command("docs").description("Generate comprehensive codebase documentati
1090
1274
  trackCommand("docs", packageJson.version);
1091
1275
  const startTime = Date.now();
1092
1276
  try {
1093
- const projectRoot = directory ? resolve2(directory) : findProjectRoot();
1094
- const outputDir = options.output ? resolve2(options.output) : join4(projectRoot, ".depwire");
1277
+ const projectRoot = directory ? resolve4(directory) : findProjectRoot();
1278
+ const outputDir = options.output ? resolve4(options.output) : join5(projectRoot, ".depwire");
1095
1279
  const includeList = options.include.split(",").map((s) => s.trim());
1096
1280
  const onlyList = options.only ? options.only.split(",").map((s) => s.trim()) : void 0;
1097
1281
  if (options.gitignore === void 0 && !existsSyncNode(outputDir)) {
@@ -1153,16 +1337,16 @@ async function promptGitignore() {
1153
1337
  input: process.stdin,
1154
1338
  output: process.stdout
1155
1339
  });
1156
- return new Promise((resolve3) => {
1340
+ return new Promise((resolve5) => {
1157
1341
  rl.question("Add .depwire/ to .gitignore? [Y/n] ", (answer) => {
1158
1342
  rl.close();
1159
1343
  const normalized = answer.trim().toLowerCase();
1160
- resolve3(normalized === "" || normalized === "y" || normalized === "yes");
1344
+ resolve5(normalized === "" || normalized === "y" || normalized === "yes");
1161
1345
  });
1162
1346
  });
1163
1347
  }
1164
1348
  function addToGitignore(projectRoot, pattern) {
1165
- const gitignorePath = join4(projectRoot, ".gitignore");
1349
+ const gitignorePath = join5(projectRoot, ".gitignore");
1166
1350
  try {
1167
1351
  let content = "";
1168
1352
  if (existsSyncNode(gitignorePath)) {
@@ -1187,7 +1371,7 @@ ${pattern}
1187
1371
  program.command("health").description("Analyze dependency architecture health (0-100 score)").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--json", "Output as JSON").option("--verbose", "Show detailed breakdown").action(async (directory, options) => {
1188
1372
  trackCommand("health", packageJson.version);
1189
1373
  try {
1190
- const projectRoot = directory ? resolve2(directory) : findProjectRoot();
1374
+ const projectRoot = directory ? resolve4(directory) : findProjectRoot();
1191
1375
  const startTime = Date.now();
1192
1376
  const parsedFiles = await parseProject(projectRoot);
1193
1377
  const graph = buildGraph(parsedFiles);
@@ -1211,7 +1395,7 @@ program.command("health").description("Analyze dependency architecture health (0
1211
1395
  program.command("dead-code").description("Identify dead code - symbols defined but never referenced").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--confidence <level>", "Minimum confidence level to show: high, medium, low (default: medium)", "medium").option("--json", "Output as JSON (for CI/automation)").option("--verbose", "Show detailed info for each dead symbol").option("--stats", "Show summary statistics").option("--include-tests", "Include test files in analysis").option("--include-low", "Shortcut for --confidence low").option("--debug", "Show debug information (exclusion stats)").action(async (directory, options) => {
1212
1396
  trackCommand("dead-code", packageJson.version);
1213
1397
  try {
1214
- const projectRoot = directory ? resolve2(directory) : findProjectRoot();
1398
+ const projectRoot = directory ? resolve4(directory) : findProjectRoot();
1215
1399
  const startTime = Date.now();
1216
1400
  const parsedFiles = await parseProject(projectRoot);
1217
1401
  const graph = buildGraph(parsedFiles);
@@ -1247,4 +1431,13 @@ program.command("whatif").description("Simulate architectural changes before tou
1247
1431
  process.exit(1);
1248
1432
  }
1249
1433
  });
1434
+ program.command("security").description("Scan codebase for security vulnerabilities (deterministic, no API key required)").argument("[directory]", "Project directory to scan (defaults to current directory or auto-detected project root)").option("--target <file>", "Scan a single file instead of the whole repo").option("--class <classes...>", "Only run specific vulnerability class checks").option("--format <format>", "Output format: table (default), json, sarif", "table").option("--fail-on <level>", "Exit with code 1 if findings at this severity or above").action(async (directory, options) => {
1435
+ trackCommand("security", packageJson.version);
1436
+ try {
1437
+ await securityCommand(directory || ".", options);
1438
+ } catch (err) {
1439
+ console.error("Error running security scan:", err);
1440
+ process.exit(1);
1441
+ }
1442
+ });
1250
1443
  program.parse();
@@ -4,11 +4,11 @@ import {
4
4
  startMcpServer,
5
5
  updateFileInGraph,
6
6
  watchProject
7
- } from "./chunk-ORGAO3HT.js";
7
+ } from "./chunk-RGD3YJYQ.js";
8
8
  import {
9
9
  buildGraph,
10
10
  parseProject
11
- } from "./chunk-QHVWDUSX.js";
11
+ } from "./chunk-DA5LWNJ4.js";
12
12
 
13
13
  // src/mcpb-entry.ts
14
14
  import { resolve } from "path";