Skip to content

Commit 1bc7191

Browse files
committed
feat: add cli implementation
1 parent 7fba424 commit 1bc7191

File tree

11 files changed

+201
-27
lines changed

11 files changed

+201
-27
lines changed

.changeset/add-cli-entrypoint.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@snutsjs/core": minor
3+
---
4+
5+
Add a real CLI entrypoint so the package can be executed with `npx @snutsjs/core watch .`.

README.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
# snuts.js-core
1+
# @snutsjs/core
22

33
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
44

55
## 🎯 Goal
66

7-
`snuts.js-core` aims to be a robust and extensible static analysis tool designed to identify and report common "smells" or anti-patterns in JavaScript and TypeScript test files. By integrating with your development workflow, it helps maintain high-quality, readable, and effective test suites.
7+
`@snutsjs/core` aims to be a robust and extensible static analysis tool designed to identify and report common "smells" or anti-patterns in JavaScript and TypeScript test files. By integrating with your development workflow, it helps maintain high-quality, readable, and effective test suites.
88

99
## 📦 Installation
1010

1111
```bash
12-
npm install snuts.js-core
12+
npm install @snutsjs/core
1313
```
1414

1515
## 🚀 Library Usage (Extension-Friendly)
1616

1717
```ts
18-
import { DetectorRunner, detectors } from "snuts.js-core";
18+
import { DetectorRunner, detectors } from "@snutsjs/core";
1919

2020
const detectorInstances = Object.values(detectors).map((DetectorClass) => new DetectorClass());
2121
const runner = new DetectorRunner(detectorInstances);
@@ -29,11 +29,23 @@ console.log(smells);
2929
Use the runtime subpath when you want the side-effectful watcher behavior:
3030

3131
```ts
32-
import "snuts.js-core/runtime/watch";
32+
import "@snutsjs/core/runtime/watch";
3333
```
3434

3535
The root package import is side-effect free and safe for VS Code extension integration.
3636

37+
## CLI Usage
38+
39+
```bash
40+
npx @snutsjs/core watch .
41+
```
42+
43+
You can also point to a specific directory:
44+
45+
```bash
46+
npx @snutsjs/core watch src
47+
```
48+
3749
---
3850

3951
## 📁 Project Structure
@@ -148,7 +160,7 @@ To run this project, you will need:
148160
yarn start
149161
```
150162

151-
`snuts.js-core` will automatically watch all files in the current directory and its subdirectories and report findings.
163+
`@snutsjs/core` will automatically watch all files in the selected directory and its subdirectories and report findings.
152164

153165
### 📚 Build and Validate
154166

bin/snuts.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env node
2+
3+
import("../dist/bin/cli.js")
4+
.then(({ runCli }) => runCli(process.argv.slice(2)))
5+
.then((exitCode) => {
6+
process.exitCode = exitCode;
7+
})
8+
.catch((error) => {
9+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
10+
process.stderr.write(`${message}\n`);
11+
process.exit(1);
12+
});

