@westbayberry/dg 2.0.10 → 2.0.11

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.
@@ -1,5 +1,6 @@
1
1
  import { readdirSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
+ import { gitIgnoredDirectories } from "./discovery.js";
3
4
  import { parseLockfilePackages } from "../verify/preflight.js";
4
5
  export const LOCKFILE_ECOSYSTEMS = {
5
6
  "package-lock.json": "npm",
@@ -49,11 +50,15 @@ function lockfilesPerEcosystem(entries) {
49
50
  }
50
51
  return matches;
51
52
  }
52
- function shouldDescend(entry) {
53
- return entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name) && !entry.name.startsWith(".");
53
+ function shouldDescend(entry, directory, gitIgnored) {
54
+ return (entry.isDirectory() &&
55
+ !IGNORED_DIRECTORIES.has(entry.name) &&
56
+ !entry.name.startsWith(".") &&
57
+ !gitIgnored.has(join(directory, entry.name)));
54
58
  }
55
59
  export function discoverScanProjects(root) {
56
60
  const projects = [];
61
+ const gitIgnored = gitIgnoredDirectories(root);
57
62
  walk(root, 0);
58
63
  return projects;
59
64
  function walk(directory, depth) {
@@ -71,7 +76,7 @@ export function discoverScanProjects(root) {
71
76
  });
72
77
  }
73
78
  for (const entry of entries) {
74
- if (shouldDescend(entry)) {
79
+ if (shouldDescend(entry, directory, gitIgnored)) {
75
80
  walk(join(directory, entry.name), depth + 1);
76
81
  }
77
82
  }
@@ -79,6 +84,7 @@ export function discoverScanProjects(root) {
79
84
  }
80
85
  export async function discoverScanProjectsAsync(root, onProgress) {
81
86
  const projects = [];
87
+ const gitIgnored = gitIgnoredDirectories(root);
82
88
  let lastYield = Date.now();
83
89
  await walk(root, 0);
84
90
  return projects;
@@ -103,7 +109,7 @@ export async function discoverScanProjectsAsync(root, onProgress) {
103
109
  onProgress?.({ path: relativePath === "." ? depFile : `${relativePath}/${depFile}`, found: projects.length });
104
110
  }
105
111
  for (const entry of entries) {
106
- if (shouldDescend(entry)) {
112
+ if (shouldDescend(entry, directory, gitIgnored)) {
107
113
  await walk(join(directory, entry.name), depth + 1);
108
114
  }
109
115
  }
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { readdirSync, readFileSync, statSync } from "node:fs";
2
3
  import { basename, dirname, relative, resolve, sep } from "node:path";
3
4
  const IGNORED_DIRECTORIES = new Set([
@@ -75,10 +76,71 @@ function resolveStatus(counts) {
75
76
  }
76
77
  function discoverPackageManifests(root) {
77
78
  const manifests = [];
78
- walk(root, 0, manifests);
79
+ walk(root, 0, manifests, gitIgnoredDirectories(root));
79
80
  return manifests.sort((left, right) => displayPath(root, left).localeCompare(displayPath(root, right)));
80
81
  }
81
- function walk(directory, depth, manifests) {
82
+ export function gitIgnoredDirectories(root) {
83
+ const ignored = new Set();
84
+ if (!insideGitWorkTree(root)) {
85
+ return ignored;
86
+ }
87
+ let level = [root];
88
+ for (let depth = 0; depth <= MAX_DISCOVERY_DEPTH && level.length > 0; depth++) {
89
+ const candidates = [];
90
+ for (const directory of level) {
91
+ let entries;
92
+ try {
93
+ entries = readdirSync(directory, { withFileTypes: true });
94
+ }
95
+ catch {
96
+ continue;
97
+ }
98
+ for (const entry of entries) {
99
+ if (entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)) {
100
+ candidates.push(resolve(directory, entry.name));
101
+ }
102
+ }
103
+ }
104
+ if (candidates.length === 0) {
105
+ break;
106
+ }
107
+ const flagged = checkIgnoreBatch(root, candidates);
108
+ for (const directory of flagged) {
109
+ ignored.add(directory);
110
+ }
111
+ level = candidates.filter((directory) => !flagged.has(directory));
112
+ }
113
+ return ignored;
114
+ }
115
+ function insideGitWorkTree(root) {
116
+ try {
117
+ const out = execFileSync("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], {
118
+ encoding: "utf8",
119
+ timeout: 3000,
120
+ stdio: ["ignore", "pipe", "ignore"]
121
+ });
122
+ return out.trim() === "true";
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ function checkIgnoreBatch(root, directories) {
129
+ try {
130
+ const out = execFileSync("git", ["-C", root, "check-ignore", "--stdin", "-z"], {
131
+ input: directories.join("\0"),
132
+ encoding: "utf8",
133
+ timeout: 5000,
134
+ maxBuffer: 16 * 1024 * 1024,
135
+ stdio: ["pipe", "pipe", "ignore"]
136
+ });
137
+ return new Set(out.split("\0").filter(Boolean));
138
+ }
139
+ catch {
140
+ return new Set();
141
+ }
142
+ }
143
+ function walk(directory, depth, manifests, gitIgnored) {
82
144
  if (depth > MAX_DISCOVERY_DEPTH) {
83
145
  return;
84
146
  }
@@ -94,8 +156,8 @@ function walk(directory, depth, manifests) {
94
156
  for (const entry of entries) {
95
157
  const absolutePath = resolve(directory, entry.name);
96
158
  if (entry.isDirectory()) {
97
- if (!IGNORED_DIRECTORIES.has(entry.name)) {
98
- walk(absolutePath, depth + 1, manifests);
159
+ if (!IGNORED_DIRECTORIES.has(entry.name) && !gitIgnored.has(absolutePath)) {
160
+ walk(absolutePath, depth + 1, manifests, gitIgnored);
99
161
  }
100
162
  continue;
101
163
  }
@@ -1157,7 +1157,7 @@ export const InteractiveResultsView = ({ result, config: _config, durationMs, on
1157
1157
  : chalk.yellow;
1158
1158
  const arrow = level === "summary" ? "\u25BE" : "\u25B8"; // ▾ expanded, ▸ collapsed
1159
1159
  return (_jsxs(Box, { flexDirection: "column", children: [isCursor ? (_jsxs(Text, { backgroundColor: "#1a1a2e", wrap: "truncate-end", children: [chalk.cyan("\u258C"), " ", chalk.cyan(arrow), " ", ` `, color(pad(label, BADGE_COL)), chalk.bold(pad(truncate(names, nameCol - 2), nameCol)), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3)), " "] })) : (_jsxs(Text, { wrap: "truncate-end", children: [` ${chalk.dim(arrow)} `, color(pad(label, BADGE_COL)), pad(truncate(names, nameCol - 2), nameCol), lcColor(pad(lcStr, lcCol)), color(scoreStr.padStart(3))] })), level === "summary" && (_jsx(FindingsSummary, { group: group, maxWidth: innerWidth - 8, maxLines: group.key === view.expandedKey ? animVisibleLines : undefined }))] }, group.key));
1160
- }), belowCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", belowCount, " more below"] }))] })] })), searchQuery && groups.length === 0 && (_jsx(Text, { dimColor: true, children: ` No packages match "${searchQuery}"` })), !compact && _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [discoveredTotal !== undefined && discoveredTotal > total && (_jsxs(Text, { dimColor: true, children: ["Scanned ", total, " of ", discoveredTotal, " packages"] })), _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { wrap: "truncate-end", children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] })] }), !compact && _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), searchMode ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("\u2588"), " ", chalk.dim("Esc clear")] })) : searchQuery ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.bold.cyan("/"), " ", searchQuery, " ", chalk.dim(`${matchCount} of ${total} packages`), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("clear"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] })) : (_jsxs(Text, { wrap: "truncate-end", children: [" ", groups.length > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("\u23CE"), " ", chalk.dim("expand"), " "] })), total > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("/"), " ", chalk.dim("search"), " "] })), chalk.bold.cyan("l"), " ", chalk.dim("licenses"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", onBack && _jsxs(_Fragment, { children: [chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " "] }), chalk.bold.cyan("q"), " ", chalk.dim("quit"), " ", chalk.dim("\u00B7 Ctrl+C or q to exit"), exportMsg && _jsxs(_Fragment, { children: [" ", chalk.green(exportMsg)] })] }))] }));
1160
+ }), belowCount > 0 && (_jsxs(Text, { dimColor: true, children: [chalk.cyan(" \u2193"), " ", belowCount, " more below"] }))] })] })), searchQuery && groups.length === 0 && (_jsx(Text, { dimColor: true, children: ` No packages match "${searchQuery}"` })), !compact && _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, width: "100%", children: [discoveredTotal !== undefined && discoveredTotal > total && (_jsxs(Text, { dimColor: true, children: ["Scanned ", total, " of ", discoveredTotal, " packages"] })), _jsxs(Box, { justifyContent: "space-between", children: [clean.length > 0 ? (_jsxs(Text, { wrap: "truncate-end", children: [chalk.green("\u2713"), " ", chalk.green.bold(String(clean.length)), " ", chalk.dim(`package${clean.length !== 1 ? "s" : ""} passed`), " ", chalk.dim(`\u00b7 ${(durationMs / 1000).toFixed(1)}s`)] })) : (_jsxs(Text, { dimColor: true, children: [(durationMs / 1000).toFixed(1), "s"] })), result.freeScansRemaining !== undefined && (_jsx(Text, { dimColor: true, children: "Free tier \u00B7 dg login for higher scan limits" }))] })] }), !compact && _jsx(Text, { dimColor: true, children: chalk.dim("\u2500".repeat(Math.max(20, termCols - 4))) }), searchMode ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.bold.cyan("/"), " ", searchQuery, chalk.cyan("\u2588"), " ", chalk.dim("Esc clear")] })) : exportMsg ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.green(exportMsg)] })) : searchQuery ? (_jsxs(Text, { wrap: "truncate-end", children: [" ", chalk.bold.cyan("/"), " ", searchQuery, " ", chalk.dim(`${matchCount} of ${total} packages`), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("clear"), " ", chalk.bold.cyan("q"), " ", chalk.dim("quit")] })) : (_jsxs(Text, { wrap: "truncate-end", children: [" ", groups.length > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("\u2191\u2193"), " ", chalk.dim("navigate"), " ", chalk.bold.cyan("\u23CE"), " ", chalk.dim("expand"), " "] })), total > 0 && (_jsxs(_Fragment, { children: [chalk.bold.cyan("/"), " ", chalk.dim("search"), " "] })), chalk.bold.cyan("l"), " ", chalk.dim("licenses"), " ", chalk.bold.cyan("e"), " ", chalk.dim("export"), " ", onBack && _jsxs(_Fragment, { children: [chalk.bold.cyan("Esc"), " ", chalk.dim("back"), " "] }), chalk.bold.cyan("q"), " ", chalk.dim("quit")] }))] }));
1161
1161
  };
1162
1162
  const T = {
1163
1163
  branch: chalk.dim("\u251C\u2500\u2500"),
@@ -31,11 +31,14 @@ export async function launchScanTui(initialView = "results") {
31
31
  ? `Update available: ${update.current} → ${update.latest} · run dg update`
32
32
  : undefined;
33
33
  enterTui();
34
+ const instance = render(react.default.createElement(app.App, { config, initialView, updateAvailable }), { exitOnCtrlC: true });
35
+ const clearStaleFrameOnResize = () => instance.clear();
36
+ process.stdout.on("resize", clearStaleFrameOnResize);
34
37
  try {
35
- const instance = render(react.default.createElement(app.App, { config, initialView, updateAvailable }), { exitOnCtrlC: true });
36
38
  await instance.waitUntilExit();
37
39
  }
38
40
  finally {
41
+ process.stdout.off("resize", clearStaleFrameOnResize);
39
42
  leaveTui();
40
43
  }
41
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westbayberry/dg",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "description": "Dependency Guardian supply-chain firewall CLI",
5
5
  "type": "module",
6
6
  "bin": {