sadist 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm version](https://img.shields.io/npm/v/sadist.svg)](https://www.npmjs.com/package/sadist)
5
5
  [![CI](https://github.com/ElBenjaMasLindo/sadist/actions/workflows/ci.yml/badge.svg)](https://github.com/ElBenjaMasLindo/sadist/actions/workflows/ci.yml)
6
6
 
7
- A merciless TypeScript code quality gate.
7
+ A sadistic TypeScript gate that holds your commit hostage until the code is no longer a liability.
8
8
 
9
9
  `sadist` combines strict `tsc` compilation, exhaustive pattern matching via
10
10
  `ts-pattern`, and a small set of architectural ESLint rules into a single
package/bin/sadist.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli/index.js");
@@ -0,0 +1,17 @@
1
+ import { type Option, type PkgJson, type Result } from "./types.js";
2
+ export declare function pathExists(rel: string): boolean;
3
+ export declare function readText(rel: string): Result<string, string>;
4
+ export declare function writeText(rel: string, content: string, mode?: number): Result<string, string>;
5
+ export declare function readPkg(): Result<PkgJson, string>;
6
+ export declare function coercePkg(value: unknown): Result<PkgJson, string>;
7
+ export type PkgSeed = {
8
+ root: Record<string, unknown>;
9
+ name: string;
10
+ version: string;
11
+ scripts: Record<string, unknown>;
12
+ };
13
+ export declare function requireString(v: Record<string, unknown>, key: string): Result<string, string>;
14
+ export declare function requireObject(v: Record<string, unknown>, key: string): Result<Record<string, unknown>, string>;
15
+ export declare function optString(v: unknown): Option<string>;
16
+ export declare function stringRecord(v: unknown): Record<string, string>;
17
+ export declare function hasDep(pkg: PkgJson, name: string): boolean;
@@ -0,0 +1,100 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { ok } from "./types.js";
4
+ export function pathExists(rel) {
5
+ return existsSync(resolve(process.cwd(), rel));
6
+ }
7
+ export function readText(rel) {
8
+ try {
9
+ const content = readFileSync(resolve(process.cwd(), rel), "utf8");
10
+ return { ok: true, value: content };
11
+ }
12
+ catch (e) {
13
+ return { ok: false, error: `cannot read ${rel}: ${String(e)}` };
14
+ }
15
+ }
16
+ export function writeText(rel, content, mode) {
17
+ try {
18
+ const fullPath = resolve(process.cwd(), rel);
19
+ mkdirSync(dirname(fullPath), { recursive: true });
20
+ if (typeof mode === "number") {
21
+ writeFileSync(fullPath, content, { encoding: "utf8", mode });
22
+ }
23
+ else {
24
+ writeFileSync(fullPath, content, "utf8");
25
+ }
26
+ return { ok: true, value: rel };
27
+ }
28
+ catch (e) {
29
+ return { ok: false, error: `cannot write ${rel}: ${String(e)}` };
30
+ }
31
+ }
32
+ export function readPkg() {
33
+ const raw = readText("package.json");
34
+ if (!raw.ok)
35
+ return raw;
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(raw.value);
39
+ }
40
+ catch (e) {
41
+ return { ok: false, error: `package.json is not valid JSON: ${String(e)}` };
42
+ }
43
+ return coercePkg(parsed);
44
+ }
45
+ export function coercePkg(value) {
46
+ if (typeof value !== "object" || value === null) {
47
+ return { ok: false, error: "package.json root must be an object" };
48
+ }
49
+ const v = value;
50
+ const name = requireString(v, "name");
51
+ if (!name.ok)
52
+ return name;
53
+ const version = requireString(v, "version");
54
+ if (!version.ok)
55
+ return version;
56
+ const scripts = requireObject(v, "scripts");
57
+ if (!scripts.ok)
58
+ return scripts;
59
+ return ok(buildPkg({ root: v, name: name.value, version: version.value, scripts: scripts.value }));
60
+ }
61
+ function buildPkg(seed) {
62
+ return {
63
+ name: seed.name,
64
+ version: seed.version,
65
+ type: optString(seed.root["type"]),
66
+ scripts: stringRecord(seed.scripts),
67
+ dependencies: stringRecord(seed.root["dependencies"] ?? {}),
68
+ devDependencies: stringRecord(seed.root["devDependencies"] ?? {}),
69
+ root: seed.root,
70
+ };
71
+ }
72
+ export function requireString(v, key) {
73
+ if (typeof v[key] !== "string") {
74
+ return { ok: false, error: `package.json missing string '${key}'` };
75
+ }
76
+ return { ok: true, value: v[key] };
77
+ }
78
+ export function requireObject(v, key) {
79
+ const val = v[key];
80
+ if (typeof val !== "object" || val === null) {
81
+ return { ok: false, error: `package.json missing '${key}' object` };
82
+ }
83
+ return { ok: true, value: val };
84
+ }
85
+ export function optString(v) {
86
+ return typeof v === "string" ? { some: true, value: v } : { some: false };
87
+ }
88
+ export function stringRecord(v) {
89
+ if (typeof v !== "object" || v === null)
90
+ return {};
91
+ const out = {};
92
+ for (const [k, val] of Object.entries(v)) {
93
+ if (typeof val === "string")
94
+ out[k] = val;
95
+ }
96
+ return out;
97
+ }
98
+ export function hasDep(pkg, name) {
99
+ return name in pkg.dependencies || name in pkg.devDependencies;
100
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { match, P } from "ts-pattern";
2
+ import { runInstallCommand } from "./install.js";
3
+ const argv = process.argv.slice(2);
4
+ const sub = argv[0] ?? "install";
5
+ const rest = argv.slice(1);
6
+ match(sub)
7
+ .with("install", () => {
8
+ runCommand(runInstallCommand(rest));
9
+ })
10
+ .with("--help", () => {
11
+ printUsage();
12
+ process.exit(0);
13
+ })
14
+ .with("-h", () => {
15
+ printUsage();
16
+ process.exit(0);
17
+ })
18
+ .with(P.string, () => {
19
+ process.stderr.write(`sadist: unknown command: ${sub}\n`);
20
+ printUsage();
21
+ process.exit(1);
22
+ })
23
+ .exhaustive();
24
+ function runCommand(r) {
25
+ match(r)
26
+ .with({ ok: true }, (v) => {
27
+ process.stdout.write(`sadist: ${v.value}\n`);
28
+ process.exit(0);
29
+ })
30
+ .with({ ok: false }, (e) => {
31
+ process.stderr.write(`sadist: error: ${e.error}\n`);
32
+ process.exit(1);
33
+ })
34
+ .exhaustive();
35
+ }
36
+ function printUsage() {
37
+ process.stdout.write([
38
+ "sadist - A sadistic TypeScript gate that holds your commit hostage until the code is no longer a liability.",
39
+ "",
40
+ "usage:",
41
+ " sadist install [flags] configure a project with the gate",
42
+ " sadist --help show this help",
43
+ "",
44
+ "flags:",
45
+ " --no-husky skip pre-commit hook setup",
46
+ " --no-tsconfig skip tsconfig.json creation",
47
+ " --force overwrite existing files",
48
+ " --dry-run print plan without applying changes",
49
+ "",
50
+ ].join("\n"));
51
+ }
@@ -0,0 +1,8 @@
1
+ import type { InstallFlags, InstallPlan, PkgJson, Result } from "./types.js";
2
+ export declare function buildPlan(pkg: PkgJson, flags: InstallFlags): Result<InstallPlan, string>;
3
+ export declare function printPlan(plan: InstallPlan): void;
4
+ export declare function installMissing(plan: InstallPlan, flags: InstallFlags): Result<string, string>;
5
+ export declare function applyConfigs(flags: InstallFlags, force: boolean): Result<string, string>;
6
+ export declare function applyPackageJson(pkg: PkgJson, flags: InstallFlags): Result<string, string>;
7
+ export declare function applyHusky(flags: InstallFlags, pkgManager?: "pnpm" | "yarn" | "npm"): Result<string, string>;
8
+ export declare function runInstallCommand(argv: readonly string[]): Result<string, string>;
@@ -0,0 +1,297 @@
1
+ import { match, P } from "ts-pattern";
2
+ import { hasDep, pathExists, readPkg, writeText, } from "./files.js";
3
+ import { addCmd, detectPkgManager, exec, isGitRepo } from "./process.js";
4
+ import { eslintConfigTemplate, gitignoreTemplate, preCommitTemplate, tsconfigTemplate, } from "./templates.js";
5
+ const LAX_SCRIPT = "eslint . && tsc --noEmit";
6
+ const PREPARE_SCRIPT = "husky";
7
+ export function buildPlan(pkg, flags) {
8
+ const pkgManager = detectPkgManager();
9
+ const missing = computeMissing(pkg, flags);
10
+ const out = emptyOut();
11
+ collectPlans(pkg, flags, out);
12
+ return ok({
13
+ pkgManager,
14
+ missing,
15
+ willCreate: out.create,
16
+ willWarn: out.warn,
17
+ });
18
+ }
19
+ function emptyOut() {
20
+ return { create: [], warn: [] };
21
+ }
22
+ function collectPlans(pkg, flags, out) {
23
+ planEslint(out);
24
+ planTsconfig(flags, out);
25
+ planLaxScript(pkg, out);
26
+ planModuleType(pkg, out);
27
+ planHusky(flags, pkg, out);
28
+ planGitignore(out);
29
+ }
30
+ function planEslint(out) {
31
+ if (!pathExists("eslint.config.mjs")) {
32
+ out.create.push("eslint.config.mjs");
33
+ return;
34
+ }
35
+ out.warn.push("eslint.config.mjs already exists; add 'import strict from \"sadist/config/strict\"' and spread ...strict manually");
36
+ }
37
+ function planTsconfig(flags, out) {
38
+ if (flags.noTsconfig)
39
+ return;
40
+ if (!pathExists("tsconfig.json")) {
41
+ out.create.push("tsconfig.json");
42
+ return;
43
+ }
44
+ out.warn.push("tsconfig.json already exists; ensure strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes are enabled");
45
+ }
46
+ function planLaxScript(pkg, out) {
47
+ const current = pkg.scripts["lax"];
48
+ if (!current) {
49
+ out.create.push("package.json#scripts.lax");
50
+ return;
51
+ }
52
+ if (current !== LAX_SCRIPT) {
53
+ out.warn.push(`package.json#scripts.lax exists with value "${current}"; expected "${LAX_SCRIPT}"`);
54
+ }
55
+ }
56
+ function planModuleType(pkg, out) {
57
+ if (pkg.type.some === false) {
58
+ out.warn.push("package.json missing \"type\": \"module\"");
59
+ return;
60
+ }
61
+ if (pkg.type.value !== "module") {
62
+ out.warn.push(`package.json#type is "${pkg.type.value}"; expected "module"`);
63
+ }
64
+ }
65
+ function planHusky(flags, pkg, out) {
66
+ if (flags.noHusky)
67
+ return;
68
+ if (!pathExists(".husky/pre-commit")) {
69
+ out.create.push(".husky/pre-commit");
70
+ }
71
+ else {
72
+ out.warn.push(".husky/pre-commit already exists");
73
+ }
74
+ const prep = pkg.scripts["prepare"];
75
+ if (!prep) {
76
+ out.create.push("package.json#scripts.prepare");
77
+ }
78
+ else if (prep !== PREPARE_SCRIPT) {
79
+ out.warn.push(`package.json#scripts.prepare is "${prep}"; expected "${PREPARE_SCRIPT}"`);
80
+ }
81
+ }
82
+ function planGitignore(out) {
83
+ if (!pathExists(".gitignore")) {
84
+ out.create.push(".gitignore");
85
+ return;
86
+ }
87
+ out.warn.push(".gitignore already exists; review recommended entries");
88
+ }
89
+ export function printPlan(plan) {
90
+ process.stdout.write(`\n[plan] package manager: ${plan.pkgManager}\n`);
91
+ printSection("install", plan.missing, formatDep);
92
+ printSection("create", plan.willCreate, formatCreate);
93
+ printSection("warn", plan.willWarn, formatWarn);
94
+ process.stdout.write("\n");
95
+ }
96
+ function printSection(label, items, fmt) {
97
+ if (items.length === 0) {
98
+ process.stdout.write(`[plan] ${label}: (none)\n`);
99
+ return;
100
+ }
101
+ process.stdout.write(`[plan] ${label}:\n`);
102
+ for (const it of items)
103
+ process.stdout.write(` ${fmt(it)}\n`);
104
+ }
105
+ function formatDep(d) {
106
+ return `- ${d.name} (${d.class}) ${d.range}`;
107
+ }
108
+ function formatCreate(f) {
109
+ return `+ ${f}`;
110
+ }
111
+ function formatWarn(w) {
112
+ return `! ${w}`;
113
+ }
114
+ export function installMissing(plan, flags) {
115
+ if (flags.dryRun)
116
+ return { ok: true, value: "dry-run" };
117
+ return installByClass(plan.pkgManager, plan.missing);
118
+ }
119
+ function installByClass(pkg, missing) {
120
+ const r1 = runClass(pkg, missing, "dev");
121
+ if (!r1.ok)
122
+ return r1;
123
+ return runClass(pkg, missing, "prod");
124
+ }
125
+ function runClass(pkg, missing, klass) {
126
+ const deps = missing.filter((d) => d.class === klass).map((d) => `${d.name}@${d.range}`);
127
+ if (deps.length === 0)
128
+ return { ok: true, value: "noop" };
129
+ const [cmd, args] = addCmd(pkg, klass, deps);
130
+ return exec(cmd, args);
131
+ }
132
+ export function applyConfigs(flags, force) {
133
+ if (flags.dryRun)
134
+ return { ok: true, value: "dry-run" };
135
+ return writeAllConfigs({ flags, force });
136
+ }
137
+ function writeAllConfigs(ctx) {
138
+ const r1 = maybeWrite(ctx, "tsconfig.json", tsconfigTemplate);
139
+ if (!r1.ok)
140
+ return r1;
141
+ const r2 = maybeWrite(ctx, "eslint.config.mjs", eslintConfigTemplate);
142
+ if (!r2.ok)
143
+ return r2;
144
+ return maybeWrite(ctx, ".gitignore", gitignoreTemplate);
145
+ }
146
+ function maybeWrite(ctx, file, content) {
147
+ return match(mustWrite(ctx, file))
148
+ .with(true, () => writeText(file, content))
149
+ .with(false, () => ok("skip"))
150
+ .exhaustive();
151
+ }
152
+ function mustWrite(ctx, file) {
153
+ if (ctx.flags.noTsconfig && file === "tsconfig.json")
154
+ return false;
155
+ return ctx.force || !pathExists(file);
156
+ }
157
+ export function applyPackageJson(pkg, flags) {
158
+ if (flags.dryRun)
159
+ return { ok: true, value: "dry-run" };
160
+ const currentPkgR = readPkg();
161
+ if (!currentPkgR.ok)
162
+ return currentPkgR;
163
+ const next = buildNextPkg(currentPkgR.value, flags);
164
+ const json = `${JSON.stringify(next, null, 2)}\n`;
165
+ return writeText("package.json", json);
166
+ }
167
+ function buildNextPkg(pkg, flags) {
168
+ const scripts = mergeScripts(pkg, flags);
169
+ return {
170
+ ...pkg.root,
171
+ type: pkg.type.some ? pkg.type.value : "module",
172
+ scripts,
173
+ };
174
+ }
175
+ function mergeScripts(pkg, flags) {
176
+ const scripts = { ...pkg.scripts };
177
+ if (!scripts["lax"])
178
+ scripts["lax"] = LAX_SCRIPT;
179
+ if (!flags.noHusky && !scripts["prepare"]) {
180
+ scripts["prepare"] = PREPARE_SCRIPT;
181
+ }
182
+ return scripts;
183
+ }
184
+ export function applyHusky(flags, pkgManager = "npm") {
185
+ if (flags.noHusky)
186
+ return { ok: true, value: "skipped" };
187
+ if (flags.dryRun)
188
+ return { ok: true, value: "dry-run" };
189
+ return writeHuskyHook(pkgManager);
190
+ }
191
+ function writeHuskyHook(pkgManager) {
192
+ if (!isGitRepo()) {
193
+ return { ok: false, error: ".git not found; init a repo before husky" };
194
+ }
195
+ if (!pathExists(".husky/pre-commit")) {
196
+ const r = writeText(".husky/pre-commit", preCommitTemplate(pkgManager), 0o755);
197
+ if (!r.ok)
198
+ return r;
199
+ }
200
+ return { ok: true, value: "husky" };
201
+ }
202
+ export function runInstallCommand(argv) {
203
+ const flags = parseFlags(argv);
204
+ if (!flags.ok)
205
+ return flags;
206
+ return runWith(flags.value);
207
+ }
208
+ function runWith(flags) {
209
+ const pkgR = readPkg();
210
+ if (!pkgR.ok)
211
+ return pkgR;
212
+ const planR = buildPlan(pkgR.value, flags);
213
+ if (!planR.ok)
214
+ return planR;
215
+ printPlan(planR.value);
216
+ return executeSteps(pkgR.value, planR.value, flags);
217
+ }
218
+ function executeSteps(pkg, plan, flags) {
219
+ const steps = [
220
+ () => installMissing(plan, flags),
221
+ () => applyPackageJson(pkg, flags),
222
+ () => applyConfigs(flags, flags.force),
223
+ () => applyHusky(flags, plan.pkgManager),
224
+ ];
225
+ for (const step of steps) {
226
+ const r = step();
227
+ if (!r.ok)
228
+ return r;
229
+ }
230
+ return { ok: true, value: "done" };
231
+ }
232
+ function computeMissing(pkg, flags) {
233
+ const required = [
234
+ { name: "sadist", class: "dev", range: "0.1.0" },
235
+ { name: "typescript", class: "dev", range: "5.6.3" },
236
+ { name: "eslint", class: "dev", range: "9.39.5" },
237
+ { name: "@typescript-eslint/parser", class: "dev", range: "8.48.1" },
238
+ { name: "ts-pattern", class: "prod", range: "5.9.0" },
239
+ ...(flags.noHusky
240
+ ? []
241
+ : [{ name: "husky", class: "dev", range: "9.1.7" }]),
242
+ ];
243
+ return required.filter((d) => !hasDep(pkg, d.name));
244
+ }
245
+ function parseFlags(argv) {
246
+ const flags = {
247
+ noHusky: false,
248
+ noTsconfig: false,
249
+ force: false,
250
+ dryRun: false,
251
+ };
252
+ for (const a of argv) {
253
+ const r = parseOne(a, flags);
254
+ if (!r.ok)
255
+ return r;
256
+ }
257
+ return { ok: true, value: flags };
258
+ }
259
+ function parseOne(flag, flags) {
260
+ return match(flag)
261
+ .with("--no-husky", () => okMut(flags, "noHusky"))
262
+ .with("--no-tsconfig", () => okMut(flags, "noTsconfig"))
263
+ .with("--force", () => okMut(flags, "force"))
264
+ .with("--dry-run", () => okMut(flags, "dryRun"))
265
+ .with("--help", () => {
266
+ printHelp();
267
+ process.exit(0);
268
+ return ok(flags);
269
+ })
270
+ .with(P.string, (f) => ({
271
+ ok: false,
272
+ error: `unknown flag: ${f}`,
273
+ }))
274
+ .exhaustive();
275
+ }
276
+ function ok(value) {
277
+ return { ok: true, value };
278
+ }
279
+ function okMut(flags, key) {
280
+ flags[key] = true;
281
+ return { ok: true, value: flags };
282
+ }
283
+ function printHelp() {
284
+ process.stdout.write([
285
+ "sadist install - configure a project with the sadist gate",
286
+ "",
287
+ "usage: sadist install [flags]",
288
+ "",
289
+ "flags:",
290
+ " --no-husky skip pre-commit hook setup",
291
+ " --no-tsconfig skip tsconfig.json creation",
292
+ " --force overwrite existing files",
293
+ " --dry-run print plan without applying changes",
294
+ " --help show this help",
295
+ "",
296
+ ].join("\n"));
297
+ }
@@ -0,0 +1,5 @@
1
+ import type { PkgManager, Result } from "./types.js";
2
+ export declare function detectPkgManager(): PkgManager;
3
+ export declare function exec(cmd: string, args: readonly string[]): Result<string, string>;
4
+ export declare function addCmd(pkg: PkgManager, klass: "dev" | "prod", deps: readonly string[]): readonly [string, string[]];
5
+ export declare function isGitRepo(): boolean;
@@ -0,0 +1,42 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { pathExists } from "./files.js";
5
+ export function detectPkgManager() {
6
+ if (pathExists("pnpm-lock.yaml"))
7
+ return "pnpm";
8
+ if (pathExists("yarn.lock"))
9
+ return "yarn";
10
+ return "npm";
11
+ }
12
+ export function exec(cmd, args) {
13
+ const r = spawnSync(cmd, args, {
14
+ cwd: process.cwd(),
15
+ stdio: "inherit",
16
+ encoding: "utf8",
17
+ });
18
+ if (r.status !== 0) {
19
+ return {
20
+ ok: false,
21
+ error: `${cmd} ${args.join(" ")} exited with code ${String(r.status)}`,
22
+ };
23
+ }
24
+ return { ok: true, value: "" };
25
+ }
26
+ export function addCmd(pkg, klass, deps) {
27
+ if (pkg === "pnpm") {
28
+ const args = klass === "dev" ? ["add", "-D", ...deps] : ["add", ...deps];
29
+ return ["pnpm", args];
30
+ }
31
+ if (pkg === "yarn") {
32
+ const args = klass === "dev" ? ["add", "-D", ...deps] : ["add", ...deps];
33
+ return ["yarn", args];
34
+ }
35
+ const args = klass === "dev"
36
+ ? ["install", "--save-dev", ...deps]
37
+ : ["install", "--save", ...deps];
38
+ return ["npm", args];
39
+ }
40
+ export function isGitRepo() {
41
+ return existsSync(resolve(process.cwd(), ".git"));
42
+ }
@@ -0,0 +1,4 @@
1
+ export declare const eslintConfigTemplate = "import strict from \"sadist/config/strict\";\nimport tsParser from \"@typescript-eslint/parser\";\n\nexport default [\n ...strict,\n {\n files: [\"src/**/*.ts\"],\n languageOptions: {\n parser: tsParser,\n },\n },\n];\n";
2
+ export declare const tsconfigTemplate = "{\n \"compilerOptions\": {\n \"strict\": true,\n \"noUncheckedIndexedAccess\": true,\n \"exactOptionalPropertyTypes\": true,\n \"noImplicitOverride\": true,\n \"noPropertyAccessFromIndexSignature\": true,\n \"target\": \"ES2022\",\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"declaration\": true,\n \"skipLibCheck\": true\n },\n \"include\": [\"src\"]\n}\n";
3
+ export declare function preCommitTemplate(pkgManager?: "pnpm" | "yarn" | "npm"): string;
4
+ export declare const gitignoreTemplate = "# dependencies\nnode_modules/\n.pnpm-store/\n\n# build output\ndist/\n\n# environment\n.env\n.env.*\n\n# editor\n.vscode/\n.idea/\n.zed/\n\n# os\n.DS_Store\nThumbs.db\n";
@@ -0,0 +1,56 @@
1
+ export const eslintConfigTemplate = `import strict from "sadist/config/strict";
2
+ import tsParser from "@typescript-eslint/parser";
3
+
4
+ export default [
5
+ ...strict,
6
+ {
7
+ files: ["src/**/*.ts"],
8
+ languageOptions: {
9
+ parser: tsParser,
10
+ },
11
+ },
12
+ ];
13
+ `;
14
+ export const tsconfigTemplate = `{
15
+ "compilerOptions": {
16
+ "strict": true,
17
+ "noUncheckedIndexedAccess": true,
18
+ "exactOptionalPropertyTypes": true,
19
+ "noImplicitOverride": true,
20
+ "noPropertyAccessFromIndexSignature": true,
21
+ "target": "ES2022",
22
+ "module": "NodeNext",
23
+ "moduleResolution": "NodeNext",
24
+ "declaration": true,
25
+ "skipLibCheck": true
26
+ },
27
+ "include": ["src"]
28
+ }
29
+ `;
30
+ export function preCommitTemplate(pkgManager = "npm") {
31
+ if (pkgManager === "pnpm")
32
+ return "pnpm run lax\n";
33
+ if (pkgManager === "yarn")
34
+ return "yarn lax\n";
35
+ return "npm run lax\n";
36
+ }
37
+ export const gitignoreTemplate = `# dependencies
38
+ node_modules/
39
+ .pnpm-store/
40
+
41
+ # build output
42
+ dist/
43
+
44
+ # environment
45
+ .env
46
+ .env.*
47
+
48
+ # editor
49
+ .vscode/
50
+ .idea/
51
+ .zed/
52
+
53
+ # os
54
+ .DS_Store
55
+ Thumbs.db
56
+ `;
@@ -0,0 +1,49 @@
1
+ export type Result<T, E> = {
2
+ ok: true;
3
+ value: T;
4
+ } | {
5
+ ok: false;
6
+ error: E;
7
+ };
8
+ export declare function ok<T>(value: T): {
9
+ ok: true;
10
+ value: T;
11
+ };
12
+ export declare function err<E>(error: E): {
13
+ ok: false;
14
+ error: E;
15
+ };
16
+ export type Option<T> = {
17
+ some: true;
18
+ value: T;
19
+ } | {
20
+ some: false;
21
+ };
22
+ export type PkgManager = "pnpm" | "yarn" | "npm";
23
+ export type InstallFlags = {
24
+ readonly noHusky: boolean;
25
+ readonly noTsconfig: boolean;
26
+ readonly force: boolean;
27
+ readonly dryRun: boolean;
28
+ };
29
+ export type PkgJson = {
30
+ readonly name: string;
31
+ readonly version: string;
32
+ readonly type: Option<string>;
33
+ readonly scripts: Readonly<Record<string, string>>;
34
+ readonly dependencies: Readonly<Record<string, string>>;
35
+ readonly devDependencies: Readonly<Record<string, string>>;
36
+ readonly root: Readonly<Record<string, unknown>>;
37
+ };
38
+ export type DepClass = "dev" | "prod";
39
+ export type MissingDep = {
40
+ readonly name: string;
41
+ readonly class: DepClass;
42
+ readonly range: string;
43
+ };
44
+ export type InstallPlan = {
45
+ readonly pkgManager: PkgManager;
46
+ readonly missing: readonly MissingDep[];
47
+ readonly willCreate: readonly string[];
48
+ readonly willWarn: readonly string[];
49
+ };
@@ -0,0 +1,6 @@
1
+ export function ok(value) {
2
+ return { ok: true, value };
3
+ }
4
+ export function err(error) {
5
+ return { ok: false, error };
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sadist",
3
- "version": "0.1.0",
4
- "description": "A merciless TypeScript code quality gate.",
3
+ "version": "0.2.0",
4
+ "description": "A sadistic TypeScript gate that holds your commit hostage until the code is no longer a liability.",
5
5
  "license": "MPL-2.0",
6
6
  "keywords": [
7
7
  "typescript",
@@ -9,6 +9,7 @@
9
9
  "pre-commit",
10
10
  "code-quality",
11
11
  "strict",
12
+ "gate",
12
13
  "lint"
13
14
  ],
14
15
  "repository": {
@@ -22,12 +23,16 @@
22
23
  "type": "module",
23
24
  "main": "./dist/index.js",
24
25
  "types": "./dist/index.d.ts",
26
+ "bin": {
27
+ "sadist": "./bin/sadist.js"
28
+ },
25
29
  "exports": {
26
30
  ".": "./dist/index.js",
27
31
  "./config/strict": "./dist/config/strict.js"
28
32
  },
29
33
  "files": [
30
34
  "dist",
35
+ "bin",
31
36
  "docs/SKILL.md"
32
37
  ],
33
38
  "peerDependencies": {
@@ -42,7 +47,8 @@
42
47
  "vitest": "2.1.9",
43
48
  "typescript": "5.6.3",
44
49
  "eslint": "9.39.5",
45
- "@typescript-eslint/parser": "8.48.1"
50
+ "@typescript-eslint/parser": "8.48.1",
51
+ "@types/node": "24.13.3"
46
52
  },
47
53
  "scripts": {
48
54
  "build": "tsc -p tsconfig.build.json",