eslint.config.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,20 @@ export default tseslint.config(
4141
"eslint.config.js",
4242
"prettier.config.js",
4343
"vite.config.ts",
44-
"dist",
45-
"node_modules",
46-
"coverage",
47-
"bin/",
44+
"dist/**",
45+
"node_modules/**",
46+
"coverage/**",
47+
"bin/**",
48+
"dumb/**",
4849
],
4950
},
5051
{
5152
files: ["**/*.js"], // Apply this configuration to all .js files
5253
languageOptions: {
5354
parser: babelParser, // Use the imported babel parser
55+
globals: {
56+
process: "readonly",
57+
},
5458
parserOptions: {
5559
requireConfigFile: false, // Allow parsing without a Babel config file
5660
babelOptions: {

lib/bin/cli-args.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export type ParsedCommand =
2+
| {
3+
type: "help";
4+
}
5+
| {
6+
type: "watch";
7+
paths: string[];
8+
}
9+
| {
10+
type: "error";
11+
message: string;
12+
};
13+
14+
export function usageText(): string {
15+
return [
16+
"Usage:",
17+
" snuts watch [path]",
18+
"",
19+
"Examples:",
20+
" npx @snutsjs/core watch .",
21+
" npx @snutsjs/core watch src",
22+
].join("\n");
23+
}
24+
25+
export function parseCliArgs(argv: string[]): ParsedCommand {
26+
const [command, ...rest] = argv;
27+
28+
if (!command || command === "help" || command === "--help" || command === "-h") {
29+
return { type: "help" };
30+
}
31+
32+
if (command === "watch") {
33+
return {
34+
type: "watch",
35+
paths: rest.length > 0 ? rest : ["."],
36+
};
37+
}
38+
39+
return {
40+
type: "error",
41+
message: `Unknown command: ${command}`,
42+
};
43+
}

lib/bin/cli.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { parseCliArgs } from "./cli-args";
2+
3+
describe("parseCliArgs", () => {
4+
it("returns help when no arguments are provided", () => {
5+
expect(parseCliArgs([])).toEqual({ type: "help" });
6+
});
7+
8+
it("returns help when help flag is provided", () => {
9+
expect(parseCliArgs(["--help"])).toEqual({ type: "help" });
10+
});
11+
12+
it("parses watch command with explicit paths", () => {
13+
expect(parseCliArgs(["watch", ".", "src"])).toEqual({
14+
type: "watch",
15+
paths: [".", "src"],
16+
});
17+
});
18+
19+
it("defaults watch command to current directory", () => {
20+
expect(parseCliArgs(["watch"])).toEqual({
21+
type: "watch",
22+
paths: ["."],
23+
});
24+
});
25+
26+
it("returns an error for unknown commands", () => {
27+
expect(parseCliArgs(["scan"])).toEqual({
28+
type: "error",
29+
message: "Unknown command: scan",
30+
});
31+
});
32+
});

lib/bin/cli.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import path from "node:path";
2+
import { pathToFileURL } from "node:url";
3+
4+
import { parseCliArgs, usageText } from "./cli-args";
5+
6+
import { startWatch } from "@/runtime/watch-runner";
7+
8+
function writeOutput(message: string, target: "stdout" | "stderr"): void {
9+
const content = `${message}\n`;
10+
11+
if (target === "stderr") {
12+
process.stderr.write(content);
13+
return;
14+
}
15+
16+
process.stdout.write(content);
17+
}
18+
19+
export async function runCli(argv: string[]): Promise<number> {
20+
const parsedCommand = parseCliArgs(argv);
21+
22+
if (parsedCommand.type === "help") {
23+
writeOutput(usageText(), "stdout");
24+
return 0;
25+
}
26+
27+
if (parsedCommand.type === "error") {
28+
writeOutput(parsedCommand.message, "stderr");
29+
writeOutput(usageText(), "stderr");
30+
return 1;
31+
}
32+
33+
const resolvedPaths = parsedCommand.paths.map((targetPath) =>
34+
path.resolve(process.cwd(), targetPath),
35+
);
36+
37+
await startWatch(resolvedPaths);
38+
return 0;
39+
}
40+
41+
const executedFileUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
42+
43+
if (import.meta.url === executedFileUrl) {
44+
void runCli(process.argv.slice(2)).then(
45+
(exitCode) => {
46+
process.exitCode = exitCode;
47+
},
48+
(error: unknown) => {
49+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
50+
process.stderr.write(`${message}\n`);
51+
process.exitCode = 1;
52+
},
53+
);
54+
}

lib/runtime/watch-runner.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Detector } from "../core/detector.interface";
2+
import { Watcher } from "../core/watcher";
3+
import * as detectorModule from "../detectors";
4+
5+
interface DetectorConstructor {
6+
new (): Detector;
7+
}
8+
9+
export function createDetectorInstances(): Detector[] {
10+
const detectorEntries = Object.values(detectorModule) as DetectorConstructor[];
11+
return detectorEntries.map((DetectorClass) => new DetectorClass());
12+
}
13+
14+
export async function startWatch(paths: string[]): Promise<void> {
15+
const watcher = new Watcher({
16+
paths,
17+
detectors: createDetectorInstances(),
18+
});
19+
20+
await watcher.watch();
21+
}

lib/runtime/watch.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,3 @@
1-
import type { Detector } from "../core/detector.interface";
2-
import { Watcher } from "../core/watcher";
3-
import * as detectorModule from "../detectors";
1+
import { startWatch } from "./watch-runner";
42

5-
interface DetectorConstructor {
6-
new (): Detector;
7-
}
8-
9-
const detectorEntries = Object.values(detectorModule) as DetectorConstructor[];
10-
const detectors = detectorEntries.map((DetectorClass) => new DetectorClass());
11-
12-
const watcher = new Watcher({
13-
paths: [process.cwd()],
14-
detectors,
15-
});
16-
17-
void watcher.watch();
3+
void startWatch([process.cwd()]);

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
"main": "./dist/index.js",
77
"module": "./dist/index.js",
88
"types": "./dist/index.d.ts",
9+
"bin": {
10+
"snuts": "./bin/snuts.js"
11+
},
912
"exports": {
1013
".": {
1114
"types": "./dist/index.d.ts",
@@ -22,6 +25,7 @@
2225
"./package.json": "./package.json"
2326
},
2427
"files": [
28+
"bin",
2529
"dist",
2630
"README.md",
2731
"LICENSE"
@@ -44,7 +48,7 @@
4448
"access": "public"
4549
},
4650
"scripts": {
47-
"start": "tsx lib/runtime/watch.ts",
51+
"start": "tsx lib/bin/cli.ts watch .",
4852
"clean": "rimraf dist",
4953
"build": "tsup --config tsup.config.ts",
5054
"typecheck": "tsc --noEmit -p tsconfig.build.json",

0 commit comments

Comments
 (0)