frontend-guardian 0.1.1

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/PUBLISH.md ADDED
@@ -0,0 +1,81 @@
1
+ # Publish frontend-guardian to npm
2
+
3
+ So anyone can run `npx frontend-guardian .` without your repo.
4
+
5
+ ## 1. npm account
6
+
7
+ - Sign up at https://www.npmjs.com/signup
8
+ - Login in terminal: `npm login` (username, password, email, OTP if 2FA)
9
+
10
+ ## 2. Check package name
11
+
12
+ - If **frontend-guardian** is taken on npm, use a scoped name in `package.json`:
13
+ `"name": "@YOUR_NPM_USERNAME/frontend-guardian"`
14
+ Then users run: `npx @YOUR_NPM_USERNAME/frontend-guardian .`
15
+
16
+ ## 3. Publish core first (CLI depends on it)
17
+
18
+ ```bash
19
+ cd C:/Users/ADMIN/Desktop/frontend-guardian
20
+ pnpm run build:packages
21
+ cd packages/core
22
+ ```
23
+
24
+ In `packages/core/package.json` temporarily remove the line `"private": true` (or set to `false`).
25
+
26
+ ```bash
27
+ npm publish --access public
28
+ ```
29
+
30
+ (Scoped packages like `@frontend-guardian/core` need `--access public`.)
31
+
32
+ Put `"private": true` back in core's package.json if you want to keep the repo from publishing it again by mistake.
33
+
34
+ ## 4. Point CLI to published core
35
+
36
+ In `packages/cli/package.json`, change:
37
+
38
+ ```json
39
+ "dependencies": {
40
+ "@frontend-guardian/core": "workspace:*"
41
+ }
42
+ ```
43
+
44
+ to:
45
+
46
+ ```json
47
+ "dependencies": {
48
+ "@frontend-guardian/core": "^0.1.0"
49
+ }
50
+ ```
51
+
52
+ (Use the same version you published for core, e.g. `^0.1.0`.)
53
+
54
+ ## 5. Publish CLI
55
+
56
+ ```bash
57
+ cd C:/Users/ADMIN/Desktop/frontend-guardian/packages/cli
58
+ npm publish
59
+ ```
60
+
61
+ If the name is scoped (`@YOUR_NPM_USERNAME/frontend-guardian`):
62
+
63
+ ```bash
64
+ npm publish --access public
65
+ ```
66
+
67
+ ## 6. After publish
68
+
69
+ Anyone can run:
70
+
71
+ ```bash
72
+ npx frontend-guardian .
73
+ ```
74
+
75
+ (or `npx @YOUR_NPM_USERNAME/frontend-guardian .` if you used a scoped name.)
76
+
77
+ ## Updating later
78
+
79
+ 1. Bump `version` in `packages/core/package.json` and `packages/cli/package.json`.
80
+ 2. In core: `npm publish --access public`
81
+ 3. In cli: set `"@frontend-guardian/core": "^0.1.1"` (or new version), then `npm publish` (or `--access public` if scoped).
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # frontend-guardian (CLI)
2
+
3
+ Phase 1 CLI: scan a folder or ZIP for Tailwind & component consistency.
4
+
5
+ ## Local (from repo)
6
+
7
+ ```bash
8
+ # Build packages first (once after clone)
9
+ pnpm run build:packages
10
+
11
+ # Run CLI
12
+ pnpm cli <path>
13
+ pnpm cli .
14
+ pnpm cli ./src
15
+ pnpm cli project.zip
16
+ ```
17
+
18
+ Or run the built binary:
19
+
20
+ ```bash
21
+ node packages/cli/dist/cli.js .
22
+ ```
23
+
24
+ ## Use globally (from repo, no publish)
25
+
26
+ From repo root, after `pnpm run build:packages`:
27
+
28
+ ```bash
29
+ cd packages/cli
30
+ npm link
31
+ ```
32
+
33
+ Then run `frontend-guardian .` from any folder. (On Windows/Git Bash, if `pnpm link --global` fails with PATH, use `npm link` instead.)
34
+
35
+ ## Publish to npm
36
+
37
+ From repo root:
38
+
39
+ ```bash
40
+ pnpm run build:packages
41
+ cd packages/cli
42
+ npm publish
43
+ ```
44
+
45
+ Then anyone can run:
46
+
47
+ ```bash
48
+ npx frontend-guardian .
49
+ npx frontend-guardian ./src
50
+ npx frontend-guardian project.zip
51
+ ```
52
+
53
+ ## Output
54
+
55
+ - Score (0–100)
56
+ - Warnings (spacing, radius, colors, buttons, unused imports, parse errors)
57
+ - Duplicate components (same JSX structure)
package/dist/cli.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { scanFiles, scanZip } from "@justinmoto/frontend-guardian-core";
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const EXT = [".js", ".jsx", ".ts", ".tsx"];
10
+ const IGNORE = ["node_modules", ".next", "dist", "build", ".git"];
11
+ function isCodeFile(name) {
12
+ return EXT.some((e) => name.toLowerCase().endsWith(e));
13
+ }
14
+ function isSourcePath(relativePath) {
15
+ const normalized = relativePath.replace(/\\/g, "/");
16
+ return !IGNORE.some((part) => normalized.includes(part));
17
+ }
18
+ function readDirRecursive(dir, base = "") {
19
+ const out = [];
20
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
21
+ for (const e of entries) {
22
+ const rel = base ? `${base}/${e.name}` : e.name;
23
+ const full = path.join(dir, e.name);
24
+ if (e.isDirectory()) {
25
+ if (IGNORE.includes(e.name))
26
+ continue;
27
+ out.push(...readDirRecursive(full, rel));
28
+ }
29
+ else if (e.isFile() && isCodeFile(e.name) && isSourcePath(rel)) {
30
+ try {
31
+ const code = fs.readFileSync(full, "utf-8");
32
+ out.push({ path: rel, code });
33
+ }
34
+ catch {
35
+ // skip
36
+ }
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ function issueType(w) {
42
+ const msg = w.message;
43
+ if (msg.startsWith("Mixed spacing values:"))
44
+ return { title: "Mixed Spacing", detected: msg.replace(/^Mixed spacing values:\s*/, "") };
45
+ if (msg.startsWith("Inconsistent border-radius:"))
46
+ return { title: "Inconsistent Border Radius", detected: msg.replace(/^Inconsistent border-radius:\s*/, "") };
47
+ if (msg.startsWith("Arbitrary color"))
48
+ return { title: "Arbitrary Colors", detected: msg };
49
+ if (msg.startsWith("Unused import:"))
50
+ return { title: "Unused Imports", detected: msg.replace(/^Unused import:\s*/, "") };
51
+ if (msg.startsWith("Parse error"))
52
+ return { title: "Parse Error", detected: msg };
53
+ if (msg.startsWith("Mixed button padding:"))
54
+ return { title: "Mixed Button Padding", detected: msg.replace(/^Mixed button padding:\s*/, "") };
55
+ if (msg.startsWith("Inconsistent button border-radius:"))
56
+ return { title: "Inconsistent Button Border Radius", detected: msg.replace(/^Inconsistent button border-radius:\s*/, "") };
57
+ return { title: "Other", detected: msg };
58
+ }
59
+ function scoreColor(score) {
60
+ if (score >= 70)
61
+ return chalk.green;
62
+ if (score >= 40)
63
+ return chalk.yellow;
64
+ return chalk.red;
65
+ }
66
+ const SHORT_SUGGESTION = {
67
+ "Mixed Spacing": "standardize spacing scale",
68
+ "Inconsistent Border Radius": "use one border radius token",
69
+ "Arbitrary Colors": "use Tailwind theme colors",
70
+ "Unused Imports": "remove unused imports",
71
+ "Parse Error": "fix syntax or remove file",
72
+ "Mixed Button Padding": "use one button padding",
73
+ "Inconsistent Button Border Radius": "use one radius for all buttons",
74
+ "Duplicate Components": "extract shared component and reuse",
75
+ };
76
+ function shortSuggestion(title, full) {
77
+ return SHORT_SUGGESTION[title] ?? (full.slice(0, 55) + (full.length > 55 ? "…" : ""));
78
+ }
79
+ const WRAP_WIDTH = 72;
80
+ const INDENT = " ";
81
+ function wrapLine(prefix, text) {
82
+ const maxLen = WRAP_WIDTH - prefix.length;
83
+ if (text.length <= maxLen) {
84
+ console.log(prefix + text);
85
+ return;
86
+ }
87
+ let rest = text;
88
+ let first = true;
89
+ while (rest.length > 0) {
90
+ const use = first ? prefix : INDENT;
91
+ const allowed = WRAP_WIDTH - use.length;
92
+ if (rest.length <= allowed) {
93
+ console.log(use + rest);
94
+ break;
95
+ }
96
+ let breakAt = rest.slice(0, allowed).lastIndexOf(", ") + 1 || rest.slice(0, allowed).lastIndexOf("; ") + 1;
97
+ if (breakAt <= 0)
98
+ breakAt = allowed;
99
+ const chunk = rest.slice(0, breakAt).trim();
100
+ rest = rest.slice(breakAt).trim();
101
+ if (chunk)
102
+ console.log(use + chunk);
103
+ first = false;
104
+ }
105
+ }
106
+ function printResult(projectName, result) {
107
+ const line = chalk.gray("━".repeat(28));
108
+ console.log("\n" + line);
109
+ console.log(chalk.bold(" 🛡 Frontend Guardian"));
110
+ console.log(line + "\n");
111
+ let filesScanned = "filesScanned" in result ? result.filesScanned : 0;
112
+ if (filesScanned === 0 && (result.warnings.length > 0 || result.duplicates.length > 0)) {
113
+ const files = new Set();
114
+ result.warnings.forEach((w) => files.add(w.file));
115
+ result.duplicates.forEach((d) => d.files.forEach((f) => files.add(f)));
116
+ filesScanned = files.size;
117
+ }
118
+ console.log(" Project: " + projectName);
119
+ console.log(" Files scanned: " + String(filesScanned));
120
+ const scoreColorFn = scoreColor(result.score);
121
+ console.log(" Consistency Score: " + scoreColorFn(result.score + " / 100"));
122
+ console.log(chalk.gray(" (each warning −5, each duplicate group −10, from 100)") + "\n");
123
+ const byTitle = new Map();
124
+ for (const w of result.warnings) {
125
+ const { title, detected } = issueType(w);
126
+ const sug = w.suggestion ?? "";
127
+ if (!byTitle.has(title))
128
+ byTitle.set(title, { detected: [], suggestion: sug });
129
+ const entry = byTitle.get(title);
130
+ if (!entry.detected.includes(detected))
131
+ entry.detected.push(detected);
132
+ }
133
+ for (const d of result.duplicates) {
134
+ const title = "Duplicate Components";
135
+ const detected = d.files.join(", ");
136
+ if (!byTitle.has(title))
137
+ byTitle.set(title, { detected: [], suggestion: d.suggestion ?? "" });
138
+ byTitle.get(title).detected.push(detected);
139
+ }
140
+ const allIssues = [...byTitle.entries()].map(([title, { detected, suggestion }]) => ({
141
+ title,
142
+ detected: detected.join("; "),
143
+ suggestion: shortSuggestion(title, suggestion),
144
+ }));
145
+ if (allIssues.length > 0) {
146
+ console.log(chalk.yellow(" ⚠ ") + chalk.bold("Issues Found") + "\n");
147
+ allIssues.forEach((issue, i) => {
148
+ console.log(" " + (i + 1) + ". " + issue.title);
149
+ if (issue.title === "Unused Imports") {
150
+ wrapLine(INDENT, issue.detected);
151
+ }
152
+ else {
153
+ wrapLine(INDENT + "Detected: ", issue.detected);
154
+ console.log(INDENT + "Suggestion: " + issue.suggestion);
155
+ }
156
+ if (i < allIssues.length - 1)
157
+ console.log("");
158
+ });
159
+ console.log("");
160
+ }
161
+ console.log(line);
162
+ console.log(chalk.bold(" Scan Complete") + "\n");
163
+ }
164
+ async function main() {
165
+ const arg = process.argv[2];
166
+ if (!arg) {
167
+ console.error("Usage: frontend-guardian <path>");
168
+ console.error(" npx frontend-guardian .");
169
+ console.error(" npx frontend-guardian ./src");
170
+ console.error(" npx frontend-guardian project.zip");
171
+ process.exit(1);
172
+ }
173
+ const resolved = path.resolve(process.cwd(), arg);
174
+ if (!fs.existsSync(resolved)) {
175
+ console.error("Path not found:", resolved);
176
+ process.exit(1);
177
+ }
178
+ const projectName = path.basename(resolved).replace(/\.zip$/i, "") || "project";
179
+ const stat = fs.statSync(resolved);
180
+ let result;
181
+ if (stat.isDirectory()) {
182
+ const files = readDirRecursive(resolved);
183
+ if (files.length === 0) {
184
+ console.error("No .js/.jsx/.ts/.tsx files found (or all under node_modules/.next/dist/build).");
185
+ process.exit(1);
186
+ }
187
+ const spinner = ora("Scanning " + files.length + " file(s)...").start();
188
+ try {
189
+ result = scanFiles(files);
190
+ spinner.succeed("Scan complete.");
191
+ }
192
+ catch (e) {
193
+ spinner.fail("Scan failed.");
194
+ throw e;
195
+ }
196
+ }
197
+ else if (resolved.toLowerCase().endsWith(".zip")) {
198
+ const spinner = ora("Scanning ZIP...").start();
199
+ try {
200
+ const buf = fs.readFileSync(resolved);
201
+ result = await scanZip(buf);
202
+ spinner.succeed("Scan complete.");
203
+ }
204
+ catch (e) {
205
+ spinner.fail("Scan failed.");
206
+ throw e;
207
+ }
208
+ }
209
+ else {
210
+ console.error("Provide a folder path or a .zip file.");
211
+ process.exit(1);
212
+ }
213
+ printResult(projectName, result);
214
+ }
215
+ main().catch((err) => {
216
+ console.error(err);
217
+ process.exit(1);
218
+ });
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "frontend-guardian",
3
+ "version": "0.1.1",
4
+ "description": "Scan frontend projects for Tailwind & component consistency (Phase 1 CLI)",
5
+ "type": "module",
6
+ "bin": {
7
+ "frontend-guardian": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc"
11
+ },
12
+ "dependencies": {
13
+ "@justinmoto/frontend-guardian-core": "^0.1.2",
14
+ "chalk": "^5.3.0",
15
+ "ora": "^8.0.1"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "^5"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "keywords": ["tailwind", "consistency", "frontend", "scan", "cli"],
24
+ "license": "MIT"
25
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { scanFiles, scanZip, type ScanResult, type Warning } from "@justinmoto/frontend-guardian-core";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const EXT = [".js", ".jsx", ".ts", ".tsx"];
11
+ const IGNORE = ["node_modules", ".next", "dist", "build", ".git"];
12
+
13
+ function isCodeFile(name: string): boolean {
14
+ return EXT.some((e) => name.toLowerCase().endsWith(e));
15
+ }
16
+
17
+ function isSourcePath(relativePath: string): boolean {
18
+ const normalized = relativePath.replace(/\\/g, "/");
19
+ return !IGNORE.some((part) => normalized.includes(part));
20
+ }
21
+
22
+ function readDirRecursive(dir: string, base = ""): { path: string; code: string }[] {
23
+ const out: { path: string; code: string }[] = [];
24
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
25
+ for (const e of entries) {
26
+ const rel = base ? `${base}/${e.name}` : e.name;
27
+ const full = path.join(dir, e.name);
28
+ if (e.isDirectory()) {
29
+ if (IGNORE.includes(e.name)) continue;
30
+ out.push(...readDirRecursive(full, rel));
31
+ } else if (e.isFile() && isCodeFile(e.name) && isSourcePath(rel)) {
32
+ try {
33
+ const code = fs.readFileSync(full, "utf-8");
34
+ out.push({ path: rel, code });
35
+ } catch {
36
+ // skip
37
+ }
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function issueType(w: Warning): { title: string; detected: string } {
44
+ const msg = w.message;
45
+ if (msg.startsWith("Mixed spacing values:"))
46
+ return { title: "Mixed Spacing", detected: msg.replace(/^Mixed spacing values:\s*/, "") };
47
+ if (msg.startsWith("Inconsistent border-radius:"))
48
+ return { title: "Inconsistent Border Radius", detected: msg.replace(/^Inconsistent border-radius:\s*/, "") };
49
+ if (msg.startsWith("Arbitrary color"))
50
+ return { title: "Arbitrary Colors", detected: msg };
51
+ if (msg.startsWith("Unused import:"))
52
+ return { title: "Unused Imports", detected: msg.replace(/^Unused import:\s*/, "") };
53
+ if (msg.startsWith("Parse error"))
54
+ return { title: "Parse Error", detected: msg };
55
+ if (msg.startsWith("Mixed button padding:"))
56
+ return { title: "Mixed Button Padding", detected: msg.replace(/^Mixed button padding:\s*/, "") };
57
+ if (msg.startsWith("Inconsistent button border-radius:"))
58
+ return { title: "Inconsistent Button Border Radius", detected: msg.replace(/^Inconsistent button border-radius:\s*/, "") };
59
+ return { title: "Other", detected: msg };
60
+ }
61
+
62
+ function scoreColor(score: number): (s: string) => string {
63
+ if (score >= 70) return chalk.green;
64
+ if (score >= 40) return chalk.yellow;
65
+ return chalk.red;
66
+ }
67
+
68
+ const SHORT_SUGGESTION: Record<string, string> = {
69
+ "Mixed Spacing": "standardize spacing scale",
70
+ "Inconsistent Border Radius": "use one border radius token",
71
+ "Arbitrary Colors": "use Tailwind theme colors",
72
+ "Unused Imports": "remove unused imports",
73
+ "Parse Error": "fix syntax or remove file",
74
+ "Mixed Button Padding": "use one button padding",
75
+ "Inconsistent Button Border Radius": "use one radius for all buttons",
76
+ "Duplicate Components": "extract shared component and reuse",
77
+ };
78
+
79
+ function shortSuggestion(title: string, full: string): string {
80
+ return SHORT_SUGGESTION[title] ?? (full.slice(0, 55) + (full.length > 55 ? "…" : ""));
81
+ }
82
+
83
+ const WRAP_WIDTH = 72;
84
+ const INDENT = " ";
85
+
86
+ function wrapLine(prefix: string, text: string): void {
87
+ const maxLen = WRAP_WIDTH - prefix.length;
88
+ if (text.length <= maxLen) {
89
+ console.log(prefix + text);
90
+ return;
91
+ }
92
+ let rest = text;
93
+ let first = true;
94
+ while (rest.length > 0) {
95
+ const use = first ? prefix : INDENT;
96
+ const allowed = WRAP_WIDTH - use.length;
97
+ if (rest.length <= allowed) {
98
+ console.log(use + rest);
99
+ break;
100
+ }
101
+ let breakAt = rest.slice(0, allowed).lastIndexOf(", ") + 1 || rest.slice(0, allowed).lastIndexOf("; ") + 1;
102
+ if (breakAt <= 0) breakAt = allowed;
103
+ const chunk = rest.slice(0, breakAt).trim();
104
+ rest = rest.slice(breakAt).trim();
105
+ if (chunk) console.log(use + chunk);
106
+ first = false;
107
+ }
108
+ }
109
+
110
+ function printResult(projectName: string, result: ScanResult) {
111
+ const line = chalk.gray("━".repeat(28));
112
+ console.log("\n" + line);
113
+ console.log(chalk.bold(" 🛡 Frontend Guardian"));
114
+ console.log(line + "\n");
115
+
116
+ let filesScanned = "filesScanned" in result ? (result as ScanResult & { filesScanned?: number }).filesScanned : 0;
117
+ if (filesScanned === 0 && (result.warnings.length > 0 || result.duplicates.length > 0)) {
118
+ const files = new Set<string>();
119
+ result.warnings.forEach((w) => files.add(w.file));
120
+ result.duplicates.forEach((d) => d.files.forEach((f) => files.add(f)));
121
+ filesScanned = files.size;
122
+ }
123
+ console.log(" Project: " + projectName);
124
+ console.log(" Files scanned: " + String(filesScanned));
125
+ const scoreColorFn = scoreColor(result.score);
126
+ console.log(" Consistency Score: " + scoreColorFn(result.score + " / 100"));
127
+ console.log(chalk.gray(" (each warning −5, each duplicate group −10, from 100)") + "\n");
128
+
129
+ const byTitle = new Map<string, { detected: string[]; suggestion: string }>();
130
+ for (const w of result.warnings) {
131
+ const { title, detected } = issueType(w);
132
+ const sug = w.suggestion ?? "";
133
+ if (!byTitle.has(title)) byTitle.set(title, { detected: [], suggestion: sug });
134
+ const entry = byTitle.get(title)!;
135
+ if (!entry.detected.includes(detected)) entry.detected.push(detected);
136
+ }
137
+ for (const d of result.duplicates) {
138
+ const title = "Duplicate Components";
139
+ const detected = d.files.join(", ");
140
+ if (!byTitle.has(title)) byTitle.set(title, { detected: [], suggestion: d.suggestion ?? "" });
141
+ byTitle.get(title)!.detected.push(detected);
142
+ }
143
+
144
+ const allIssues = [...byTitle.entries()].map(([title, { detected, suggestion }]) => ({
145
+ title,
146
+ detected: detected.join("; "),
147
+ suggestion: shortSuggestion(title, suggestion),
148
+ }));
149
+
150
+ if (allIssues.length > 0) {
151
+ console.log(chalk.yellow(" ⚠ ") + chalk.bold("Issues Found") + "\n");
152
+ allIssues.forEach((issue, i) => {
153
+ console.log(" " + (i + 1) + ". " + issue.title);
154
+ if (issue.title === "Unused Imports") {
155
+ wrapLine(INDENT, issue.detected);
156
+ } else {
157
+ wrapLine(INDENT + "Detected: ", issue.detected);
158
+ console.log(INDENT + "Suggestion: " + issue.suggestion);
159
+ }
160
+ if (i < allIssues.length - 1) console.log("");
161
+ });
162
+ console.log("");
163
+ }
164
+
165
+ console.log(line);
166
+ console.log(chalk.bold(" Scan Complete") + "\n");
167
+ }
168
+
169
+ async function main() {
170
+ const arg = process.argv[2];
171
+ if (!arg) {
172
+ console.error("Usage: frontend-guardian <path>");
173
+ console.error(" npx frontend-guardian .");
174
+ console.error(" npx frontend-guardian ./src");
175
+ console.error(" npx frontend-guardian project.zip");
176
+ process.exit(1);
177
+ }
178
+ const resolved = path.resolve(process.cwd(), arg);
179
+ if (!fs.existsSync(resolved)) {
180
+ console.error("Path not found:", resolved);
181
+ process.exit(1);
182
+ }
183
+ const projectName = path.basename(resolved).replace(/\.zip$/i, "") || "project";
184
+
185
+ const stat = fs.statSync(resolved);
186
+ let result: ScanResult;
187
+
188
+ if (stat.isDirectory()) {
189
+ const files = readDirRecursive(resolved);
190
+ if (files.length === 0) {
191
+ console.error("No .js/.jsx/.ts/.tsx files found (or all under node_modules/.next/dist/build).");
192
+ process.exit(1);
193
+ }
194
+ const spinner = ora("Scanning " + files.length + " file(s)...").start();
195
+ try {
196
+ result = scanFiles(files);
197
+ spinner.succeed("Scan complete.");
198
+ } catch (e) {
199
+ spinner.fail("Scan failed.");
200
+ throw e;
201
+ }
202
+ } else if (resolved.toLowerCase().endsWith(".zip")) {
203
+ const spinner = ora("Scanning ZIP...").start();
204
+ try {
205
+ const buf = fs.readFileSync(resolved);
206
+ result = await scanZip(buf);
207
+ spinner.succeed("Scan complete.");
208
+ } catch (e) {
209
+ spinner.fail("Scan failed.");
210
+ throw e;
211
+ }
212
+ } else {
213
+ console.error("Provide a folder path or a .zip file.");
214
+ process.exit(1);
215
+ }
216
+
217
+ printResult(projectName, result);
218
+ }
219
+
220
+ main().catch((err) => {
221
+ console.error(err);
222
+ process.exit(1);
223
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["src/**/*.ts"],
12
+ "exclude": ["node_modules", "dist"]
13
+ }